/* modes/prep.js — Opening preparation: repertoires as variation trees,
   spaced-repetition drilling, coverage statistics, transposition
   detection, PGN import, weak-line report. */
'use strict';

import { h, clear } from '../ui/dom.js';
import { icon } from '../ui/icons.js';
import { showDialog, promptDialog, confirmDialog, toast, makePanel, emptyState, attachContextMenu } from '../ui/widgets.js';
import { Dock, loadLayout, saveLayout } from '../ui/dock.js';
import { GameDoc, parsePgnGame, splitPgn } from '../core/doc.js';
import { positionKey, gamedb } from '../svc/gamedb.js';
import { idb } from '../svc/idb.js';
import { commands } from '../svc/commands.js';
import { sound } from '../svc/sound.js';
import { BoardSession } from '../panels/session.js';
import { createNotationPanel } from '../panels/notation.js';
import { downloadText } from '../ui/appbus.js';

/* repertoire: {id, name, color, doc(serialized), srs:{pathKey:{due,iv,ease,lapses,seen}}, created} */

const DAY = 86400000;

export const prepMode = {
  id: 'prep', title: 'Openings', icon: 'bookOpen', kbd: 'Ctrl+9',

  mount(root, ctx) {
    this.ctx = ctx;
    this.reps = [];
    this.rep = null;
    this.doc = new GameDoc();
    this.drill = null;          // active drill state
    this._saveTimer = null;

    /* ---- repertoire list ---- */
    this.listEl = h('div', { style: { overflowY: 'auto', height: '100%' } });
    const listPanel = makePanel({
      id: 'replist', title: 'Repertoires', icon: 'bookOpen',
      actions: [
        { icon: 'plus', title: 'New repertoire', onClick: () => this.newRep() },
        { icon: 'upload', title: 'Import PGN as repertoire', onClick: () => this.importRep() },
      ],
      body: this.listEl,
    });

    /* ---- stats panel ---- */
    this.statsEl = h('div', { style: { padding: '10px', display: 'flex', flexDirection: 'column', gap: '10px', fontSize: 'var(--fs-12)' } });
    const statsPanel = makePanel({ id: 'repstats', title: 'Repertoire health', icon: 'graph', body: this.statsEl });

    /* ---- board ---- */
    this.session = new BoardSession({
      doc: this.doc,
      userTurn: () => this.drill ? this.drillTurn() : 'both',
      onUserMove: uci => this.drill ? this.drillMove(uci) : this.editMove(uci),
    });
    this.notation = createNotationPanel(this.doc, {});

    this.toolbar = h('div.toolbar', {},
      this.titleEl = h('span.mode-toolbar-title', {}, 'No repertoire open'),
      h('div.toolbar-sep'),
      this.btnDrill = h('button.btn.btn-primary.btn-sm', { html: icon('repeat') + '<span>Train due lines</span>', onclick: () => this.startDrill() }),
      this.btnStopDrill = h('button.btn.btn-sm', { html: icon('stop') + '<span>Stop drill</span>', style: { display: 'none' }, onclick: () => this.stopDrill() }),
      h('div.toolbar-sep'),
      h('button.btn.btn-sm', { html: icon('search') + '<span>Coverage check</span>', title: 'Find popular moves (from your databases) that the repertoire does not answer', onclick: () => this.coverage() }),
      h('button.btn.btn-ghost.btn-sm', { html: icon('download') + '<span>Report</span>', onclick: () => this.report() }),
      h('div.toolbar-spacer'),
      this.drillStatus = h('span.mode-toolbar-title'),
    );

    const defaults = {
      left: { size: 240, panels: ['replist'] },
      right: { size: 320, panels: ['notation', 'repstats'] },
      bottom: { size: 140, panels: [] }, hidden: [],
    };
    this.dock = new Dock(root, {
      registry: {
        replist: { title: 'Repertoires', icon: 'bookOpen', create: () => listPanel },
        notation: { title: 'Lines', icon: 'list', create: () => this.notation.panel },
        repstats: { title: 'Repertoire health', icon: 'graph', create: () => statsPanel },
      },
      layout: loadLayout('prep', defaults), defaults,
      onChange: l => saveLayout('prep', l),
    });
    this.dock.centerEl.append(h('div.dock-col', { style: { flex: 1, height: '100%' } }, this.toolbar, this.session.el));
    this.dock.render();

    commands.registerAll([
      { id: 'prep.drill', title: 'Train opening repertoire', category: 'Openings', icon: 'repeat', run: () => { ctx.switchMode('prep'); this.startDrill(); } },
    ]);

    this.load();
  },

  activate() { this.session.update(false); this.paintStats(); },
  deactivate() { this.saveNow(); this.stopDrill(); },
  getDoc() { return this.doc; },
  getSession() { return this.session; },
  toggleZen() { this.dock.setZen(!this.dock.zen); },

  /* ================= persistence ================= */
  async load() {
    this.reps = await idb.getAll('repertoires');
    this.paintList();
    if (this.reps.length && !this.rep) this.openRep(this.reps[0]);
  },
  scheduleSave() { clearTimeout(this._saveTimer); this._saveTimer = setTimeout(() => this.saveNow(), 700); },
  async saveNow() {
    if (!this.rep) return;
    this.rep.doc = this.doc.serialize();
    await idb.put('repertoires', this.rep);
  },

  paintList() {
    clear(this.listEl);
    if (!this.reps.length) {
      this.listEl.append(emptyState({
        icon: 'bookOpen', title: 'No repertoires',
        hint: 'Create a White or Black repertoire, then add lines by playing moves on the board.',
        action: h('button.btn.btn-primary', { onclick: () => this.newRep() }, 'New repertoire'),
      }));
      return;
    }
    for (const rep of this.reps) {
      const due = this.dueCount(rep);
      const row = h('div.list-row', {
        class: this.rep?.id === rep.id ? 'sel' : '',
        onclick: () => this.openRep(rep),
      },
        h('span', { html: icon('bookOpen', 'ic-sm'), style: { color: rep.color === 'w' ? 'var(--eval-white)' : 'var(--text-faint)' } }),
        h('span.grow', {}, rep.name),
        due ? h('span.badge.acc', {}, due + ' due') : null);
      attachContextMenu(row, () => [
        { label: 'Rename…', icon: 'pencil', action: async () => {
          const name = await promptDialog({ title: 'Rename repertoire', label: 'Name', value: rep.name });
          if (name) { rep.name = name; await idb.put('repertoires', rep); this.paintList(); }
        } },
        { label: 'Delete', icon: 'trash', danger: true, action: async () => {
          if (await confirmDialog({ title: 'Delete repertoire', message: `Delete "${rep.name}"?`, confirmLabel: 'Delete', danger: true })) {
            await idb.del('repertoires', rep.id);
            if (this.rep?.id === rep.id) this.rep = null;
            this.load();
          }
        } },
      ]);
      this.listEl.append(row);
    }
  },

  async newRep() {
    const colorRow = h('div.radio-row', {},
      h('button.seg.active', { onclick: e => pick('w', e) }, 'For White'),
      h('button.seg', { onclick: e => pick('b', e) }, 'For Black'));
    let color = 'w';
    const pick = (c, e) => { color = c; colorRow.querySelectorAll('.seg').forEach(x => x.classList.toggle('active', x === e.target)); };
    const nameIn = h('input.input', { placeholder: 'e.g. 1.e4 repertoire' });
    const dlg = showDialog({
      title: 'New repertoire',
      body: h('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px', minWidth: '320px' } },
        h('div.field', {}, h('label', {}, 'Name'), nameIn),
        h('div.field', {}, h('label', {}, 'Side'), colorRow)),
      footer: [
        h('button.btn', { onclick: () => dlg.close() }, 'Cancel'),
        h('button.btn.btn-primary', { onclick: async () => {
          if (!nameIn.value.trim()) return;
          dlg.close();
          const rep = { name: nameIn.value.trim(), color, doc: new GameDoc().serialize(), srs: {}, created: Date.now() };
          rep.id = await idb.add('repertoires', rep);
          await idb.put('repertoires', rep);
          this.reps.push(rep);
          this.openRep(rep);
        } }, 'Create'),
      ],
    });
  },

  async importRep() {
    const text = await promptDialog({ title: 'Import repertoire PGN', label: 'Games/lines are merged into one tree', multiline: true, confirmLabel: 'Import' });
    if (!text) return;
    try {
      const chunks = splitPgn(text);
      const merged = new GameDoc();
      let lines = 0;
      for (const chunk of chunks) {
        try {
          const doc = parsePgnGame(chunk);
          const walk = (node, path) => {
            const p = [...path, node.uci];
            for (const child of node.children) walk(child, p);
            if (!node.children.length) {
              merged.goStart();
              for (const u of p) merged.playMove(u);
              lines++;
            }
          };
          doc.root.children.forEach(c => walk(c, []));
        } catch { /* skip */ }
      }
      if (!lines) throw new Error('no readable lines');
      merged.goStart();
      const name = await promptDialog({ title: 'Repertoire name', label: 'Name', value: 'Imported repertoire' });
      if (!name) return;
      const rep = { name, color: 'w', doc: merged.serialize(), srs: {}, created: Date.now() };
      rep.id = await idb.add('repertoires', rep);
      await idb.put('repertoires', rep);
      this.reps.push(rep);
      this.openRep(rep);
      toast(`Imported ${lines} lines`, 'ok');
    } catch (e) { toast('Import failed: ' + e.message, 'err'); }
  },

  openRep(rep) {
    this.saveNow();
    this.stopDrill();
    this.rep = rep;
    this.doc = GameDoc.deserialize(rep.doc);
    this._docUnsub?.forEach(u => u());
    this._docUnsub = [
      this.doc.on('change', () => { this.scheduleSave(); this.paintStats(); }),
      this.doc.on('navigate', () => this.checkTransposition()),
    ];
    this.session.bindDoc(this.doc);
    this.notation.bind(this.doc);
    this.session.board.setOrientation(rep.color);
    this.titleEl.textContent = `${rep.name} (${rep.color === 'w' ? 'White' : 'Black'})`;
    this.paintList();
    this.paintStats();
  },

  editMove(uci) {
    const pos = this.doc.position;
    const m = pos.findUci(uci);
    if (!m) return;
    this.session.playSoundFor(m);
    this.doc.playMove(m);
  },

  checkTransposition() {
    if (this.drill) return;
    const key = positionKey(this.doc.position.fen());
    const cur = this.doc.current;
    if (cur === this.doc.root) return;
    let dupe = null;
    for (const n of this.doc.flatten()) {
      if (n === cur) continue;
      const pos = this.doc.positionAt(n);
      if (positionKey(pos.fen()) === key) { dupe = n; break; }
    }
    if (dupe) toast(`Transposition: this position also arises after ${dupe.san} (move ${this.doc.moveNumberAt(dupe)})`, 'info');
  },

  /* ================= SRS drill ================= */
  userMoveNodes() {
    const mine = [];
    const walk = n => {
      for (const c of n.children) {
        const isWhiteMove = this.doc.isWhiteMove(c);
        if ((this.rep.color === 'w') === isWhiteMove) mine.push(c);
        walk(c);
      }
    };
    walk(this.doc.root);
    return mine;
  },

  pathKey(node) { return this.doc.path(node).map(n => n.uci).join(' '); },

  dueCount(rep) {
    if (!rep.srs) return 0;
    const now = Date.now();
    let n = 0;
    for (const v of Object.values(rep.srs)) if (v.due <= now) n++;
    return n;
  },

  startDrill() {
    if (!this.rep) return toast('Open a repertoire first', 'warn');
    const nodes = this.userMoveNodes();
    if (!nodes.length) return toast('Add some lines first — play moves on the board', 'warn');
    const now = Date.now();
    const srs = this.rep.srs = this.rep.srs || {};
    const scored = nodes.map(n => {
      const k = this.pathKey(n);
      const s = srs[k];
      return { n, k, due: s ? s.due : 0, seen: !!s };
    });
    const queue = scored.filter(x => !x.seen || x.due <= now)
      .sort((a, b) => a.due - b.due)
      .slice(0, 20);
    if (!queue.length) return toast('Nothing due — all lines are scheduled for later.', 'ok');
    this.drill = { queue, i: 0, correct: 0, wrong: 0, missed: [] };
    this.btnDrill.style.display = 'none';
    this.btnStopDrill.style.display = '';
    this.nextDrillItem();
  },

  nextDrillItem() {
    const d = this.drill;
    if (!d || d.i >= d.queue.length) return this.finishDrill();
    const item = d.queue[d.i];
    this.doc.goTo(item.n.parent === this.doc.root ? this.doc.root : item.n.parent);
    this.session.update(false);
    this.drillStatus.textContent = `Line ${d.i + 1}/${d.queue.length} — play the repertoire move`;
  },

  drillTurn() {
    return this.rep ? this.rep.color : null;
  },

  drillMove(uci) {
    const d = this.drill;
    if (!d) return;
    const item = d.queue[d.i];
    const srs = this.rep.srs;
    const s = srs[item.k] || { iv: 0, ease: 2.3, lapses: 0, seen: 0 };
    if (uci === item.n.uci) {
      sound.correct();
      d.correct++;
      s.iv = s.iv ? Math.round(s.iv * s.ease) : 1;
      s.due = Date.now() + s.iv * DAY;
      s.seen++;
      srs[item.k] = s;
      this.doc.goTo(item.n);
      this.session.update();
    } else {
      sound.wrong();
      d.wrong++;
      d.missed.push(item.n);
      s.iv = 0; s.lapses = (s.lapses || 0) + 1;
      s.ease = Math.max(1.4, s.ease - 0.2);
      s.due = Date.now() + 10 * 60 * 1000;   // again in 10 min
      s.seen++;
      srs[item.k] = s;
      const pos = this.doc.position;
      const m = pos.findUci(item.n.uci);
      if (m) this.session.board.setArrows([{ from: m.from, to: m.to, color: 'green' }]);
      this.drillStatus.textContent = `Correct was ${item.n.san} — continuing…`;
    }
    this.scheduleSave();
    d.i++;
    setTimeout(() => { this.session.board.setArrows([]); this.nextDrillItem(); }, uci === item.n.uci ? 450 : 1600);
  },

  stopDrill() {
    if (!this.drill) return;
    this.drill = null;
    this.btnDrill.style.display = '';
    this.btnStopDrill.style.display = 'none';
    this.drillStatus.textContent = '';
    this.doc.goStart();
    this.session.update(false);
  },

  finishDrill() {
    const d = this.drill;
    this.stopDrill();
    this.paintList();
    this.paintStats();
    showDialog({
      title: 'Drill complete',
      body: h('div', { style: { minWidth: '300px', display: 'flex', flexDirection: 'column', gap: '10px' } },
        h('div.stat-strip', {},
          h('div.stat-cell', {}, h('div.sc-label', {}, 'Correct'), h('div.sc-value', { style: { color: 'var(--ok-text)' } }, String(d.correct))),
          h('div.stat-cell', {}, h('div.sc-label', {}, 'Missed'), h('div.sc-value', { style: { color: 'var(--err-text)' } }, String(d.wrong)))),
        d.missed.length ? h('div', {},
          h('div.settings-group-title', {}, 'Recently missed'),
          ...d.missed.slice(0, 8).map(n => h('div.muted', {}, this.doc.path(n).map(x => x.san).join(' ')))) : null),
    });
  },

  /* ================= analysis of repertoire ================= */
  paintStats() {
    if (!this.rep) { clear(this.statsEl); return; }
    clear(this.statsEl);
    const mine = this.userMoveNodes();
    const opp = this.doc.flatten().length - mine.length;
    const srs = this.rep.srs || {};
    const now = Date.now();
    let due = 0, learned = 0, weak = 0;
    for (const n of mine) {
      const s = srs[this.pathKey(n)];
      if (!s) continue;
      if (s.due <= now) due++;
      if (s.iv >= 4) learned++;
      if ((s.lapses || 0) >= 2) weak++;
    }
    const trained = mine.filter(n => srs[this.pathKey(n)]).length;
    this.statsEl.append(
      h('div.stat-strip', {},
        h('div.stat-cell', {}, h('div.sc-label', {}, 'Your moves'), h('div.sc-value', {}, String(mine.length))),
        h('div.stat-cell', {}, h('div.sc-label', {}, 'Replies'), h('div.sc-value', {}, String(opp)))),
      h('div.stat-strip', {},
        h('div.stat-cell', {}, h('div.sc-label', {}, 'Trained'), h('div.sc-value', {}, mine.length ? Math.round(trained / mine.length * 100) + '%' : '-')),
        h('div.stat-cell', {}, h('div.sc-label', {}, 'Due now'), h('div.sc-value', {}, String(due))),
        h('div.stat-cell', {}, h('div.sc-label', {}, 'Weak lines'), h('div.sc-value', { style: weak ? { color: 'var(--err-text)' } : {} }, String(weak)))),
      h('div.sr-hint', {}, 'Weak lines have been missed twice or more. Build lines by playing moves; the side to move alternates automatically. Right-click moves in Lines to annotate or reorder priorities.'),
    );
  },

  async coverage() {
    if (!this.rep) return;
    const gaps = [];
    const nodes = [this.doc.root, ...this.doc.flatten()];
    for (const n of nodes.slice(0, 200)) {
      const ply = n === this.doc.root ? 0 : this.doc.ply(n);
      const isOppToMove = n === this.doc.root
        ? this.rep.color === 'b'
        : this.doc.isWhiteMove(n) === (this.rep.color === 'w');
      if (!isOppToMove || ply > 16) continue;
      const pos = this.doc.positionAt(n);
      const stats = await gamedb.explorer(pos.fen(), null);
      const covered = new Set(n.children.map(c => c.san.replace(/[+#]/g, '')));
      for (const s of stats.slice(0, 3)) {
        if (s.count >= 5 && !covered.has(s.san.replace(/[+#]/g, '')))
          gaps.push({ line: n === this.doc.root ? '(start)' : this.doc.path(n).map(x => x.san).join(' '), move: s.san, count: s.count });
      }
      if (gaps.length >= 20) break;
    }
    showDialog({
      title: 'Coverage check',
      body: gaps.length
        ? h('div', { style: { maxHeight: '50vh', overflowY: 'auto', minWidth: '420px' } },
            h('p.muted', { style: { marginBottom: '8px', fontSize: 'var(--fs-12)' } }, 'Popular opponent moves (in your databases) without a prepared reply:'),
            ...gaps.map(g => h('div.setting-row', {},
              h('div.sr-label', {}, g.line, h('div.sr-hint', {}, `opponent plays ${g.move} (${g.count} games)`)))))
        : h('div', { style: { minWidth: '320px' } }, 'No gaps found against your database games (checked the first 16 plies). Import more games to sharpen the check.'),
    });
  },

  report() {
    if (!this.rep) return;
    const mine = this.userMoveNodes();
    const srs = this.rep.srs || {};
    let out = `Repertoire report — ${this.rep.name} (${this.rep.color === 'w' ? 'White' : 'Black'})\n`;
    out += `Generated ${new Date().toLocaleString()}\n\n`;
    out += `Moves to know: ${mine.length}\n`;
    out += `Trained: ${mine.filter(n => srs[this.pathKey(n)]).length}\n\n`;
    out += 'LINES\n=====\n';
    const walk = (n, depth) => {
      for (const c of n.children) {
        const s = srs[this.pathKey(c)];
        out += '  '.repeat(depth) + `${this.doc.moveNumberAt(c)}${this.doc.isWhiteMove(c) ? '.' : '...'} ${c.san}`
          + (c.comment ? `  {${c.comment}}` : '')
          + (s && (s.lapses || 0) >= 2 ? '  [WEAK]' : '') + '\n';
        walk(c, depth + 1);
      }
    };
    walk(this.doc.root, 0);
    downloadText(this.rep.name.replace(/[^\w-]+/g, '_') + '_report.txt', out);
    toast('Report exported', 'ok');
  },
};
