}
function fmtMoney(n) {
  if (n == null) return "—";
  return "$" + Number(n).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function plural(n, w) { return `${n} ${w}${n === 1 ? "" : "s"}`; }

/* ---------- API client ---------- */
const CSRF = {
  get() { return getCookie("mailer_csrf"); },
};
function getCookie(name) {
  return document.cookie.split("; ").find(r => r.startsWith(name + "="))?.split("=").slice(1).join("=") || "";
}

async function api(path, opts = {}) {
  const headers = Object.assign({}, opts.headers || {});
  if (opts.json !== undefined) {
    headers["Content-Type"] = "application/json";
    opts.body = JSON.stringify(opts.json);
    delete opts.json;
  } else if (opts.body && typeof opts.body === "object" &&
             !(opts.body instanceof FormData) && !(opts.body instanceof Blob) &&
             !(opts.body instanceof URLSearchParams) && !(opts.body instanceof ArrayBuffer)) {
    // plain object → JSON body (guards against accidental "[object Object]" sends)
    headers["Content-Type"] = "application/json";
    opts.body = JSON.stringify(opts.body);
  }
  if ((opts.method || "GET").toUpperCase() !== "GET") headers["X-CSRF-Token"] = CSRF.get();
  const res = await fetch(path, Object.assign({}, opts, { headers, credentials: "same-origin" }));
  let data = null;
  try { data = await res.json(); } catch { /* no body */ }
  if (res.status === 401 && !location.pathname.startsWith("/login")) {
    location.href = "/login?next=" + encodeURIComponent(location.pathname);
    throw new Error("Not authenticated");
  }
  if (!res.ok) {
    let detail = data && (data.detail?.message || data.detail || data.error);
    // Pydantic 422: detail is [{loc:[...], msg:'...'}] — surface the msg
    if (Array.isArray(detail)) {
      detail = detail.map(e => {
        let field = Array.isArray(e?.loc) ? e.loc.filter(x => typeof x === "string").join(".") : "";
        field = field.replace(/^body\./, "");
        let msg = e?.msg || String(e);
        msg = msg.replace(/^Value error,\s*/i, "");
        return (field ? field + ": " : "") + msg;
      }).join("; ");
    }
    if (detail && typeof detail === "object") detail = JSON.stringify(detail);
    const err = new Error(typeof detail === "string" && detail ? detail : (res.status + " " + res.statusText));
    err.status = res.status;
    err.data = data;
    throw err;
  }
  return data;
}
const GET = p => api(p);
const POST = (p, json) => api(p, { method: "POST", json });

function b64Blob(b64, mime) {
  const bin = atob(b64);
  const arr = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
  return new Blob([arr], { type: mime });
}
function dlBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click();
  setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 1500);
}
const PATCH = (p, json) => api(p, { method: "PATCH", json });
const PUT = (p, json) => api(p, { method: "PUT", json });
const DEL = p => api(p, { method: "DELETE" });

