// ── API layer (cookie + CSRF auth) ───────────────────────────────────────────
// Auth tokens now live in httpOnly cookies set by the server, so there is no
// token in JS-readable storage. We send credentials with every request and
// attach the double-submit CSRF token (a JS-readable cookie) on mutations.
const API_BASE =
  window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"
    ? "http://localhost:3001/api"
    : "/api";

function getCookie(name) {
  const m = document.cookie.match("(?:^|; )" + name.replace(/([.$?*|{}()\[\]\\\/\+^])/g, "\\$1") + "=([^;]*)");
  return m ? decodeURIComponent(m[1]) : null;
}

// Make sure we have a CSRF token (the server sets it on any response, but fetch
// it explicitly if this is a very first interaction).
async function ensureCsrf() {
  let t = getCookie("cl_csrf");
  if (!t) {
    try {
      const r = await fetch(API_BASE + "/csrf", { credentials: "include" });
      const d = await r.json();
      t = d.csrfToken || getCookie("cl_csrf");
    } catch {
      /* ignore */
    }
  }
  return t;
}

const UNSAFE = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 };

// Serialize token refresh. After the 15m access token expires, a page load fires
// many parallel requests that ALL get 401 at once. If each posted /auth/refresh
// independently, the first would rotate the refresh cookie and the rest would
// present the now-stale token — the server's reuse-detection then revokes the
// whole token family, silently logging the user out and making every list go
// empty ("all data disappeared"). Sharing one in-flight refresh fixes that.
// Endpoints where a 401 must NOT trigger a refresh-retry: the refresh call
// itself (would recurse) and the unauthenticated auth endpoints where a 401
// means bad credentials, not an expired token. Crucially this does NOT include
// /auth/me — a page load after the 15m access token expires must be allowed to
// refresh, otherwise the user is silently logged out on every reload.
const NO_REFRESH_RETRY = /^\/auth\/(refresh|login|logout|signup|forgot|reset|verify-email)(\/|$|\?)/;

let _refreshPromise = null;
function refreshOnce() {
  if (!_refreshPromise) {
    _refreshPromise = fetch(API_BASE + "/auth/refresh", { method: "POST", credentials: "include" })
      .then((r) => r.ok)
      .catch(() => false)
      .finally(() => { _refreshPromise = null; });
  }
  return _refreshPromise;
}

async function apiFetch(endpoint, opts = {}) {
  const method = (opts.method || "GET").toUpperCase();
  const headers = { "Content-Type": "application/json", ...(opts.headers || {}) };

  if (UNSAFE[method]) {
    const token = getCookie("cl_csrf") || (await ensureCsrf());
    if (token) headers["X-CSRF-Token"] = token;
  }

  let res = await fetch(API_BASE + endpoint, { ...opts, headers, credentials: "include" });

  // Access token expired? Silently rotate once (shared across concurrent calls)
  // via the refresh cookie and retry.
  if (res.status === 401 && !opts._retry && !NO_REFRESH_RETRY.test(endpoint)) {
    const ok = await refreshOnce();
    if (ok) {
      return apiFetch(endpoint, { ...opts, _retry: true });
    }
  }
  return res;
}

// Convenience JSON helpers.
const apiGet = (endpoint) => apiFetch(endpoint).then((r) => r.json());
const apiSend = (endpoint, method, body) =>
  apiFetch(endpoint, { method, body: body != null ? JSON.stringify(body) : undefined });

// Global session holder so non-React helpers (sidebars, role gates) can read the
// current role/permissions without threading props everywhere. Populated by
// me()/login() and cleared on logout.
// `currency` is the organisation's billing currency from Settings → Currency,
// delivered by /auth/me because most roles can't read /settings directly.
const Session = { user: null, permissions: [], currency: "INR" };
function currentRole() {
  return Session.user ? Session.user.role : null;
}
function hasPerm(p) {
  if (currentRole() === "superadmin") return true;
  return (Session.permissions || []).indexOf(p) !== -1;
}

// ── Money ─────────────────────────────────────────────────────────────────────
// One place decides how an amount is rendered. Everything that shows a fee must
// go through these, so changing Settings → Currency changes the whole app rather
// than the handful of screens that happened to hardcode a symbol.
const CURRENCY_SYMBOL = { INR: "₹", USD: "$" };
// `code` is optional: pass a row's own currency when it has one (a generated
// payment link fixes the currency for that lead), otherwise the org default wins.
const curSym = (code) => CURRENCY_SYMBOL[String(code || Session.currency || "INR").toUpperCase()] || "";
// "—" for a missing amount, so callers don't each invent their own placeholder.
const fmtMoney = (n, code) =>
  n == null || n === "" ? "—" : curSym(code) + Number(n).toLocaleString(undefined, { maximumFractionDigits: 2 });

// Auth helpers used by the login/logout flows.
const authApi = {
  async login(email, password) {
    const r = await apiFetch("/auth/login", { method: "POST", body: JSON.stringify({ email, password }) });
    const data = await r.json().catch(() => ({}));
    if (!r.ok) throw new Error(data.error || "Sign in failed");
    await ensureCsrf();
    // Pull permissions for the freshly logged-in user.
    const full = await authApi.me();
    return full || data.user;
  },
  async me() {
    const r = await apiFetch("/auth/me");
    if (!r.ok) {
      Session.user = null;
      Session.permissions = [];
      return null;
    }
    const data = await r.json();
    Session.user = data.user;
    Session.permissions = data.permissions || [];
    if (data.org && data.org.currency) Session.currency = data.org.currency;
    return { ...data.user, permissions: Session.permissions };
  },
  async logout() {
    try {
      await apiFetch("/auth/logout", { method: "POST" });
    } finally {
      Session.user = null;
      Session.permissions = [];
    }
  },
};

Object.assign(window, { apiFetch, apiGet, apiSend, useApi, Spinner, authApi, Session, currentRole, hasPerm, curSym, fmtMoney });

const useApi = (endpoint, fallback = []) => {
  const [data, setData] = React.useState(fallback);
  const [loading, setLoading] = React.useState(!!endpoint);
  React.useEffect(() => {
    if (!endpoint) {
      setLoading(false);
      setData(fallback);
      return;
    }
    setLoading(true);
    apiFetch(endpoint)
      .then((r) => (r.ok ? r.json() : Promise.reject(r)))
      .then((d) => {
        // Paginated list endpoints return { data, total } — unwrap to the array
        // so existing components that expect an array keep working.
        setData(d && typeof d === "object" && Array.isArray(d.data) ? d.data : d);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, [endpoint]);
  return { data, loading };
};

// Coerce any list payload to an array. Paginated endpoints return
// { data, total, ... }; some return a bare array; errors may return an object.
// This guarantees callers can safely .filter/.map without a white-screen crash.
const asArray = (d) =>
  Array.isArray(d) ? d : (d && Array.isArray(d.data) ? d.data : []);

// Manual re-fetch of a list endpoint (used after a mutation). Always resolves to
// an array — unwraps the paginated { data, total } shape like useApi does.
const apiList = async (endpoint, opts) => {
  try {
    const r = await apiFetch(endpoint, opts);
    if (!r.ok) return [];
    return asArray(await r.json());
  } catch {
    return [];
  }
};

// Ensure a link is absolute so the browser doesn't treat "meet.google.com/x"
// as a path under our own domain. Returns "#" for empty so href stays valid.
const normalizeUrl = (u) => {
  if (!u) return "#";
  u = String(u).trim();
  if (!u) return "#";
  return /^https?:\/\//i.test(u) ? u : "https://" + u.replace(/^\/+/, "");
};

// ── Timezone-aware session times ──────────────────────────────────────────────
// Sessions store a wall-clock time + the IANA zone it was entered in (the
// teacher's, e.g. Asia/Kolkata). These helpers convert that to the viewer's
// local time so a US student sees the correct moment and countdown.
function tzOffsetMs(tz, utcMs) {
  const dtf = new Intl.DateTimeFormat("en-US", {
    timeZone: tz, hour12: false,
    year: "numeric", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit", second: "2-digit",
  });
  const p = dtf.formatToParts(new Date(utcMs)).reduce((a, x) => ((a[x.type] = x.value), a), {});
  const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day, (+p.hour) % 24, +p.minute, +p.second);
  return asUTC - utcMs;
}
// "YYYY-MM-DD" + "7:00 PM" interpreted in tz → the real Date instant.
function zonedMoment(dayStr, timeStr, tz) {
  if (!dayStr || !timeStr) return null;
  const m = String(timeStr).match(/^(\d+):(\d+)\s*(AM|PM)?/i);
  if (!m) return null;
  let h = +m[1], min = +m[2];
  if (m[3]) { const pm = /pm/i.test(m[3]); if (pm && h !== 12) h += 12; if (!pm && h === 12) h = 0; }
  const [Y, Mo, D] = String(dayStr).split("-").map(Number);
  if (!Y) return null;
  const guess = Date.UTC(Y, Mo - 1, D, h, min);
  return new Date(guess - tzOffsetMs(tz || "Asia/Kolkata", guess));
}
function fmtLocalTime(date) {
  if (!date) return "";
  return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(date);
}
function tzAbbr(tz, date) {
  try {
    const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(date || new Date());
    return (parts.find(p => p.type === "timeZoneName") || {}).value || "";
  } catch { return ""; }
}
function localTzAbbr(date) {
  try {
    const parts = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" }).formatToParts(date || new Date());
    return (parts.find(p => p.type === "timeZoneName") || {}).value || "";
  } catch { return ""; }
}

Object.assign(window, { asArray, apiList, normalizeUrl, zonedMoment, fmtLocalTime, tzAbbr, localTzAbbr });

const Spinner = () => (
  <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: 40 }}>
    <div style={{ width: 32, height: 32, border: "3px solid var(--rule)", borderTopColor: "var(--coral)", borderRadius: "50%", animation: "spin .7s linear infinite" }} />
    <style>{`@keyframes spin{to{transform:rotate(360deg)}}`}</style>
  </div>
);
