// ── 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;
}

// Tenant LMS hosts get a t_-prefixed, host-only CSRF cookie (so a parent-domain
// cookie of the main LMS can never shadow it); prefer it when present.
const csrfCookie = () => getCookie("t_cl_csrf") || getCookie("cl_csrf");

// 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 = csrfCookie();
  if (!t) {
    try {
      const r = await fetch(API_BASE + "/csrf", { credentials: "include" });
      const d = await r.json();
      t = d.csrfToken || csrfCookie();
    } 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 = csrfCookie() || (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>
);

// ── Navigation (GET /api/navigation) ─────────────────────────────────────────
// Loaded on page load and again after sign-in / sign-out. The school's brand,
// its feature switches and the user's menus all come from the server; until it
// answers (or if it fails) callers fall back to their built-in menus, so the
// LMS never loses its navigation.
const Nav = { data: null, subs: new Set() };
async function loadNavigation() {
  try {
    const r = await apiFetch("/navigation");
    if (r.ok) { Nav.data = await r.json(); applyBrand(Nav.data.brand); }
  } catch { /* keep the last good copy */ }
  Nav.subs.forEach((f) => f());
}

// The school's branding on the page itself: tab title, favicon, accent colour.
// Anything the school left empty keeps the EpiqMinds default.
const DEFAULT_TITLE = document.title;
function applyBrand(brand) {
  if (!brand) return;
  const custom = brand.name && brand.name !== "EpiqMinds";
  document.title = brand.title || (custom ? brand.name : DEFAULT_TITLE);
  if (brand.logoUrl) {
    let icon = document.querySelector("link[rel~='icon']");
    if (!icon) { icon = document.createElement("link"); icon.rel = "icon"; document.head.appendChild(icon); }
    icon.href = brand.logoUrl;
  }
  // One rule that beats the student themes' own --coral (.theme-space etc.).
  // The server only stores strict colour syntax, so this can't inject CSS.
  let style = document.getElementById("brand-accent");
  if (brand.accent) {
    if (!style) { style = document.createElement("style"); style.id = "brand-accent"; document.head.appendChild(style); }
    style.textContent = `html, html * { --coral: ${brand.accent} !important; }`;
  } else if (style) style.remove();
  // The look: "professional" restyles via CSS in index.html.
  if (brand.preset === "professional") document.documentElement.dataset.preset = "professional";
  else delete document.documentElement.dataset.preset;
  applyTerms(brand.terms);
}

// ── School terms (branding.terms): "Student" → "Educator", "Class" → "Cohort" …
// Rewrites rendered text nodes, so every screen follows without each of them
// needing a lookup. Lesson bodies, code and editable fields are left alone.
// ponytail: DOM text rewrite — it also renames these words inside plain-text
// user content (e.g. a course called "Student kit"); move hot spots to a t()
// helper if that ever matters.
const TERM_BASE = { student: "Student", teacher: "Teacher", parent: "Parent", class: "Class", batch: "Batch" };
const pluralOf = (w) => /(s|x|z|ch|sh)$/i.test(w) ? w + "es" : /[^aeiou]y$/i.test(w) ? w.slice(0, -1) + "ies" : w + "s";
const caseLike = (src, w) =>
  src === src.toUpperCase() && src.length > 1 ? w.toUpperCase() : src[0] === src[0].toUpperCase() ? w[0].toUpperCase() + w.slice(1) : w.toLowerCase();
const Terms = { re: null, map: null, observer: null };
const TERM_SKIP = ".rich-text, [contenteditable], code, pre, script, style, textarea";

function renameText(node) {
  if (!Terms.re || !node.nodeValue || !node.parentElement || node.parentElement.closest(TERM_SKIP)) return;
  const next = node.nodeValue.replace(Terms.re, (m) => caseLike(m, Terms.map.get(m.toLowerCase())));
  if (next !== node.nodeValue) node.nodeValue = next;
}
function renameUnder(root) {
  if (root.nodeType === 3) return renameText(root);
  if (root.nodeType !== 1) return;
  const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
  for (let n = walk.nextNode(); n; n = walk.nextNode()) renameText(n);
}
function applyTerms(terms) {
  const pairs = [];
  for (const [k, base] of Object.entries(TERM_BASE)) {
    const to = terms && typeof terms[k] === "string" ? terms[k].trim() : "";
    if (to && to.toLowerCase() !== base.toLowerCase()) pairs.push([pluralOf(base), pluralOf(to)], [base, to]);
  }
  Terms.re = pairs.length ? new RegExp(`\\b(${pairs.map((p) => p[0]).join("|")})\\b`, "gi") : null;
  Terms.map = new Map(pairs.map(([a, b]) => [a.toLowerCase(), b]));
  if (!Terms.re) { if (Terms.observer) Terms.observer.disconnect(); Terms.observer = null; return; }
  if (!Terms.observer) {
    Terms.observer = new MutationObserver((records) => {
      for (const r of records) r.type === "characterData" ? renameText(r.target) : r.addedNodes.forEach(renameUnder);
      Terms.observer.takeRecords(); // our own edits — don't loop on them
    });
    Terms.observer.observe(document.body, { childList: true, subtree: true, characterData: true });
  }
  renameUnder(document.body);
}
function useNav() {
  const [, rerender] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => { Nav.subs.add(rerender); return () => Nav.subs.delete(rerender); }, []);
  return Nav.data;
}
// [{ title, items: [{ key, label, icon, path }] }] from the server, or null.
const navMenu = (nav, name) => (nav && nav.menus && nav.menus[name]) || null;
const pageBlocked = (nav, route) => !!nav && (nav.blockedPages || []).some((p) => route === p || route.startsWith(p + "/"));
const featureOn = (nav, key) => !nav || !nav.flags || nav.flags[key] !== false;
// The school's logo + name; EpiqMinds keeps its two-tone wordmark.
const BrandMark = ({ nav, logoSize = 22 }) => {
  const brand = (nav && nav.brand) || {};
  const name = brand.name || "EpiqMinds";
  const logo = brand.logoUrl
    ? <img src={brand.logoUrl} alt="" style={{ height: logoSize, width: "auto", maxWidth: logoSize * 4, objectFit: "contain", verticalAlign: "middle", marginRight: 8 }} />
    : null;
  if (name === "EpiqMinds") return <>{logo}<span style={{ color: "var(--coral)" }}>Epiq</span>Minds</>;
  return <>{logo}{name}</>;
};
// Where the header logo links: the school's website, else EpiqMinds.
const brandHref = (nav) => (nav && nav.brand && nav.brand.websiteUrl) || "https://epiqminds.com";

Object.assign(window, { Nav, loadNavigation, useNav, navMenu, pageBlocked, featureOn, BrandMark, brandHref });
