// =====================================================================
// irikos — Classrooms data layer.
// Seeded directory of synced student accounts, classrooms, homework
// assignments, teacher tasks, and the current account (role sticks).
// A single store persisted to localStorage; screens read it via props.
// Globals: React, VOCAB, CATEGORY_META, FAMILY_ORDER, FAMILY_META, todayKey.
// =====================================================================

const CLASS_STORE_KEY = "ege.classroom.v2";
const FAMILIES = window.FAMILY_ORDER; // ["word-formation","grammar","multiple-choice","irregular"]

// ---------- deterministic PRNG so seeded profiles are stable ----------
function seedRng(str) {
  let h = 1779033703 ^ str.length;
  for (let i = 0; i < str.length; i++) {
    h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  return function () {
    h = Math.imul(h ^ (h >>> 16), 2246822507);
    h = Math.imul(h ^ (h >>> 13), 3266489909);
    h ^= h >>> 16;
    return (h >>> 0) / 4294967296;
  };
}
function ymd(d) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function daysAgo(n) {
  const d = new Date();
  d.setDate(d.getDate() - n);
  return d;
}

// ---------- build a full practice profile for one student ----------
// skill ~0.4..0.95 governs accuracy; activity governs volume + recency.
function buildProfile(handle, name, skill, activeDaysAgo) {
  const r = seedRng(handle + "·" + skill);
  const byFamily = {};
  let totalAnswered = 0, totalCorrect = 0;
  for (const fam of FAMILIES) {
    const fSkill = Math.max(0.2, Math.min(0.99, skill + (r() - 0.5) * 0.28));
    const seen = Math.round(28 + r() * 80);
    const correct = Math.round(seen * fSkill);
    byFamily[fam] = { seen, correct, acc: seen ? correct / seen : 0 };
    totalAnswered += seen; totalCorrect += correct;
  }
  // last-14-days answer series
  const series = [];
  for (let i = 13; i >= 0; i--) {
    const active = i >= activeDaysAgo && r() > 0.32;
    const answered = active ? Math.round(4 + r() * 22) : 0;
    const correct = Math.round(answered * Math.min(0.99, skill + (r() - 0.5) * 0.2));
    series.push({ key: ymd(daysAgo(i)), answered, correct });
  }
  // day streak (count back consecutive active days from today)
  let dayStreak = 0;
  for (let i = series.length - 1; i >= 0; i--) {
    if (series[i].answered > 0) dayStreak++; else break;
  }
  const bestStreak = Math.round(6 + skill * 30 + r() * 8);
  // mistakes — real VOCAB items, weighted to lower-skill students
  const pool = window.VOCAB.slice().sort(() => r() - 0.5);
  const nMiss = Math.round((1 - skill) * 16 + r() * 5);
  const mistakes = pool.slice(0, nMiss).map((it) => {
    const misses = 1 + Math.floor(r() * 3);
    const seen = misses + Math.floor(r() * 3);
    const ans = it.answer || "";
    const wrote = ans.length > 3 ? ans.slice(0, -1) : (it.base ? it.base.toLowerCase() : null);
    return { id: it.id, misses, seen, acc: (seen - misses) / seen, wrote };
  });
  // recent sessions (self-practice + a little homework)
  const sessions = [];
  for (let i = 0; i < 6; i++) {
    const total = [10, 12, 16, 20][Math.floor(r() * 4)];
    const correct = Math.round(total * Math.min(0.99, skill + (r() - 0.5) * 0.2));
    const qs = [];
    for (let j = 0; j < Math.min(total, 4); j++) {
      const it = pool[(i * 4 + j) % pool.length];
      const ok = j < correct ? 1 : 0;
      const ans = it.answer || "";
      qs.push({ id: it.id, c: ok, ua: ok ? ans : (ans.length > 3 ? ans.slice(0, -1) : (it.base ? it.base.toLowerCase() : "—")) });
    }
    sessions.push({
      ts: daysAgo(i + Math.floor(r() * 2)).getTime(),
      total, correct, score: correct * 10 + Math.round(r() * 40),
      accuracy: total ? correct / total : 0,
      durationMs: (120 + Math.round(r() * 220)) * 1000,
      source: r() > 0.72 ? "homework" : "self",
      questions: qs,
    });
  }
  sessions.sort((a, b) => b.ts - a.ts);
  const totalScore = sessions.reduce((a, s) => a + s.score, 0) + Math.round(totalCorrect * 4);
  return {
    handle, name, skill,
    avatarHue: Math.round(seedRng(handle)() * 360),
    totalAnswered, totalCorrect, totalScore,
    accuracy: totalAnswered ? totalCorrect / totalAnswered : 0,
    byFamily, series, dayStreak, bestStreak,
    bestSessionScore: Math.round(40 + skill * 80 + r() * 20),
    lastSeenDaysAgo: activeDaysAgo,
    activeToday: activeDaysAgo === 0,
    exam: seedRng(handle)() > 0.72 ? "oge" : "ege",
    sessions, mistakes,
  };
}

// ---------- seeded directory of synced accounts ----------
const DIRECTORY_SEED = [
  ["mila",    "Mila Orlova",       0.92, 0],
  ["arseny",  "Arseny Kuznetsov",  0.74, 0],
  ["dasha",   "Dasha Sokolova",    0.81, 1],
  ["timur",   "Timur Gaziev",      0.58, 3],
  ["polina",  "Polina Vasileva",   0.88, 0],
  ["kirill",  "Kirill Morozov",    0.46, 6],
  ["sofia",   "Sofia Lebedeva",    0.79, 1],
  ["roman",   "Roman Pavlov",      0.63, 2],
  ["nastya",  "Nastya Volkova",    0.85, 0],
  ["egor",    "Egor Smirnov",      0.52, 9],
  ["vera",    "Vera Ivanova",      0.70, 4],
  ["lev",     "Lev Antonov",       0.67, 1],
];

function buildDirectory() {
  const dir = {};
  for (const [h, n, s, a] of DIRECTORY_SEED) dir[h] = buildProfile(h, n, s, a);
  return dir;
}

// ---------- join code ----------
function makeCode() {
  const A = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
  let s = "";
  for (let i = 0; i < 6; i++) s += A[Math.floor(Math.random() * A.length)];
  return s;
}

// ---------- homework assignment helpers ----------
function assignmentLabel(kind) {
  if (kind.type === "set") return `${kind.n} from ${FAMILY_META[kind.family].label}`;
  if (kind.type === "test") return CATEGORY_META[kind.categoryId]?.label || "Test";
  if (kind.type === "task") return "My task";
  if (kind.type === "drill") return `Irregular Verbs · ${kind.len} questions`;
  return "Assignment";
}

// ---------- initial seed ----------
function seedStore() {
  const directory = buildDirectory();
  const roster = ["mila", "arseny", "dasha", "timur", "polina", "kirill", "sofia", "roman", "nastya"];
  const classId = "c_9b";
  const code = "K7PM2Q";

  // homework — mixed completion + an overdue one
  const mkSub = (handles, statusFn) => {
    const subs = {};
    for (const h of handles) subs[h] = statusFn(h);
    return subs;
  };
  const scoreFor = (h, base) => {
    const p = directory[h];
    const acc = Math.min(0.99, p.skill + (seedRng(h + base)() - 0.5) * 0.2);
    const total = base;
    const correct = Math.round(total * acc);
    return { correct, total, accuracy: correct / total, score: correct * 10 };
  };

  const assignments = {
    a1: {
      id: "a1", classroomId: classId,
      title: "Suffixes warm-up", note: "Quick 10 before Friday — focus on -ence / -ance.",
      kind: { type: "set", family: "word-formation", n: 10 },
      assignedTo: "all", dueDate: ymd(daysAgo(-3)), publishedAt: daysAgo(2).getTime(),
      submissions: mkSub(roster, (h) => {
        const p = directory[h];
        if (p.skill > 0.8 || (p.skill > 0.6 && p.lastSeenDaysAgo <= 1)) {
          const s = scoreFor(h, 10);
          return { status: "completed", ...s, missed: directory[h].mistakes.slice(0, 10 - s.correct).map(m => m.id), answeredAt: daysAgo(1).getTime(), feedbackNote: "" };
        }
        if (p.lastSeenDaysAgo <= 2) return { status: "in-progress", answered: 4, total: 10 };
        return { status: "not-started" };
      }),
    },
    a2: {
      id: "a2", classroomId: classId,
      title: "Grammar — tenses check", note: "",
      kind: { type: "set", family: "grammar", n: 8 },
      assignedTo: "all", dueDate: ymd(daysAgo(2)), publishedAt: daysAgo(6).getTime(),
      submissions: mkSub(roster, (h) => {
        const p = directory[h];
        if (p.skill > 0.7) {
          const s = scoreFor(h, 8);
          return { status: "completed", ...s, missed: directory[h].mistakes.slice(0, 8 - s.correct).map(m => m.id), answeredAt: daysAgo(3).getTime(), feedbackNote: h === "arseny" ? "Nice recovery on the conditionals — keep it up." : "" };
        }
        return { status: "overdue" };
      }),
    },
    a3: {
      id: "a3", classroomId: classId,
      title: "Lexical choice mock", note: "Exam-style. No rush — due next week.",
      kind: { type: "test", categoryId: "mc1" },
      assignedTo: ["timur", "kirill", "roman", "egor"], dueDate: ymd(daysAgo(-6)), publishedAt: daysAgo(1).getTime(),
      submissions: mkSub(["timur", "kirill", "roman", "egor"], () => ({ status: "not-started" })),
    },
  };

  const classrooms = {
    [classId]: {
      id: classId, name: "9B — EGE English", ownerHandle: "teacher",
      code, studentHandles: roster,
      pendingInvites: [
        { handle: "egor", status: "pending", invitedAt: daysAgo(1).getTime() },
        { handle: "vera", status: "pending", invitedAt: daysAgo(0).getTime() },
        { handle: "ghostuser", status: "no-account", invitedAt: daysAgo(2).getTime() },
      ],
      createdAt: daysAgo(40).getTime(),
    },
    c_tutor: {
      id: "c_tutor", name: "Sat. tutoring group", ownerHandle: "teacher",
      code: "R4WD8X", studentHandles: ["lev", "nastya"],
      pendingInvites: [],
      createdAt: daysAgo(12).getTime(),
    },
  };

  // demo-only leaderboard scores (prod gets real scores from /members)
  const demoScore = (h) => {
    let x = 0; for (let i = 0; i < h.length; i++) x = (x * 31 + h.charCodeAt(i)) >>> 0;
    const correct = 40 + (x % 260);
    return { score: correct * 700 + (x % 5000), correct, answered: correct + 20 + (x % 90), best: 1200 + (x % 4200), streak: x % 320 };
  };
  for (const c of Object.values(classrooms)) {
    c.studentScores = {};
    for (const h of c.studentHandles) c.studentScores[h] = demoScore(h);
  }

  const tasks = {
    mt1: { id: "mt1", type: "word-formation", sentence: "The committee reached a unanimous ___ after a long debate.", base: "DECIDE", answer: "decision", alts: [], note: "Built for 9B", createdAt: daysAgo(5).getTime() },
    mt2: { id: "mt2", type: "grammar", sentence: "If she ___ harder, she would have passed the mock.", base: "STUDY", answer: "had studied", alts: [], note: "", createdAt: daysAgo(3).getTime() },
    mt3: { id: "mt3", type: "multiple-choice", sentence: "He could not ___ the temptation to check his phone.", choices: ["resist", "deny", "refuse", "avoid"], answer: "resist", alts: [], note: "", createdAt: daysAgo(2).getTime() },
  };

  return {
    directory,
    classrooms,
    assignments,
    tasks,
    account: null, // set on sign-in
    studentMemberships: { mila: ["c_9b"], lev: ["c_9b", "c_tutor"] }, // who belongs where (student view)
    studentInvites: {
      // pending invites shown to the *student* on their Class home
      mila: [], // mila already a member, but we also model a fresh invite below for demo persona "you"
    },
    seededAt: Date.now(),
  };
}

// ---------- persistence ----------
function loadStore() {
  try {
    const raw = localStorage.getItem(CLASS_STORE_KEY);
    if (!raw) return seedStore();
    const parsed = JSON.parse(raw);
    if (!parsed.directory || !parsed.classrooms) return seedStore();
    return parsed;
  } catch (e) { return seedStore(); }
}
function persistStore(s) {
  try { localStorage.setItem(CLASS_STORE_KEY, JSON.stringify(s)); } catch (e) {}
}

// ---------- the store hook (live app) ----------
function useClassroomStore() {
  const [store, setStore] = React.useState(() => loadStore());
  React.useEffect(() => { persistStore(store); }, [store]);

  const mut = React.useCallback((fn) => setStore((prev) => {
    const next = fn(JSON.parse(JSON.stringify(prev)));
    return next || prev;
  }), []);

  const actions = React.useMemo(() => makeClassActions(mut, setStore), [mut]);
  return [store, actions];
}

// actions factory — shared by the live hook and the non-persisting gallery frames
function makeClassActions(mut, setStore) {
  return {
    reseed: () => setStore(seedStore()),
    signIn: (account) => mut((s) => {
      s.account = account;
      // a new teacher inherits the seeded demo classrooms (placeholder owner)
      if (account.role === "teacher") {
        for (const c of Object.values(s.classrooms)) {
          if (c.ownerHandle === "teacher") c.ownerHandle = account.handle;
        }
      }
      return s;
    }),
    signOut: () => mut((s) => { s.account = null; return s; }),

    createClassroom: (name) => {
      const id = "c_" + Math.random().toString(36).slice(2, 8);
      const code = makeCode();
      mut((s) => {
        s.classrooms[id] = {
          id, name: name.trim(), ownerHandle: s.account?.handle || "teacher",
          code, studentHandles: [], pendingInvites: [], createdAt: Date.now(),
        };
        return s;
      });
      return { id, code };
    },
    rotateCode: (classId) => mut((s) => { s.classrooms[classId].code = makeCode(); return s; }),
    deleteClassroom: (classId) => mut((s) => {
      delete s.classrooms[classId];
      for (const id of Object.keys(s.assignments)) if (s.assignments[id].classroomId === classId) delete s.assignments[id];
      for (const h of Object.keys(s.studentMemberships)) s.studentMemberships[h] = s.studentMemberships[h].filter(id => id !== classId);
      return s;
    }),
    addStudent: (classId, handle) => {
      let known = false;
      mut((s) => {
        const c = s.classrooms[classId];
        known = !!s.directory[handle];
        if (known) {
          if (!c.studentHandles.includes(handle) && !c.pendingInvites.find(p => p.handle === handle))
            c.pendingInvites.push({ handle, status: "pending", invitedAt: Date.now() });
        } else {
          if (!c.pendingInvites.find(p => p.handle === handle))
            c.pendingInvites.push({ handle, status: "no-account", invitedAt: Date.now() });
        }
        return s;
      });
      // Mirror the server contract so the toast can report truthfully (demo's
      // directory stands in for "has a synced account").
      return known ? { ok: true } : { ok: false, err: `@${handle} hasn't synced an account yet — share the join code so they can add themselves.` };
    },
    removeStudent: (classId, handle) => mut((s) => {
      const c = s.classrooms[classId];
      c.studentHandles = c.studentHandles.filter(h => h !== handle);
      c.pendingInvites = c.pendingInvites.filter(p => p.handle !== handle);
      return s;
    }),
    cancelInvite: (classId, handle) => mut((s) => {
      const c = s.classrooms[classId];
      c.pendingInvites = c.pendingInvites.filter(p => p.handle !== handle);
      return s;
    }),
    publishAssignment: (a) => mut((s) => {
      const id = "a_" + Math.random().toString(36).slice(2, 8);
      const c = s.classrooms[a.classroomId];
      const targets = a.assignedTo === "all" ? c.studentHandles : a.assignedTo;
      const submissions = {};
      for (const h of targets) submissions[h] = { status: "not-started" };
      s.assignments[id] = { ...a, id, publishedAt: Date.now(), submissions };
      return s;
    }),
    addTask: (task) => mut((s) => {
      const id = "mt_" + Math.random().toString(36).slice(2, 8);
      s.tasks[id] = { ...task, id, createdAt: Date.now() };
      return s;
    }),
    updateTask: (id, task) => mut((s) => { if (s.tasks[id]) s.tasks[id] = { ...s.tasks[id], ...task, id }; return s; }),
    deleteTask: (id) => mut((s) => { delete s.tasks[id]; return s; }),
    setFeedback: (assignmentId, handle, note) => mut((s) => {
      const sub = s.assignments[assignmentId]?.submissions?.[handle];
      if (sub) sub.feedbackNote = note;
      return s;
    }),
    // student side
    acceptInvite: (classId, handle) => mut((s) => {
      const c = s.classrooms[classId];
      c.pendingInvites = c.pendingInvites.filter(p => p.handle !== handle);
      if (!c.studentHandles.includes(handle)) c.studentHandles.push(handle);
      s.studentMemberships[handle] = [...new Set([...(s.studentMemberships[handle] || []), classId])];
      seedStudentSubs(s, handle);
      return s;
    }),
    // fill missing submissions for a student so their homework list reads realistically
    seedStudentHomework: (handle) => mut((s) => { seedStudentSubs(s, handle); return s; }),
    declineInvite: (classId, handle) => mut((s) => {
      const c = s.classrooms[classId];
      c.pendingInvites = c.pendingInvites.filter(p => p.handle !== handle);
      return s;
    }),
    joinByCode: (code, handle) => {
      let found = null;
      mut((s) => {
        const c = Object.values(s.classrooms).find(c => c.code.toUpperCase() === code.toUpperCase());
        if (c) {
          found = c.id;
          if (!c.studentHandles.includes(handle)) c.studentHandles.push(handle);
          s.studentMemberships[handle] = [...new Set([...(s.studentMemberships[handle] || []), c.id])];
          seedStudentSubs(s, handle);
        }
        return s;
      });
      return found;
    },
    submitHomework: (assignmentId, handle, result) => mut((s) => {
      const a = s.assignments[assignmentId];
      if (a) a.submissions[handle] = { status: "completed", ...result, answeredAt: Date.now() };
      return s;
    }),
  };
}

// ---------- derived selectors (pure, reused by canvas gallery) ----------
function classOverview(store, classId) {
  const c = store.classrooms[classId];
  if (!c) return null;
  const students = c.studentHandles.map(h => store.directory[h]).filter(Boolean);
  const byFamily = {};
  for (const fam of FAMILIES) byFamily[fam] = { seen: 0, correct: 0 };
  let activeToday = 0;
  for (const p of students) {
    for (const fam of FAMILIES) {
      byFamily[fam].seen += p.byFamily[fam].seen;
      byFamily[fam].correct += p.byFamily[fam].correct;
    }
    if (p.activeToday) activeToday++;
  }
  for (const fam of FAMILIES) byFamily[fam].acc = byFamily[fam].seen ? byFamily[fam].correct / byFamily[fam].seen : 0;
  // homework completion across this class
  const classAssignments = Object.values(store.assignments).filter(a => a.classroomId === classId);
  let due = 0, done = 0;
  for (const a of classAssignments) {
    for (const h of Object.keys(a.submissions)) {
      due++;
      if (a.submissions[h].status === "completed") done++;
    }
  }
  return {
    classroom: c, students, byFamily, activeToday,
    studentCount: students.length,
    completion: due ? done / due : 0, due, done,
    assignments: classAssignments,
  };
}

// flags for the per-classroom card badges
function classFlags(store, classId) {
  const c = store.classrooms[classId];
  const classAssignments = Object.values(store.assignments).filter(a => a.classroomId === classId);
  let overdue = 0, needsReview = 0;
  for (const a of classAssignments) {
    for (const h of Object.keys(a.submissions)) {
      const st = a.submissions[h].status;
      if (st === "overdue") overdue++;
      // "needs review" = completed but not yet marked seen (matches the backend
      // flag + the Mark-all-seen button; a feedback note is optional and separate).
      if (st === "completed" && !a.submissions[h].seen) needsReview++;
    }
  }
  return { overdue, needsReview, pending: c.pendingInvites.length };
}

function studentAssignments(store, handle) {
  const out = [];
  for (const a of Object.values(store.assignments)) {
    if (!a.submissions[handle]) continue;
    out.push({ ...a, sub: a.submissions[handle] });
  }
  return out.sort((x, y) => new Date(x.dueDate) - new Date(y.dueDate));
}

// fill missing homework submissions for a student so their list reads realistically
function seedStudentSubs(s, handle) {
  const memberOf = s.studentMemberships[handle] || [];
  const r = seedRng("subs·" + handle);
  let idx = 0;
  for (const a of Object.values(s.assignments)) {
    if (!memberOf.includes(a.classroomId)) continue;
    const targeted = a.assignedTo === "all"
      ? s.classrooms[a.classroomId].studentHandles.includes(handle)
      : a.assignedTo.includes(handle);
    if (!targeted) continue;
    if (a.submissions[handle]) continue;
    const overdue = a.dueDate && new Date(a.dueDate + "T23:59:59") < new Date();
    if (idx === 0) {
      // first one: completed, with a score + missed + teacher note (demo richness)
      const total = a.kind.type === "set" ? a.kind.n : 8;
      const correct = Math.round(total * (0.7 + r() * 0.25));
      const missed = window.VOCAB.slice().sort(() => r() - 0.5).slice(0, total - correct).map(v => v.id);
      a.submissions[handle] = {
        status: "completed", correct, total, accuracy: correct / total, score: correct * 10,
        missed, answeredAt: Date.now() - 2 * 86400000,
        feedbackNote: "Good work — your suffixes are getting reliable. Watch -ence vs -ance.",
      };
    } else {
      a.submissions[handle] = { status: overdue ? "overdue" : "not-started" };
    }
    idx++;
  }
}

// pending invites to show the student (real + a demo invite if they have none)
function studentPendingInvites(store, handle) {
  const real = [];
  for (const c of Object.values(store.classrooms)) {
    const inv = c.pendingInvites.find(p => p.handle === handle);
    if (inv) real.push({ classId: c.id, classroom: c, invitedAt: inv.invitedAt, demo: false });
  }
  const memberOf = store.studentMemberships[handle] || [];
  if (real.length === 0 && memberOf.length === 0) {
    // demo invite so the pending-invite experience is reviewable for any fresh student
    const c = store.classrooms.c_9b;
    if (c && !c.studentHandles.includes(handle)) {
      real.push({ classId: c.id, classroom: c, invitedAt: Date.now() - 3600000, demo: true });
    }
  }
  return real;
}


window.seedStore = seedStore;
window.seedStudentSubs = seedStudentSubs;
window.makeClassCode = makeCode;
window.classOverview = classOverview;
window.classFlags = classFlags;
window.studentAssignments = studentAssignments;
window.studentPendingInvites = studentPendingInvites;
window.classTeacherName = "Ms. Sokolova";
window.assignmentLabel = assignmentLabel;
window.CLASS_FAMILIES = FAMILIES;
window.classYmd = ymd;
window.classDaysAgo = daysAgo;

// =====================================================================
// SERVER MODE — the live, multi-user backend (functions/api/classroom/*).
// Produces the SAME store shape as the demo seed above, so the screens are
// unchanged. Signed-in → server; guest → gate; ?classdemo → seeded demo.
// =====================================================================
const CLASS_TOKEN_KEY = "vp.token.v1"; // mirrors auth.jsx

function isClassDemo() {
  try {
    return localStorage.getItem("ege_class_demo") === "1" ||
      /[?&]classdemo\b/.test(location.search);
  } catch (e) { return false; }
}
function stripAt(u) { return String(u || "").replace(/^@/, ""); }
function addAt(h) { return "@" + String(h || "").replace(/^@/, ""); }
function hashHue(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return h % 360; }
function rollupFamily(perCategory) {
  const by = {}; for (const fam of FAMILIES) by[fam] = { seen: 0, correct: 0, acc: 0 };
  for (const cat in (perCategory || {})) {
    const fam = (window.CATEGORY_META[cat] || {}).family;
    if (!by[fam]) continue;
    by[fam].seen += perCategory[cat].seen || 0; by[fam].correct += perCategory[cat].correct || 0;
  }
  for (const fam of FAMILIES) by[fam].acc = by[fam].seen ? by[fam].correct / by[fam].seen : 0;
  return by;
}
function keyToDaysAgo(key) {
  if (!key) return 99;
  const [y, m, d] = key.split("-").map(Number);
  const today = new Date();
  return Math.max(0, Math.round((Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()) - Date.UTC(y, m - 1, d)) / 86400000));
}
function seriesFromHistory(dh) {
  const out = []; for (let i = 13; i >= 0; i--) { const k = ymd(daysAgo(i)); const e = (dh || {})[k] || {}; out.push({ key: k, answered: e.answered || 0, correct: e.correct || 0 }); } return out;
}

async function classApi(path, opts = {}) {
  let token = null; try { token = localStorage.getItem(CLASS_TOKEN_KEY); } catch (e) {}
  const res = await fetch(path, {
    method: opts.method || "GET",
    headers: { "content-type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });
  let data = {}; try { data = await res.json(); } catch (e) {}
  if (!res.ok || data.ok === false) { const e = new Error(data.err || `HTTP ${res.status}`); e.status = res.status; e.data = data; throw e; }
  return data;
}

// Retry transient failures (5xx / network / 408 / 429) with backoff; fail fast
// on a real 4xx (auth / not-found). Stops a single D1 blip from blanking the
// whole Class tab with "try again".
async function classApiRetry(path, opts = {}, tries = 3) {
  let lastErr;
  for (let i = 0; i < tries; i++) {
    try { return await classApi(path, opts); }
    catch (e) {
      lastErr = e;
      const s = e && e.status;
      if (s && s >= 400 && s < 500 && s !== 408 && s !== 429) throw e; // not transient
      if (i < tries - 1) await new Promise(r => setTimeout(r, 280 * (i + 1)));
    }
  }
  throw lastErr;
}

// Durable homework submit: a completed submission must never be silently lost.
// If the POST fails we persist it locally and re-send on the next load.
const PENDING_HW_KEY = "vp.pendingHomework.v1";
function loadPendingHw() { try { return JSON.parse(localStorage.getItem(PENDING_HW_KEY) || "[]"); } catch (e) { return []; } }
function savePendingHw(list) { try { localStorage.setItem(PENDING_HW_KEY, JSON.stringify(list)); } catch (e) {} }
function queuePendingHw(entry) {
  const list = loadPendingHw().filter(e => e.assignmentId !== entry.assignmentId);
  list.push(entry); savePendingHw(list);
}
function dequeuePendingHw(assignmentId) { savePendingHw(loadPendingHw().filter(e => e.assignmentId !== assignmentId)); }
function submitBody(result) {
  return { answered: result.total, correct: result.correct, accuracy: result.accuracy, score: result.score, mistakes: result.missed || [], timeSpentS: result.timeSpentS || 0 };
}
async function flushPendingHw() {
  for (const e of loadPendingHw()) {
    try { await classApi(`/api/assignments/${e.assignmentId}/submit`, { method: "POST", body: e.body }); dequeuePendingHw(e.assignmentId); } catch (x) {}
  }
}

function profileFromSummary(username, displayName, sum) {
  const handle = stripAt(username);
  return {
    handle, name: displayName || addAt(handle), avatarHue: hashHue(handle),
    totalAnswered: sum.totalAnswered || 0, totalCorrect: sum.totalCorrect || 0, accuracy: sum.accuracy || 0,
    byFamily: rollupFamily(sum.perCategory), dayStreak: sum.dayStreak || 0, bestStreak: sum.bestStreak || 0,
    bestSessionScore: sum.bestSessionScore || 0, lastSeenDaysAgo: keyToDaysAgo(sum.lastActiveKey),
    activeToday: keyToDaysAgo(sum.lastActiveKey) === 0, series: [],
    mistakes: Array.from({ length: sum.mistakeCount || 0 }).map((_, i) => ({ id: `_m${i}`, misses: 1, seen: 1, acc: 0 })),
    _detail: false,
  };
}
function kindFromApi(a) {
  if (a.sourceKind === "set") return { type: "set", family: a.family, n: a.size, exam: a.exam || null };
  if (a.sourceKind === "test") return { type: "test", categoryId: a.testId };
  return { type: "task", taskId: (a.taskIds && a.taskIds[0]) || "" };
}
function apiSubToDesign(s, size) {
  if (!s) return { status: "not-started" };
  if (s.status === "completed") return { status: "completed", correct: s.correct, total: size || s.answered, accuracy: s.accuracy, score: s.score, missed: s.mistakes || [], feedbackNote: "", reviewedAt: s.reviewedAt || null, seen: !!s.reviewedAt };
  const st = s.effectiveStatus === "overdue" ? "overdue" : (s.status === "in_progress" ? "in-progress" : "not-started");
  return { status: st, answered: s.answered, total: size };
}
function teacherAssignment(a, subs, classroom) {
  const submissions = {};
  for (const s of (subs || [])) submissions[stripAt(s.studentUsername)] = apiSubToDesign(s, a.size);
  if (a.audience === "all") for (const h of classroom.studentHandles) if (!submissions[h]) submissions[h] = { status: a.dueAt && Date.now() > a.dueAt ? "overdue" : "not-started" };
  return { id: a.id, classroomId: a.classroomId, title: a.title, note: a.instructions || "", kind: kindFromApi(a), assignedTo: a.audience === "all" ? "all" : Object.keys(submissions), dueDate: a.dueAt ? ymd(new Date(a.dueAt)) : null, publishedAt: a.publishedAt, submissions, progress: a.progress };
}
function studentAssignment(a) {
  return { id: a.id, classroomId: a.classroomId, title: a.title, note: a.instructions || "", kind: kindFromApi(a), assignedTo: "all", dueDate: a.dueAt ? ymd(new Date(a.dueAt)) : null, submissions: { [a._me]: apiSubToDesign(a.submission, a.size) } };
}
function mapTask(t) { return { id: t.id, type: t.type, sentence: t.sentence, base: t.base, answer: t.answer, alts: t.alts || [], choices: t.choices || undefined, note: t.note || "", exam: t.exam || null, createdAt: t.createdAt }; }

async function loadServerStore(handle) {
  const me = await classApiRetry("/api/me");   // retry transient blips, don't blank the tab
  await flushPendingHw();                        // re-send any homework that failed to submit earlier
  const account = { handle, name: me.profile.displayName || addAt(handle), role: me.profile.accountType || "student", accountType: me.profile.accountType || "student" };
  const store = { account, classrooms: {}, directory: {}, assignments: {}, tasks: {}, studentMemberships: {} };

  for (const c of me.owned) {
    try {
      const [det, ros, ov, asg] = await Promise.all([
        classApi(`/api/classrooms/${c.id}`), classApi(`/api/classrooms/${c.id}/roster`),
        classApi(`/api/classrooms/${c.id}/overview`), classApi(`/api/classrooms/${c.id}/assignments`),
      ]);
      const cc = det.classroom;
      store.classrooms[c.id] = {
        id: c.id, name: cc.name, ownerHandle: handle, code: cc.joinCode, createdAt: cc.createdAt,
        exam: cc.exam || "ege",
        studentCount: c.memberCount,
        studentHandles: ros.members.map(m => stripAt(m.username)),
        pendingInvites: ros.pending.map(p => ({ handle: stripAt(p.username), status: "pending", invitedAt: p.invitedAt, membershipId: p.membershipId })),
        _members: Object.fromEntries(ros.members.map(m => [stripAt(m.username), m.membershipId])),
      };
      store.classrooms[c.id].memberStats = {};
      for (const m of ov.members) {
        const mh = stripAt(m.username);
        store.directory[mh] = profileFromSummary(m.username, m.displayName, m.summary);
        // Per-class, exam-scoped summary so the Students page can show each class's numbers.
        store.classrooms[c.id].memberStats[mh] = { accuracy: m.summary.accuracy || 0, totalScore: m.summary.totalScore || 0, answered: m.summary.totalAnswered || 0, lastActiveKey: m.summary.lastActiveKey || null };
      }
      for (const a of asg.assignments) {
        let subs = [];
        try { subs = (await classApi(`/api/assignments/${a.id}`)).submissions || []; } catch (e) {}
        store.assignments[a.id] = teacherAssignment(a, subs, store.classrooms[c.id]);
      }
    } catch (e) { /* skip a class that failed to load */ }
  }

  store.studentMemberships[handle] = me.joined.map(c => c.id);
  // Fetch each joined class's roster (NAMES only) so a student can see who else
  // is in the group — and so the count reflects the real members.
  await Promise.all(me.joined.map(async (c) => {
    if (store.classrooms[c.id]) return;
    let handles = [];
    const scores = {};
    try {
      const mem = await classApi(`/api/classrooms/${c.id}/members`);
      handles = (mem.members || []).map(m => stripAt(m.username));
      for (const m of (mem.members || [])) {
        const h = stripAt(m.username);
        if (!store.directory[h]) store.directory[h] = { handle: h, name: m.displayName || addAt(h), avatarHue: hashHue(h) };
        scores[h] = { score: m.score || 0, correct: m.correct || 0, answered: m.answered || 0, best: m.best || 0, streak: m.streak || 0 };
      }
    } catch (e) {}
    store.classrooms[c.id] = {
      id: c.id, name: c.name, ownerHandle: stripAt(c.ownerUsername), code: "", exam: c.exam || "ege",
      studentHandles: handles, studentScores: scores, studentCount: c.memberCount != null ? c.memberCount : handles.length,
      pendingInvites: [], createdAt: 0,
    };
  }));
  try {
    const hw = await classApi("/api/me/homework");
    for (const a of hw.homework) { a._me = handle; store.assignments[a.id] = store.assignments[a.id] || studentAssignment(a); }
  } catch (e) {}
  try {
    const inv = await classApi("/api/me/invites");
    for (const v of inv.invites) {
      store.classrooms[v.classroomId] = store.classrooms[v.classroomId] || { id: v.classroomId, name: v.classroomName, ownerHandle: stripAt(v.ownerUsername), code: "", studentHandles: [], pendingInvites: [], createdAt: 0 };
      const c = store.classrooms[v.classroomId];
      if (!c.pendingInvites.find(p => p.handle === handle)) c.pendingInvites.push({ handle, status: "pending", invitedAt: v.invitedAt, membershipId: v.membershipId });
    }
  } catch (e) {}
  try { const tk = await classApi("/api/tasks"); for (const t of tk.tasks) store.tasks[t.id] = mapTask(t); } catch (e) {}
  return store;
}

function findMembershipId(store, classId, handle) {
  const c = store.classrooms[classId]; if (!c) return null;
  if (c._members && c._members[handle]) return c._members[handle];
  const inv = (c.pendingInvites || []).find(p => p.handle === handle);
  return inv ? inv.membershipId : null;
}

function serverActions(getStore, reload, patch) {
  const wrap = (fn) => (...args) => Promise.resolve().then(() => fn(...args)).then(() => reload()).catch(() => {});
  return {
    signOut: () => {}, reseed: () => {}, seedStudentHomework: () => {},
    setFeedback: () => {}, // teacher notes — not persisted server-side yet (future hook)
    createClassroom: async (name, exam) => {
      try { const r = await classApi("/api/classrooms", { method: "POST", body: { name, exam: exam === "oge" ? "oge" : "ege" } }); await reload(); return { id: r.classroom.id, code: r.classroom.joinCode }; }
      catch (e) { return null; }
    },
    rotateCode: wrap((classId) => classApi(`/api/classrooms/${classId}/rotate-code`, { method: "POST" })),
    deleteClassroom: wrap((classId) => classApi(`/api/classrooms/${classId}`, { method: "DELETE" })),
    addStudent: async (classId, handle) => {
      // Don't use wrap(): it swallows the server error, which is exactly the
      // "No synced account @X" message the teacher needs to see. Return a
      // structured result and still refresh so a successful invite shows up.
      try { await classApi(`/api/classrooms/${classId}/invite`, { method: "POST", body: { username: addAt(handle) } }); await reload(); return { ok: true }; }
      catch (e) { await reload().catch(() => {}); return { ok: false, err: e.message || `Couldn't invite @${handle}.` }; }
    },
    removeStudent: wrap((classId, handle) => { const id = findMembershipId(getStore(), classId, handle); return id ? classApi(`/api/memberships/${id}`, { method: "DELETE" }) : null; }),
    cancelInvite: wrap((classId, handle) => { const id = findMembershipId(getStore(), classId, handle); return id ? classApi(`/api/memberships/${id}`, { method: "DELETE" }) : null; }),
    publishAssignment: wrap((a) => {
      const body = { title: a.title, instructions: a.note, audience: a.assignedTo === "all" ? "all" : "selected", targets: a.assignedTo === "all" ? [] : a.assignedTo.map(addAt), dueAt: a.dueDate ? new Date(a.dueDate + "T23:59:59").getTime() : null, publish: true };
      if (a.kind.type === "set") { body.sourceKind = "set"; body.family = a.kind.family; body.size = a.kind.n; }
      else if (a.kind.type === "test") { body.sourceKind = "test"; body.testId = a.kind.categoryId; }
      else { body.sourceKind = "custom"; body.taskIds = [a.kind.taskId]; }
      return classApi(`/api/classrooms/${a.classroomId}/assignments`, { method: "POST", body });
    }),
    addTask: wrap((t) => classApi("/api/tasks", { method: "POST", body: { type: t.type, sentence: t.sentence, base: t.base, answer: t.answer, alts: t.alts || [], choices: t.choices, note: t.note, exam: t.exam } })),
    updateTask: wrap((id, t) => classApi(`/api/tasks/${id}`, { method: "PATCH", body: { type: t.type, sentence: t.sentence, base: t.base, answer: t.answer, alts: t.alts || [], choices: t.choices, note: t.note, exam: t.exam } })),
    deleteTask: wrap((id) => classApi(`/api/tasks/${id}`, { method: "DELETE" })),
    acceptInvite: wrap((classId, handle) => { const id = findMembershipId(getStore(), classId, handle); return id ? classApi(`/api/memberships/${id}/accept`, { method: "POST" }) : null; }),
    declineInvite: wrap((classId, handle) => { const id = findMembershipId(getStore(), classId, handle); return id ? classApi(`/api/memberships/${id}/decline`, { method: "POST" }) : null; }),
    joinByCode: async (code) => {
      try { const r = await classApi("/api/classrooms/join", { method: "POST", body: { code } }); await reload(); return r.classroom ? r.classroom.id : null; }
      catch (e) { return null; }
    },
    // Durable submit: await it, surface success/failure, and never silently
    // lose a completed homework. One quick retry, then persist + re-send later.
    submitHomework: async (assignmentId, handle, result) => {
      const body = submitBody(result);
      const tryPost = () => classApi(`/api/assignments/${assignmentId}/submit`, { method: "POST", body });
      try {
        await tryPost(); dequeuePendingHw(assignmentId); await reload().catch(() => {});
        return { ok: true };
      } catch (e) {
        try { await tryPost(); dequeuePendingHw(assignmentId); await reload().catch(() => {}); return { ok: true }; }
        catch (e2) {
          queuePendingHw({ assignmentId, handle, body, ts: Date.now() });
          return { ok: false, err: (e2 && e2.message) || "Couldn't reach the server." };
        }
      }
    },
    // Instant exam toggle: optimistic local flip, then persist + reload.
    setClassroomExam: (classId, exam) => {
      const e = exam === "oge" ? "oge" : "ege";
      if (patch) patch((s) => { if (s.classrooms[classId]) s.classrooms[classId].exam = e; return s; });
      return Promise.resolve()
        .then(() => classApi(`/api/classrooms/${classId}`, { method: "PATCH", body: { exam: e } }))
        .then(() => reload()).catch(() => {});
    },
    markSeen: wrap((assignmentId, handle) => classApi(`/api/assignments/${assignmentId}/review`, { method: "POST", body: { student: addAt(handle) } })),
    markAllSeen: wrap((classId) => classApi(`/api/classrooms/${classId}/review-all`, { method: "POST" })),
    ensureStudentDetail: async (classId, handle, patch) => {
      const store = getStore(); const p = store.directory[handle];
      if (p && p._detail) return;
      try {
        const d = await classApi(`/api/classrooms/${classId}/students/${addAt(handle)}`);
        patch((s) => {
          const prof = s.directory[handle] || profileFromSummary(addAt(handle), d.displayName, d.summary || {});
          const st = d.stats || {};
          prof.series = seriesFromHistory(st.dailyHistory);
          const pi = st.perItem || {};
          // Latest wrong answer the student actually typed, per item, from kept session detail.
          const wrongByItem = {};
          for (const sess of (st.sessions || [])) for (const q of (sess.questions || [])) {
            if (q && q.c === 0 && q.id && q.ua) wrongByItem[q.id] = q.ua;
          }
          prof.mistakes = Object.keys(pi).filter(id => { const x = pi[id]; return x.lastCorrect === false || (x.lastCorrect === undefined && x.seen > x.correct); })
            .map(id => ({ id, misses: (pi[id].seen - pi[id].correct) || 1, seen: pi[id].seen, acc: pi[id].seen ? pi[id].correct / pi[id].seen : 0, wrote: wrongByItem[id] || null }))
            .sort((a, b) => b.misses - a.misses).slice(0, 6);
          prof.byFamily = rollupFamily(st.perCategory);
          prof.exam = d.exam === "oge" ? "oge" : "ege";
          prof.totalScore = st.totalScore || 0;
          prof.otherExam = d.otherExam || null;
          prof.sessions = Array.isArray(st.sessions) ? st.sessions.slice(-12).reverse() : [];
          prof._detail = true;
          s.directory[handle] = prof; return s;
        });
      } catch (e) {}
    },
  };
}

// ---------- the unified hook the app uses ----------
function useClassroomData(auth, enabled = true) {
  const demo = isClassDemo();
  const isGuest = !auth || !auth.currentUser || auth.currentUser === "guest";

  const [store, setStore] = React.useState(() => {
    if (demo) { const s = seedStore(); s.account = { handle: "teacher", name: "You (demo)", role: "teacher", accountType: "teacher" }; return s; }
    return { account: null, classrooms: {}, directory: {}, assignments: {}, tasks: {}, studentMemberships: {} };
  });
  const [dataState, setDataState] = React.useState(demo ? "ready" : (isGuest ? "guest" : "loading"));
  const storeRef = React.useRef(store); storeRef.current = store;
  const patch = React.useCallback((fn) => setStore(prev => fn(JSON.parse(JSON.stringify(prev))) || prev), []);

  const reload = React.useCallback(async () => {
    if (demo || isGuest) return;
    try { const s = await loadServerStore(stripAt(auth.currentUser)); setStore(s); setDataState("ready"); }
    catch (e) { setDataState("error"); }
  }, [demo, isGuest, auth && auth.currentUser]);

  React.useEffect(() => {
    if (demo || isGuest) { setDataState(demo ? "ready" : "guest"); return; }
    if (!enabled) return; // defer the fetch until the Class tab is opened
    let cancelled = false; setDataState("loading");
    loadServerStore(stripAt(auth.currentUser))
      .then(s => { if (!cancelled) { setStore(s); setDataState("ready"); } })
      .catch(() => { if (!cancelled) setDataState("error"); });
    return () => { cancelled = true; };
  }, [demo, isGuest, enabled, auth && auth.currentUser, auth && auth.syncTick]);

  const actions = React.useMemo(() => {
    if (demo) {
      const mut = (fn) => setStore(prev => fn(JSON.parse(JSON.stringify(prev))) || prev);
      return {
        ...makeClassActions(mut, setStore),
        setClassroomExam: (classId, exam) => mut((s) => { if (s.classrooms[classId]) s.classrooms[classId].exam = (exam === "oge" ? "oge" : "ege"); return s; }),
        markSeen: (assignmentId, handle) => mut((s) => { const a = s.assignments[assignmentId]; if (a && a.submissions[handle]) a.submissions[handle].seen = true; return s; }),
        markAllSeen: () => mut((s) => { for (const a of Object.values(s.assignments)) for (const h in (a.submissions || {})) if (a.submissions[h].status === "completed") a.submissions[h].seen = true; return s; }),
        ensureStudentDetail: () => {},
      };
    }
    const a = serverActions(() => storeRef.current, reload, patch);
    const orig = a.ensureStudentDetail;
    a.ensureStudentDetail = (classId, handle) => orig(classId, handle, patch);
    return a;
  }, [demo, reload, patch]);

  return { store, actions, dataState, reload };
}

window.useClassroomData = useClassroomData;
