/* === PHYSL 210 — study stats / analytics capture (local only) =====================

   Records practice + exam answers and flashcard judgments into a compact,
   schema-versioned localStorage aggregate. NOT an unbounded event log — we keep:
     • per-module rollups: lifetime attempts/correct + a last-result map per item
     • per-day activity counts (questions vs cards)
     • a course-vs-textbook accuracy split
   The daily streak is derived from the per-day map on read, so it decays on its
   own when a day is missed (no stale stored value).

   Stable identity: question banks and flashcard decks are shuffled at runtime and
   carry no id field, so array index is NOT identity. We hash module id + the
   question stem / card term (FNV-1a → base36) to get a key that survives reshuffles
   and bank edits.

   Everything is wrapped in try/catch with sane fallbacks and kept strictly local.
   Personal notes (physl.notes.v1) and course progress (physl.progress.v1) are never
   touched here. */

const STATS_KEY = 'physl.stats.v1';   // storage namespace (unchanged); schema version lives in the `v` field
const STATS_VERSION = 2;

/* ---- stable item keys ---- */
function statsHash(str) {
  let h = 0x811c9dc5;                       // FNV-1a 32-bit
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return (h >>> 0).toString(36);
}
function statsKey(moduleId, text) {
  return statsHash((moduleId || '?') + '|' + (text || ''));
}

/* ---- flashcard mastery model (schema v2) ----
   Each card stores an integer LEVEL 0..4 (no time-based scheduling / due dates).
   The four rating buttons move the level via nextLevel(). A card counts as
   "mastered" for EVERY ring / progress read ONLY at the top level (=== MASTERED). */
const CARD_MAX_LEVEL = 4;
const MASTERED = CARD_MAX_LEVEL;
const CARD_LEVEL_LABELS = ['New', 'Learning', 'Familiar', 'Proficient', 'Mastered'];

/* pure transition table — the single place to tune how a rating moves a card */
function nextLevel(level, rating) {
  const l = Math.max(0, Math.min(CARD_MAX_LEVEL, level | 0));
  switch (rating) {
    case 'again': return 1;                            // failed recall → back to Learning
    case 'hard':  return Math.max(1, l);               // shaky → hold (floor of Learning)
    case 'good':  return Math.min(CARD_MAX_LEVEL, l + 1);
    case 'easy':  return Math.min(CARD_MAX_LEVEL, l + 2);
    default:      return l;
  }
}
function _countMastered(items) {
  let n = 0; for (const k in items) if (items[k] === MASTERED) n++; return n;
}

/* ---- schema ---- */
function freshStats() {
  return {
    v: STATS_VERSION,
    modules: {},          // { id: { q:{attempts,correct,items:{key:0|1}}, c:{attempts,known,items:{key:level 0..4}} } }
    days: {},             // { 'YYYY-MM-DD': { q:n, c:n } }
    bySource: { course: { attempts: 0, correct: 0 }, textbook: { attempts: 0, correct: 0 } },
  };
}

/* v1 → v2: card results were a binary { key: 0|1 }; promote to the level model so
   existing progress survives — a known card (1) becomes Mastered (top level), a
   seen-but-missed card (0) becomes Learning (1). Question items stay 0|1. */
function _migrateV1toV2(p) {
  try {
    Object.keys(p.modules || {}).forEach(id => {
      const c = (p.modules[id] || {}).c;
      if (c && c.items) {
        Object.keys(c.items).forEach(k => { c.items[k] = (c.items[k] === 1) ? MASTERED : 1; });
        c.known = _countMastered(c.items);
      }
    });
    p.v = STATS_VERSION;
  } catch (e) {}
}

function readStats() {
  try {
    const raw = localStorage.getItem(STATS_KEY);
    if (!raw) return freshStats();
    const p = JSON.parse(raw);
    // unparseable handled by catch; older/incompatible schema → reset gracefully
    if (!p || typeof p.modules !== 'object' || p.modules === null) return freshStats();
    if (p.v === 1) _migrateV1toV2(p);                  // promote binary cards → level model
    if (p.v !== STATS_VERSION) return freshStats();    // unknown/newer schema → reset gracefully
    if (!p.days || typeof p.days !== 'object') p.days = {};
    if (!p.bySource) p.bySource = { course: { attempts: 0, correct: 0 }, textbook: { attempts: 0, correct: 0 } };
    if (!p.bySource.course) p.bySource.course = { attempts: 0, correct: 0 };
    if (!p.bySource.textbook) p.bySource.textbook = { attempts: 0, correct: 0 };
    return p;
  } catch (e) {
    return freshStats();
  }
}

/* Replace the in-memory stats with a server-provided object (cloud-sync hydrate on
   login); normalizes/migrates exactly like readStats, then mirrors to the local cache. */
function hydrateStats(p) {
  try {
    if (!p || typeof p.modules !== 'object' || p.modules === null) { _stats = freshStats(); }
    else {
      if (p.v === 1) _migrateV1toV2(p);
      if (p.v !== STATS_VERSION) { _stats = freshStats(); }
      else {
        if (!p.days || typeof p.days !== 'object') p.days = {};
        if (!p.bySource) p.bySource = { course: { attempts: 0, correct: 0 }, textbook: { attempts: 0, correct: 0 } };
        if (!p.bySource.course) p.bySource.course = { attempts: 0, correct: 0 };
        if (!p.bySource.textbook) p.bySource.textbook = { attempts: 0, correct: 0 };
        _stats = p;
      }
    }
    try { localStorage.setItem(STATS_KEY, JSON.stringify(_stats)); } catch (e) {}
  } catch (e) { _stats = freshStats(); }
}

/* in-memory source of truth for this session — keeps the Progress tab in sync with
   answers recorded moments ago even before the debounced write lands. */
let _stats = readStats();
let _saveTimer = null;

function _scheduleSave() {
  try {
    // notify the cloud-sync engine (data-sync.jsx) so it can debounce a server push
    try { window.dispatchEvent(new CustomEvent('physl-progress-dirty')); } catch (e) {}
    if (_saveTimer) return;             // coalesce a burst of answers into one write
    _saveTimer = setTimeout(() => {
      _saveTimer = null;
      try { localStorage.setItem(STATS_KEY, JSON.stringify(_stats)); } catch (e) {}
    }, 400);
  } catch (e) {}
}
function flushStats() {
  try {
    if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
    localStorage.setItem(STATS_KEY, JSON.stringify(_stats));
  } catch (e) {}
}

/* ---- dates (local) ---- */
function _dayStr(d) {
  const x = d || new Date();
  const mm = String(x.getMonth() + 1).padStart(2, '0');
  const dd = String(x.getDate()).padStart(2, '0');
  return `${x.getFullYear()}-${mm}-${dd}`;
}

/* ---- mutators ---- */
function _modRollup(id) {
  if (!_stats.modules[id]) _stats.modules[id] = { q: { attempts: 0, correct: 0, items: {} }, c: { attempts: 0, known: 0, items: {} } };
  const r = _stats.modules[id];
  if (!r.q) r.q = { attempts: 0, correct: 0, items: {} };
  if (!r.c) r.c = { attempts: 0, known: 0, items: {} };
  return r;
}
function _bumpDay(kind) {
  const t = _dayStr();
  if (!_stats.days[t]) _stats.days[t] = { q: 0, c: 0 };
  _stats.days[t][kind] = (_stats.days[t][kind] || 0) + 1;
}

function recordAnswer(moduleId, itemKey, source, correct) {
  try {
    if (!moduleId || !itemKey) return;
    const r = _modRollup(moduleId);
    r.q.attempts++;
    if (correct) r.q.correct++;
    r.q.items[itemKey] = correct ? 1 : 0;        // latest result wins
    const src = source === 'textbook' ? 'textbook' : 'course';
    _stats.bySource[src].attempts++;
    if (correct) _stats.bySource[src].correct++;
    _bumpDay('q');
    _scheduleSave();
  } catch (e) {}
}

/* record a flashcard rating (LEARN mode only). Reads the card's current level,
   advances it via nextLevel(), and returns the new level for the UI. CRAM mode
   never calls this — it must not touch storage, the streak, or activity counts. */
function recordCardRating(moduleId, itemKey, rating) {
  try {
    if (!moduleId || !itemKey) return 0;
    const r = _modRollup(moduleId);
    const cur = (typeof r.c.items[itemKey] === 'number') ? r.c.items[itemKey] : 0;
    const lvl = nextLevel(cur, rating);
    r.c.items[itemKey] = lvl;
    r.c.attempts++;                          // lifetime rating events
    r.c.known = _countMastered(r.c.items);   // cards at the top level
    _bumpDay('c');
    _scheduleSave();
    return lvl;
  } catch (e) { return 0; }
}

/* legacy boolean API kept for safety — maps onto the rating model */
function recordCard(moduleId, itemKey, known) {
  return recordCardRating(moduleId, itemKey, known ? 'good' : 'again');
}

/* current stored level (0 = New) for a single card */
function cardLevel(moduleId, text) {
  try {
    const items = (((_stats.modules[moduleId] || {}).c) || {}).items || {};
    const v = items[statsKey(moduleId, text)];
    return (typeof v === 'number') ? v : 0;
  } catch (e) { return 0; }
}

/* per-deck mastery breakdown for the deck-detail panel. Takes the already
   source-filtered card list, so it honors the active Curated/Quizlet/All toggle.
   levels[i] = how many of these cards sit at level i; mastered = level === top. */
function cardBreakdown(moduleId, cards) {
  const out = { total: 0, attempted: 0, mastered: 0, pct: 0, levels: [0, 0, 0, 0, 0] };
  try {
    const list = cards || [];
    out.total = list.length;
    const items = (((_stats.modules[moduleId] || {}).c) || {}).items || {};
    list.forEach(c => {
      const v = items[statsKey(moduleId, c && c.t)];
      const lvl = (typeof v === 'number') ? v : 0;
      out.levels[lvl] = (out.levels[lvl] || 0) + 1;
      if (lvl > 0) out.attempted++;
      if (lvl === MASTERED) out.mastered++;
    });
    out.pct = out.total ? Math.round(out.mastered / out.total * 100) : 0;
  } catch (e) {}
  return out;
}

/* ---- derived reads ---- */

/* consecutive days with at least one recorded event, counting today if active
   (otherwise counts the run ending yesterday so a streak survives until day's end). */
function currentStreak(stats) {
  const days = (stats || _stats).days || {};
  let streak = 0;
  const cur = new Date();
  let key = _dayStr(cur);
  if (!days[key]) { cur.setDate(cur.getDate() - 1); key = _dayStr(cur); }
  while (days[key]) { streak++; cur.setDate(cur.getDate() - 1); key = _dayStr(cur); }
  return streak;
}

/* last n days oldest→newest for the activity chart */
function lastNDays(n, stats) {
  const days = (stats || _stats).days || {};
  const out = [];
  const cur = new Date();
  cur.setDate(cur.getDate() - (n - 1));
  for (let i = 0; i < n; i++) {
    const key = _dayStr(cur);
    const d = days[key] || {};
    out.push({ date: key, dow: cur.getDay(), dom: cur.getDate(), q: d.q || 0, c: d.c || 0, total: (d.q || 0) + (d.c || 0) });
    cur.setDate(cur.getDate() + 1);
  }
  return out;
}

/* ---- helpers shared with the pages ---- */

/* shallow-clone each item carrying its module id, so a mixed/exam set can attribute
   every answer to the right module without mutating the shared bank objects. */
function tagModule(arr, modId) {
  return (arr || []).map(o => (o && o._mod === modId) ? o : Object.assign({}, o, { _mod: modId }));
}

/* unified per-module mastery (questions + flashcards) — the single definition
   shared by Home, Practice and Progress so a module reads the same everywhere.
   Totals come from the loaded banks at call time; never hard-coded. */
function moduleMastery(id) {
  try {
    const TBQ = (typeof TEXTBOOK_QUESTIONS !== 'undefined') ? TEXTBOOK_QUESTIONS : {};
    const qTotal = (QUESTIONS[id] || []).filter(q => !q.src || q.src === 'course').length + (TBQ[id] || []).length;
    const cTotal = (FLASHCARDS[id] || []).length;
    const mr = _stats.modules[id] || {};
    const qItems = ((mr.q || {}).items) || {}, cItems = ((mr.c || {}).items) || {};
    const qKeys = Object.keys(qItems), cKeys = Object.keys(cItems);
    const qMastered = qKeys.reduce((n, k) => n + (qItems[k] === 1 ? 1 : 0), 0);
    const cKnown = cKeys.reduce((n, k) => n + (cItems[k] === MASTERED ? 1 : 0), 0);  // strict: only top-level cards count
    const total = qTotal + cTotal, mastered = qMastered + cKnown, attempted = qKeys.length + cKeys.length;
    return { qMastered, qTotal, cKnown, cTotal, mastered, total, attempted, pct: total ? Math.round(mastered / total * 100) : 0 };
  } catch (e) { return { qMastered:0, qTotal:0, cKnown:0, cTotal:0, mastered:0, total:0, attempted:0, pct:0 }; }
}

function resetStats() {
  try {
    if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
    localStorage.removeItem(STATS_KEY);
  } catch (e) {}
  _stats = freshStats();
}

/* persist any pending write before the tab is hidden or closed */
try {
  window.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') flushStats(); });
  window.addEventListener('pagehide', flushStats);
} catch (e) {}

Object.assign(window, {
  STATS_KEY, statsKey, readStats, recordAnswer, recordCard,
  currentStreak, lastNDays, tagModule, resetStats, flushStats, moduleMastery,
  recordCardRating, nextLevel, cardLevel, cardBreakdown,
  CARD_MAX_LEVEL, CARD_LEVEL_LABELS, hydrateStats,
  liveStats: () => _stats,
});
