/* Veridyne Learning Portal — core: icons, UI primitives, store, router */
const { useState, useEffect, useCallback, useContext, createContext, useMemo, useRef } = React;

/* ---------------- Icons (24x24 stroke) ---------------- */
const ICON_PATHS = {
  home: <><path d="M3 10.5 12 3l9 7.5" /><path d="M5 9.5V21h14V9.5" /></>,
  book: <><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V2H6.5A2.5 2.5 0 0 0 4 4.5v15Z" /><path d="M4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5" /></>,
  shield: <><path d="M12 22s8-3.5 8-10V5l-8-3-8 3v7c0 6.5 8 10 8 10Z" /></>,
  flow: <><circle cx="5" cy="6" r="2.5" /><circle cx="19" cy="18" r="2.5" /><path d="M7.5 6H15a3 3 0 0 1 3 3v6.5" /><path d="m15 12.5 3 3 3-3" /></>,
  diagram: <><rect x="3" y="3" width="7" height="6" rx="1.5" /><rect x="14" y="15" width="7" height="6" rx="1.5" /><path d="M6.5 9v4a2 2 0 0 0 2 2H14" /></>,
  checkCircle: <><circle cx="12" cy="12" r="9" /><path d="m8.5 12.5 2.5 2.5 5-6" /></>,
  library: <><path d="M4 4v16" /><path d="M9 4v16" /><path d="m13.5 4.5 4.5 15" /></>,
  award: <><circle cx="12" cy="9" r="5.5" /><path d="m8.5 13.5-2 8 5.5-3 5.5 3-2-8" /></>,
  list: <><path d="M8 6h13" /><path d="M8 12h13" /><path d="M8 18h13" /><path d="M3.5 6h.01" /><path d="M3.5 12h.01" /><path d="M3.5 18h.01" /></>,
  lock: <><rect x="4" y="11" width="16" height="10" rx="2.5" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></>,
  logout: <><path d="M9 21H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3" /><path d="m16 17 5-5-5-5" /><path d="M21 12H9" /></>,
  menu: <><path d="M4 7h16" /><path d="M4 12h16" /><path d="M4 17h16" /></>,
  x: <><path d="m6 6 12 12" /><path d="m18 6-12 12" /></>,
  chevronRight: <path d="m9 5 7 7-7 7" />,
  chevronLeft: <path d="m15 5-7 7 7 7" />,
  chevronDown: <path d="m5 9 7 7 7-7" />,
  external: <><path d="M14 4h6v6" /><path d="m20 4-9 9" /><path d="M19 13v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></>,
  play: <path d="M7 4.5v15l13-7.5-13-7.5Z" />,
  replay: <><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 3v6h6" /></>,
  search: <><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></>,
  clock: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3.5 2" /></>,
  check: <path d="m4.5 12.5 5 5 10-11" />,
  alert: <><path d="M12 3 2.5 20h19L12 3Z" /><path d="M12 10v4" /><path d="M12 17.5h.01" /></>,
  info: <><circle cx="12" cy="12" r="9" /><path d="M12 11v6" /><path d="M12 7.5h.01" /></>,
  arrowRight: <><path d="M4 12h16" /><path d="m14 6 6 6-6 6" /></>,
  arrowLeft: <><path d="M20 12H4" /><path d="m10 6-6 6 6 6" /></>,
  fileText: <><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9l-6-6Z" /><path d="M14 3v6h6" /><path d="M9 13h6" /><path d="M9 17h6" /></>,
  video: <><rect x="2.5" y="6" width="13" height="12" rx="2.5" /><path d="m15.5 11 6-3.5v9l-6-3.5" /></>,
  zap: <path d="M13 2 4 14h6l-1 8 9-12h-6l1-8Z" />,
  key: <><circle cx="8" cy="15" r="4.5" /><path d="m11.5 11.5 9-9" /><path d="M17 6l3 3" /></>,
  users: <><circle cx="9" cy="8" r="3.5" /><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6" /><path d="M16 4.5a3.5 3.5 0 0 1 0 7" /><path d="M17.5 14.5c2.1.8 3.5 2.9 3.5 5.5" /></>,
  target: <><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="5" /><circle cx="12" cy="12" r="1" /></>,
  backspace: <><path d="M21 5H9L3 12l6 7h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1Z" /><path d="m12 9.5 5 5" /><path d="m17 9.5-5 5" /></>,
  layers: <><path d="m12 2 9 5-9 5-9-5 9-5Z" /><path d="m3 12 9 5 9-5" /><path d="m3 17 9 5 9-5" /></>,
  refresh: <><path d="M21 12a9 9 0 1 1-2.6-6.4" /><path d="M21 3v6h-6" /></>,
  grid: <><rect x="3" y="3" width="8" height="8" rx="1.5" /><rect x="13" y="3" width="8" height="8" rx="1.5" /><rect x="3" y="13" width="8" height="8" rx="1.5" /><rect x="13" y="13" width="8" height="8" rx="1.5" /></>,
};

function Icon({ name, size = 18, strokeWidth = 1.8, style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
      strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
      style={{ flexShrink: 0, ...style }} aria-hidden="true">
      {ICON_PATHS[name] || <circle cx="12" cy="12" r="9" />}
    </svg>
  );
}

/* ---------------- Brand mark ---------------- */
function VeridyneMark({ size = 36 }) {
  return <img src="assets/veridyne-mark.png" alt="Veridyne" width={size} height={size} style={{ display: "block", objectFit: "contain" }} />;
}

/* ---------------- Small UI primitives ---------------- */
const PRODUCT_META = {
  okta: { label: "Okta", badge: "badge-okta", varName: "--okta" },
  auth0: { label: "Auth0", badge: "badge-auth0", varName: "--auth0" },
  protocols: { label: "Protocols", badge: "badge-proto", varName: "--proto" },
};

function ProductBadge({ product }) {
  const m = PRODUCT_META[product] || PRODUCT_META.protocols;
  return <span className={"badge " + m.badge}>{m.label}</span>;
}

function DifficultyBadge({ level }) {
  const cls = level === "Foundation" ? "badge-ok" : level === "Practitioner" ? "badge-warn" : "badge-ember";
  return <span className={"badge " + cls}>{level}</span>;
}

function OfficialBadge() {
  return <span className="badge badge-ok"><Icon name="checkCircle" size={12} /> Official source</span>;
}

function ProgressRing({ value, size = 64, stroke = 5, color = "var(--ember-bright)", label }) {
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const pct = Math.max(0, Math.min(100, value));
  return (
    <div style={{ position: "relative", width: size, height: size, flexShrink: 0 }}>
      <svg width={size} height={size}>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="rgba(229,221,208,0.12)" strokeWidth={stroke} />
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={color} strokeWidth={stroke}
          strokeLinecap="round" strokeDasharray={c} strokeDashoffset={c * (1 - pct / 100)}
          transform={`rotate(-90 ${size / 2} ${size / 2})`}
          style={{ transition: "stroke-dashoffset 0.8s cubic-bezier(0.2,0.7,0.2,1)" }} />
      </svg>
      <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", fontSize: size * 0.22, fontWeight: 600 }}>
        {label != null ? label : Math.round(pct) + "%"}
      </div>
    </div>
  );
}

function Accordion({ title, icon, children, defaultOpen = false, count }) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <div className="glass-inset" style={{ overflow: "hidden" }}>
      <button onClick={() => setOpen(!open)} aria-expanded={open}
        style={{ width: "100%", display: "flex", alignItems: "center", gap: 10, padding: "14px 16px", background: "none", border: "none", cursor: "pointer", textAlign: "left", minHeight: 48 }}>
        {icon ? <Icon name={icon} size={17} style={{ color: "var(--ember-bright)" }} /> : null}
        <span style={{ fontWeight: 600, flex: 1, fontSize: "0.95em" }}>{title}</span>
        {count != null ? <span className="badge">{count}</span> : null}
        <Icon name="chevronDown" size={16} style={{ transform: open ? "rotate(180deg)" : "none", transition: "transform 0.25s", color: "var(--bone-50)" }} />
      </button>
      {open ? <div className="anim-fade-in" style={{ padding: "2px 16px 16px" }}>{children}</div> : null}
    </div>
  );
}

function DocLinks({ docs, compact }) {
  if (!docs || !docs.length) return null;
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      {docs.map((d, i) => (
        <a key={i} href={d.url} target="_blank" rel="noopener noreferrer"
          style={{ display: "flex", alignItems: "center", gap: 10, padding: compact ? "8px 12px" : "10px 14px", borderRadius: 12, background: "rgba(16,23,29,0.4)", border: "1px solid rgba(229,221,208,0.08)", color: "var(--bone)", fontSize: "0.9em", textDecoration: "none" }}>
          <Icon name="fileText" size={15} style={{ color: "var(--ember-bright)" }} />
          <span style={{ flex: 1 }}>{d.label}</span>
          <OfficialBadge />
          <Icon name="external" size={14} style={{ color: "var(--bone-50)" }} />
        </a>
      ))}
    </div>
  );
}

/* ---------------- localStorage store ---------------- */
const LS = {
  session: "veridyne_session",
  cacheUser: "veridyne_cache_user",
  dirty: "veridyne_dirty",
  completed: "veridyne_completed_modules",
  scores: "veridyne_quiz_scores",
  oktaProgress: "veridyne_okta_progress",
  auth0Progress: "veridyne_auth0_progress",
  xp: "veridyne_xp",
};

function lsGet(key, fallback) {
  try { const v = localStorage.getItem(key); return v == null ? fallback : JSON.parse(v); }
  catch (e) { return fallback; }
}
function lsSet(key, value) {
  try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) {}
}

const StoreContext = createContext(null);

/* ---------------- API client (Vercel function → Neon) ---------------- */
const API_BASE = (window.VERIDYNE_CONFIG && window.VERIDYNE_CONFIG.apiBase) || "";
async function apiCall(action, payload, timeoutMs) {
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), timeoutMs || 8000);
  try {
    const r = await fetch(API_BASE + "/api/portal", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action, ...(payload || {}) }),
      signal: ctrl.signal,
    });
    let data = {};
    try { data = await r.json(); } catch (e) {}
    if (!r.ok) { const err = new Error(data.error || "Request failed (" + r.status + ")"); err.status = r.status; throw err; }
    return data;
  } finally { clearTimeout(t); }
}

const EMPTY_PROGRESS = () => ({ completed: [], scores: {}, xp: { total: 0, keys: [] } });

function useStoreProvider() {
  const [session, setSession] = useState(() => lsGet(LS.session, null));
  const [mode, setMode] = useState("detecting"); // detecting | live | demo
  const [syncState, setSyncState] = useState("idle"); // idle | syncing | synced | offline
  const [completed, setCompleted] = useState(() => lsGet(LS.completed, []));
  const [scores, setScores] = useState(() => lsGet(LS.scores, {}));
  const [xp, setXp] = useState(() => lsGet(LS.xp, { total: 0, keys: [] }));
  const [toasts, setToasts] = useState([]);
  const [levelUp, setLevelUp] = useState(null);

  const sessionRef = useRef(session);
  useEffect(() => { sessionRef.current = session; }, [session]);
  const modeRef = useRef(mode);
  useEffect(() => { modeRef.current = mode; }, [mode]);

  /* detect whether the API/database is reachable */
  useEffect(() => {
    let on = true;
    apiCall("ping", {}, 4000)
      .then(() => { if (on) setMode("live"); })
      .catch(() => { if (on) setMode("demo"); });
    return () => { on = false; };
  }, []);

  /* ---------- write-through sync (debounced, with offline queue) ---------- */
  const doSync = useCallback(async () => {
    const s = sessionRef.current;
    if (!s || s.demo || modeRef.current !== "live") return;
    setSyncState("syncing");
    try {
      await apiCall("save", {
        username: s.username, code: s.code,
        progress: { completed: lsGet(LS.completed, []), scores: lsGet(LS.scores, {}), xp: lsGet(LS.xp, { total: 0, keys: [] }) },
      });
      lsSet(LS.dirty, false);
      setSyncState("synced");
    } catch (e) {
      lsSet(LS.dirty, true);
      setSyncState("offline");
    }
  }, []);

  const syncTimer = useRef(null);
  const scheduleSync = useCallback(() => {
    const s = sessionRef.current;
    if (!s || s.demo) return;
    lsSet(LS.dirty, true);
    clearTimeout(syncTimer.current);
    syncTimer.current = setTimeout(doSync, 1200);
  }, [doSync]);

  /* retry queued changes every 20s and when the browser comes back online */
  useEffect(() => {
    const retry = () => { if (lsGet(LS.dirty, false) && sessionRef.current && !sessionRef.current.demo) doSync(); };
    const iv = setInterval(retry, 20000);
    window.addEventListener("online", retry);
    return () => { clearInterval(iv); window.removeEventListener("online", retry); };
  }, [doSync]);

  /* ---------- session ---------- */
  const login = useCallback((newSession, serverProgress) => {
    if (newSession.demo) {
      /* demo: keep whatever local progress exists in this browser */
      lsSet(LS.session, newSession);
      setSession(newSession);
      return;
    }
    let final = serverProgress || EMPTY_PROGRESS();
    if (!final.xp) final.xp = { total: 0, keys: [] };
    let pushLocal = false;
    const cachedUser = lsGet(LS.cacheUser, null);
    const localXp = lsGet(LS.xp, { total: 0, keys: [] });
    /* offline-queued changes for the SAME user beat a staler server copy */
    if (cachedUser === newSession.username && lsGet(LS.dirty, false) && localXp.total > (final.xp.total || 0)) {
      final = { completed: lsGet(LS.completed, []), scores: lsGet(LS.scores, {}), xp: localXp };
      pushLocal = true;
    }
    setCompleted(final.completed || []);
    setScores(final.scores || {});
    setXp(final.xp);
    lsSet(LS.completed, final.completed || []);
    lsSet(LS.scores, final.scores || {});
    lsSet(LS.xp, final.xp);
    lsSet(LS.cacheUser, newSession.username);
    lsSet(LS.session, newSession);
    setSession(newSession);
    if (pushLocal) { lsSet(LS.dirty, true); setTimeout(doSync, 400); }
    else { lsSet(LS.dirty, false); setSyncState("synced"); }
  }, [doSync]);

  const lock = useCallback(() => {
    /* sign out only — local cache stays so queued changes survive */
    lsSet(LS.session, null);
    setSession(null);
  }, []);

  /* XP — each key is awarded once, ever (persisted) */
  const awardXp = useCallback((key, amount, reason) => {
    const prev = lsGet(LS.xp, { total: 0, keys: [] });
    if (amount <= 0 || prev.keys.includes(key)) return;
    const next = { total: prev.total + amount, keys: [...prev.keys, key] };
    lsSet(LS.xp, next);
    setXp(next);
    setToasts((t) => [...t.slice(-3), { id: key + ":" + Date.now(), amount, reason }]);
    const before = levelFor(prev.total), after = levelFor(next.total);
    if (after.idx > before.idx) setLevelUp(after);
    scheduleSync();
  }, [scheduleSync]);
  const dismissToast = useCallback((id) => setToasts((t) => t.filter((x) => x.id !== id)), []);
  const clearLevelUp = useCallback(() => setLevelUp(null), []);

  const toggleComplete = useCallback((moduleId) => {
    const prev = lsGet(LS.completed, []);
    const adding = !prev.includes(moduleId);
    const next = adding ? [...prev, moduleId] : prev.filter((m) => m !== moduleId);
    lsSet(LS.completed, next);
    setCompleted(next);
    const oktaDone = next.filter((m) => m.startsWith("okta-")).length;
    const auth0Done = next.filter((m) => m.startsWith("auth0-")).length;
    lsSet(LS.oktaProgress, Math.round((oktaDone / window.OKTA_MODULES.length) * 100));
    lsSet(LS.auth0Progress, Math.round((auth0Done / window.AUTH0_MODULES.length) * 100));
    if (adding) {
      const mod = findModule(moduleId);
      awardXp("mod:" + moduleId, 50, "Module complete" + (mod ? ": " + mod.title : ""));
    }
    scheduleSync();
  }, [awardXp, scheduleSync]);

  const recordScore = useCallback((quizId, result) => {
    const all = lsGet(LS.scores, {});
    const entry = all[quizId] || { attempts: 0, best: 0, bestCorrect: 0 };
    const prevBestCorrect = entry.bestCorrect || 0;
    const newBestCorrect = Math.max(prevBestCorrect, result.correct);
    const next = {
      ...all,
      [quizId]: {
        attempts: entry.attempts + 1,
        best: Math.max(entry.best, result.pct),
        bestCorrect: newBestCorrect,
        last: result.pct,
        lastDetail: result,
        lastAt: Date.now(),
      },
    };
    lsSet(LS.scores, next);
    setScores(next);
    const delta = newBestCorrect - prevBestCorrect;
    if (delta > 0) {
      awardXp(`quiz:${quizId}:${newBestCorrect}`, delta * 4,
        "New best on the " + (quizId === "okta" ? "Okta" : "Auth0") + " quiz");
    }
    scheduleSync();
  }, [awardXp, scheduleSync]);

  return {
    session, login, lock, mode, syncState, doSync,
    completed, toggleComplete, scores, recordScore,
    xp, awardXp, toasts, dismissToast, levelUp, clearLevelUp,
  };
}

function useStore() { return useContext(StoreContext); }

/* progress helpers */
function trackProgress(completed, prefix) {
  const mods = prefix === "okta-" ? window.OKTA_MODULES : window.AUTH0_MODULES;
  const done = completed.filter((m) => m.startsWith(prefix)).length;
  return { done, total: mods.length, pct: mods.length ? (done / mods.length) * 100 : 0 };
}

function scoreBadge(pct) {
  if (pct >= 90) return { label: "Strong", cls: "badge-ok" };
  if (pct >= 80) return { label: "Ready", cls: "badge-ok" };
  if (pct >= 60) return { label: "Developing", cls: "badge-warn" };
  return { label: "Needs Review", cls: "badge-ember" };
}

/* ---------------- XP levels ---------------- */
const LEVELS = [
  { name: "Analyst", min: 0 },
  { name: "Associate", min: 300 },
  { name: "Consultant", min: 750 },
  { name: "Senior Consultant", min: 1250 },
  { name: "Architect", min: 1800 },
];
function levelFor(total) {
  let idx = 0;
  LEVELS.forEach((l, i) => { if (total >= l.min) idx = i; });
  const level = LEVELS[idx];
  const next = LEVELS[idx + 1] || null;
  return {
    idx, name: level.name, min: level.min, next,
    progress: next ? Math.min(1, (total - level.min) / (next.min - level.min)) : 1,
  };
}

/* ---------------- hash router ---------------- */
function parseHash() {
  const h = (location.hash || "#/dashboard").replace(/^#\/?/, "");
  const [page, sub] = h.split("/");
  return { page: page || "dashboard", sub: sub || null };
}
function navigate(path) { location.hash = "#/" + path; window.scrollTo(0, 0); }

function useHashRoute() {
  const [route, setRoute] = useState(parseHash);
  useEffect(() => {
    const onHash = () => setRoute(parseHash());
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);
  return route;
}

/* module lookup */
function findModule(id) {
  return window.OKTA_MODULES.find((m) => m.id === id) || window.AUTH0_MODULES.find((m) => m.id === id) || null;
}
function moduleTrack(id) { return id && id.startsWith("okta-") ? "okta" : "auth0"; }

Object.assign(window, {
  Icon, VeridyneMark, ProductBadge, DifficultyBadge, OfficialBadge, ProgressRing, Accordion, DocLinks,
  StoreContext, useStoreProvider, useStore, trackProgress, scoreBadge, LEVELS, levelFor, apiCall,
  parseHash, navigate, useHashRoute, findModule, moduleTrack, PRODUCT_META,
});
