/* modes/analysis.js — Analysis mode.
   Multiple game tabs, two simultaneous engine panels, MultiPV, live eval
   bar + graph, clickable lines, insert-as-variation, automated game
   review (blunder detection, quality tags, accuracy), threat display,
   opening explorer, annotation editing. */
'use strict';

import { h, clear } from '../ui/dom.js';
import { icon } from '../ui/icons.js';
import { showDialog, promptDialog, confirmDialog, toast, makePanel, emptyState } from '../ui/widgets.js';
import { Dock, loadLayout, saveLayout } from '../ui/dock.js';
import { GameDoc, parsePgnGame } from '../core/doc.js';
import { Chess } from '../core/rules.js';
import { openingName } from '../core/openings.js';
import { settings } from '../svc/settings.js';
import { engines, whiteScore, winPercent, fmtEval } from '../svc/engines.js';
import { gamedb } from '../svc/gamedb.js';
import { commands } from '../svc/commands.js';
import { BoardSession } from '../panels/session.js';
import { createNotationPanel } from '../panels/notation.js';
import { createEnginePanel } from '../panels/enginepanel.js';
import { createEvalGraph } from '../panels/evalgraph.js';
import { createExplorerPanel } from '../panels/explorer.js';
import { setStatusText, downloadText } from '../ui/appbus.js';

const PIECE_VAL = { 1: 1, 2: 3, 3: 3, 4: 5, 5: 9, 6: 0 };
/** white-centric material (sum of piece values, White positive). */
function materialBalance(pos) {
  let m = 0;
  for (let s = 0; s < 128; s++) {
    if (s & 0x88) continue;
    const pc = pos.board[s];
    if (pc) m += Math.sign(pc) * PIECE_VAL[Math.abs(pc)];
  }
  return m;
}
/** rough Elo estimate from average centipawn loss (community-style curve). */
function eloFromAcpl(acpl) {
  if (acpl == null) return null;
  return Math.max(250, Math.min(2850, Math.round(3000 * Math.exp(-acpl / 130))));
}

export const analysisMode = {
  id: 'analysis', title: 'Analysis', icon: 'microscope', kbd: 'Ctrl+2',

  mount(root, ctx) {
    this.ctx = ctx;
    this.tabs = [];             // [{doc, label}]
    this.active = -1;
    this.reviewing = false;

    /* --- tab strip --- */
    this.tabstrip = h('div.tabstrip');

    /* --- toolbar --- */
    this.toolbar = h('div.toolbar', {},
      this.btnLive = h('button.btn.btn-sm', {
        html: icon('cpu') + '<span>Live engine</span>',
        title: 'Continuously evaluate the current position (drives the eval bar)',
        onclick: () => this.toggleLive(),
      }),
      h('div.toolbar-sep'),
      h('button.btn.btn-sm', { html: icon('plus') + '<span>New</span>', title: 'New analysis board', onclick: () => this.openDoc(new GameDoc(), 'New game') }),
      h('button.btn.btn-sm', { html: icon('upload') + '<span>Load PGN</span>', onclick: () => this.loadPgnDialog() }),
      h('button.btn.btn-sm', { html: icon('file') + '<span>Load FEN</span>', onclick: () => this.loadFenDialog() }),
      h('div.toolbar-sep'),
      this.btnReview = h('button.btn.btn-sm', { html: icon('zap') + '<span>Game review</span>', title: 'Evaluate every move, tag mistakes, compute accuracy', onclick: () => this.runReview() }),
      h('button.btn.btn-sm', { html: icon('warning') + '<span>Show threat</span>', title: 'What would the opponent play if it were their move?', onclick: () => this.showThreat() }),
      h('div.toolbar-sep'),
      h('button.btn.btn-ghost.btn-sm', { html: icon('save') + '<span>Save to DB</span>', onclick: () => this.saveToDb() }),
      h('button.btn.btn-ghost.btn-sm', { html: icon('flip'), title: 'Flip board (F)', onclick: () => { this.session.board.flip(); this.session.updateBars(); } }),
      h('div.toolbar-spacer'),
      this.reviewStatus = h('span.mode-toolbar-title'),
    );

    /* --- board --- */
    this.session = new BoardSession({
      doc: new GameDoc(),
      userTurn: () => 'both',
      onUserMove: uci => {
        const doc = this.doc();
        if (!doc) return;
        const pos = doc.position;
        const m = pos.findUci(uci);
        if (m) { this.session.playSoundFor(m); doc.playMove(m); }
      },
    });

    /* --- panels --- */
    this.notation = createNotationPanel(this.session.doc, { showEvals: true });
    this.graph = createEvalGraph(this.session.doc, {});
    this.engineA = createEnginePanel({
      id: 'engineA', title: 'Engine A', engine: engines.has('stockfish') ? 'stockfish' : 'rush', instance: 'anA',
      getTarget: () => this.target(),
      onPlayLine: (pv, count) => this.insertLine(pv, count),
      onBestMove: (uci, score) => this.onBestInfo(uci, score),
    });
    this.engineB = createEnginePanel({
      id: 'engineB', title: 'Engine B', engine: engines.has('rush') ? 'rush' : 'stockfish', instance: 'anB',
      getTarget: () => this.target(),
      onPlayLine: (pv, count) => this.insertLine(pv, count),
    });
    // keep the toolbar toggle in sync with the engine's running state
    this.engineA.analyser.on('state', running => {
      this.btnLive.classList.toggle('btn-primary', running);
      this.btnLive.innerHTML = icon('cpu') + `<span>${running ? 'Engine on' : 'Live engine'}</span>`;
      if (!running) this.session.board.setArrows([]);
    });
    this.explorer = createExplorerPanel({
      getFen: () => this.doc() ? this.doc().position.fen() : null,
      onPlaySan: san => { const d = this.doc(); d && d.playSan(san); },
      onOpenGame: async id => {
        const doc = await gamedb.loadDoc(id);
        if (doc) this.openDoc(doc);
      },
    });

    const defaults = {
      left: { size: 260, panels: ['explorer'] },
      right: { size: 340, panels: ['engineA', 'notation'] },
      bottom: { size: 130, panels: ['evalgraph'] },
      hidden: ['engineB'],
    };
    this.dock = new Dock(root, {
      registry: {
        notation: { title: 'Notation', icon: 'list', create: () => this.notation.panel },
        engineA: { title: 'Engine A', icon: 'cpu', create: () => this.engineA.panel },
        engineB: { title: 'Engine B', icon: 'cpu', create: () => this.engineB.panel },
        evalgraph: { title: 'Evaluation', icon: 'graph', create: () => this.graph.panel },
        explorer: { title: 'Opening explorer', icon: 'bookOpen', create: () => this.explorer.panel },
      },
      layout: loadLayout('analysis', defaults), defaults,
      onChange: l => saveLayout('analysis', l),
    });
    const center = h('div.dock-col', { style: { flex: 1, height: '100%' } }, this.tabstrip, this.toolbar, this.session.el);
    this.dock.centerEl.append(center);
    this.dock.render();

    commands.registerAll([
      { id: 'an.engine', title: 'Toggle engine analysis', category: 'Analysis', icon: 'cpu', kbd: 'Ctrl+E', run: () => this.engineA.analyser.toggle() },
      { id: 'an.review', title: 'Run game review', category: 'Analysis', icon: 'zap', run: () => { ctx.switchMode('analysis'); this.runReview(); } },
      { id: 'an.secondEngine', title: 'Toggle second engine panel', category: 'Analysis', icon: 'cpu', run: () => this.dock.togglePanel('engineB') },
      { id: 'an.explorerPanel', title: 'Toggle opening explorer panel', category: 'Analysis', icon: 'bookOpen', run: () => this.dock.togglePanel('explorer') },
      { id: 'an.threat', title: 'Show threat', category: 'Analysis', icon: 'warning', run: () => this.showThreat() },
    ]);

    this.openDoc(new GameDoc(), 'New game');
  },

  activate() {
    this.session.update(false);
    this.retarget();
    // resume live analysis automatically (default on) so the eval bar is live.
    // No availability gate: the analyser loop waits for an engine to appear,
    // which avoids a race when engine status loads after the mode mounts.
    if (settings.get('analysis.autoAnalyse') && !this.engineA.analyser.running)
      this.engineA.analyser.start();
  },
  deactivate() {
    this.engineA.analyser.stop();
    this.engineB.analyser.stop();
  },
  getDoc() { return this.doc(); },
  getSession() { return this.session; },
  toggleZen() { this.dock.setZen(!this.dock.zen); },

  toggleLive() {
    if (!engines.available().length) return toast('Engines offline — start server.py', 'err');
    this.engineA.analyser.toggle();
    // remember the user's choice as the new default
    settings.set('analysis.autoAnalyse', this.engineA.analyser.running);
  },

  doc() { return this.tabs[this.active]?.doc || null; },

  target() {
    const doc = this.doc();
    if (!doc) return null;
    const pos = doc.position;
    return {
      fen: doc.startFen,
      moves: doc.path().map(n => n.uci),
      fenNow: pos.fen(),
      stm: pos.turn === 1 ? 'w' : 'b',
    };
  },

  /* ================= tabs ================= */
  openDoc(doc, label) {
    const name = label || `${doc.headers.White} - ${doc.headers.Black}`;
    this.tabs.push({ doc, label: name });
    this.selectTab(this.tabs.length - 1);
  },

  selectTab(i) {
    if (i < 0 || i >= this.tabs.length) return;
    this.active = i;
    const doc = this.tabs[i].doc;
    this._docUnsub?.forEach(u => u());
    this._docUnsub = [
      doc.on('navigate', () => this.onNavigate()),
      doc.on('change', () => this.onNavigate()),
    ];
    this.session.bindDoc(doc);
    this.notation.bind(doc);
    this.graph.bind(doc);
    this.session.setPlayers({ w: { name: doc.headers.White, rating: doc.headers.WhiteElo }, b: { name: doc.headers.Black, rating: doc.headers.BlackElo } });
    this.paintTabs();
    this.onNavigate();
  },

  closeTab(i) {
    this.tabs.splice(i, 1);
    if (!this.tabs.length) { this.openDoc(new GameDoc(), 'New game'); return; }
    this.selectTab(Math.min(i, this.tabs.length - 1));
  },

  paintTabs() {
    clear(this.tabstrip);
    this.tabs.forEach((t, i) => {
      this.tabstrip.append(h('button.doc-tab', {
        class: i === this.active ? 'active' : '',
        onclick: () => this.selectTab(i),
        onauxclick: e => { if (e.button === 1) this.closeTab(i); },
      },
        h('span.doc-tab-label', {}, t.label),
        h('span.tab-close', {
          html: icon('close', 'ic-sm'), title: 'Close tab',
          onclick: e => { e.stopPropagation(); this.closeTab(i); },
        })));
    });
    this.tabstrip.append(h('button.tab-add', { html: icon('plus'), title: 'New analysis tab', onclick: () => this.openDoc(new GameDoc(), 'New game') }));
  },

  /* ================= navigation / engines ================= */
  onNavigate() {
    this.retarget();
    const doc = this.doc();
    if (!doc) return;
    const cur = doc.current;
    const pos = doc.position;
    // terminal positions: the engine returns no score, so set the bar directly
    if (pos.legalMoves().length === 0) {
      this.session.setEval(pos.inCheck(pos.turn)
        ? { mate: pos.turn === 1 ? -1 : 1 }   // side to move is checkmated
        : { cp: 0 });                          // stalemate
    } else if (cur.evalCp != null || cur.evalMate != null) {
      this.session.setEval(cur.evalMate != null ? { mate: cur.evalMate } : { cp: cur.evalCp });
    } else {
      const r = doc.result();                  // 50-move / repetition / material draws
      if (r && r.result === '1/2-1/2') this.session.setEval({ cp: 0 });
      // no eval for this node yet: if the engine isn't live to fill it in,
      // reset to neutral so we never show a stale score from another position
      else if (!this.engineA.analyser.running) this.session.setEval({ cp: 0 });
    }
    if (settings.get('general.openingNames'))
      setStatusText(openingName(doc.path().map(n => n.san)) || '');
    this.explorer.refresh();
  },

  retarget() {
    const t = this.target();
    if (!t) return;
    this.engineA.retarget();
    this.engineB.retarget();
    this.session.board.setArrows([]);
  },

  onBestInfo(uci, score) {
    // eval bar + best-move arrow from engine A
    this.session.setEval(score);
    const doc = this.doc();
    if (!doc) return;
    const cur = doc.current;
    if (score && (cur.evalCp == null || (this.engineA.analyser.last?.depth || 0) >= (cur.evalDepth || 0))) {
      cur.evalCp = score.cp ?? null;
      cur.evalMate = score.mate ?? null;
      cur.evalDepth = this.engineA.analyser.last?.depth || null;
      this.graph.draw();
    }
    if (settings.get('analysis.arrows') && this.engineA.analyser.running) {
      const m = doc.position.findUci(uci);
      if (m) this.session.board.setArrows([{ from: m.from, to: m.to, color: 'engine' }]);
    }
  },

  insertLine(pv, count) {
    const doc = this.doc();
    if (!doc || !pv?.length) return;
    const from = doc.current;
    let ok = 0;
    for (const u of pv.slice(0, count)) {
      if (!doc.playMove(u)) break;
      ok++;
    }
    if (count > 1 && ok) doc.goTo(from.children.find(c => c.uci === pv[0]) || doc.current);
  },

  /* ================= threat ================= */
  async showThreat() {
    const doc = this.doc();
    if (!doc) return;
    const pos = doc.position;
    // null-move position: pass the turn (clear ep, keep everything else)
    const parts = pos.fen().split(' ');
    parts[1] = parts[1] === 'w' ? 'b' : 'w';
    parts[3] = '-';
    const nullFen = parts.join(' ');
    try { new Chess(nullFen); } catch { return toast('Cannot compute threat here', 'warn'); }
    const eng = this.engineA.engineKey;
    const res = await engines.go({ engine: eng, instance: 'threat', fen: nullFen, movetime: 700 });
    if (!res?.bestmove) return toast('Threat unavailable (engine offline?)', 'warn');
    const tPos = new Chess(nullFen);
    const m = tPos.findUci(res.bestmove);
    if (!m) return;
    this.session.board.setArrows([{ from: m.from, to: m.to, color: 'red' }]);
    toast('Threat: ' + tPos.san(m), 'info');
  },

  /* ================= review ================= */
  async runReview() {
    const doc = this.doc();
    if (!doc || this.reviewing) return;
    const line = doc.mainline();
    if (line.length < 2) return toast('Nothing to review — load or play a game first', 'warn');
    if (!engines.available().length) return toast('Engines offline — start server.py', 'err');
    const engKey = engines.has('stockfish') ? 'stockfish' : 'rush';
    const perMove = settings.get('analysis.reviewMs');
    const blunderCp = settings.get('analysis.blunderCp');
    const mistakeCp = settings.get('analysis.mistakeCp');

    this.reviewing = true;
    this.btnReview.disabled = true;
    this.engineA.analyser.stop();
    this.engineB.analyser.stop();

    const pos = new Chess(doc.startFen);
    const evals = [];       // white-centric cp (mate -> +/-10000) per position (before move i)
    const moves = [];
    const stmList = [];
    let aborted = false;
    try {
      for (let i = 0; i <= line.length; i++) {
        this.reviewStatus.textContent = `Reviewing ${i}/${line.length}…`;
        const stm = pos.turn === 1 ? 'w' : 'b';
        stmList.push(stm);
        const res = await engines.go({
          engine: engKey, instance: 'review',
          fen: pos.fen(), movetime: perMove, multipv: 1,
        });
        if (!res) { aborted = true; break; }
        const sc = whiteScore(res, stm);
        evals.push(sc ? (sc.mate != null ? Math.sign(sc.mate) * 10000 : sc.cp) : 0);
        moves.push(res.bestmove ? { best: res.bestmove, pv: res.pv } : null);
        if (i < line.length) {
          const m = pos.findUci(line[i].uci);
          if (!m) { aborted = true; break; }
          pos.makeMove(m);
        }
      }
      if (!aborted) {
        const acc = { w: [], b: [] };
        const acplSum = { w: 0, b: 0 }, acplN = { w: 0, b: 0 };
        const stillBook = { w: true, b: true };
        // material (white-centric) at each ply boundary, for sacrifice detection
        const matAt = ply => {
          const p = new Chess(doc.startFen);
          for (let j = 0; j < ply; j++) { const mv = p.findUci(line[j].uci); if (!mv) break; p.makeMove(mv); }
          return materialBalance(p);
        };

        for (let i = 0; i < line.length; i++) {
          const node = line[i];
          const mover = stmList[i];
          const before = evals[i], after = evals[i + 1];
          const loss = Math.max(0, mover === 'w' ? before - after : after - before);   // cp thrown away
          node.evalCp = Math.abs(after) >= 10000 ? null : after;
          node.evalMate = Math.abs(after) >= 10000 ? Math.sign(after) * 1 : null;
          node.evalDepth = null;
          const wpBefore = winPercent(mover === 'w' ? before : -before);
          const wpAfter = winPercent(mover === 'w' ? after : -after);
          acc[mover].push(Math.max(0, Math.min(100, 100 - (wpBefore - wpAfter) * 2.2)));
          acplSum[mover] += Math.min(loss, 1000); acplN[mover]++;
          const wasBest = moves[i]?.best === node.uci;
          const moverEvalAfter = mover === 'w' ? after : -after;   // cp from mover POV

          // ---- classify (chess.com-style tiers) ----
          let q;
          if (stillBook[mover] && i < 16 && loss <= 30) q = 'book';
          else {
            stillBook[mover] = false;
            if (wasBest || loss <= 10) q = 'best';
            else if (loss <= 40) q = 'excellent';
            else if (loss <= 90) q = 'good';
            else if (loss <= 150) q = 'inaccuracy';
            else if (loss <= 300) q = 'mistake';
            else q = 'blunder';
          }
          // ---- brilliant: a best (or near-best) move that sacrifices material
          //      while keeping a clear advantage (not when already totally winning) ----
          if ((wasBest || loss <= 25) && moverEvalAfter >= 30 && moverEvalAfter < 1200 && i + 2 <= line.length) {
            const matBeforeMover = (mover === 'w' ? 1 : -1) * matAt(i);
            const matAfterMover = (mover === 'w' ? 1 : -1) * matAt(Math.min(i + 2, line.length));
            if (matBeforeMover - matAfterMover >= 2) q = 'brilliant';   // gave up a minor+ and stayed good
          }
          node.quality = q;

          node.nags = node.nags.filter(n => n > 6);
          if (q === 'brilliant') node.nags.push(3);
          else if (q === 'blunder') node.nags.push(4);
          else if (q === 'mistake') node.nags.push(2);
          else if (q === 'inaccuracy') node.nags.push(6);
          else if (q === 'best' && wasBest) node.nags.push(1);

          if ((q === 'blunder' || q === 'mistake' || q === 'inaccuracy') && moves[i]?.best && !wasBest) {
            const tmp = new Chess(doc.startFen);
            for (let j = 0; j < i; j++) tmp.makeMove(tmp.findUci(line[j].uci));
            const bm = tmp.findUci(moves[i].best);
            if (bm && !node.comment.includes('was better'))
              node.comment = (node.comment ? node.comment + ' ' : '') + `${tmp.san(bm)} was better (${fmtEval({ cp: evals[i] }, 'pawns')}).`;
          }
        }
        const mean = a => a.length ? a.reduce((x, y) => x + y, 0) / a.length : null;
        const avgW = mean(acc.w), avgB = mean(acc.b);
        const acplW = acplN.w ? acplSum.w / acplN.w : null;
        const acplB = acplN.b ? acplSum.b / acplN.b : null;
        const eloW = eloFromAcpl(acplW), eloB = eloFromAcpl(acplB);
        const count = q => line.filter(n => n.quality === q).length;
        doc.emit('change');
        this.graph.draw();

        const CATS = [
          ['brilliant', 'Brilliant', 'var(--accent-text)'],
          ['best', 'Best', 'var(--ok-text)'],
          ['excellent', 'Excellent', 'var(--ok-text)'],
          ['good', 'Good', 'var(--text-secondary)'],
          ['book', 'Book', 'var(--text-faint)'],
          ['inaccuracy', 'Inaccuracy', 'var(--warn-text)'],
          ['mistake', 'Mistake', 'var(--warn-text)'],
          ['blunder', 'Blunder', 'var(--err-text)'],
        ];
        const catRow = CATS.map(([k, lbl, col]) =>
          h('div.setting-row', { style: { padding: '4px 0' } },
            h('div.sr-label', { style: { color: col } }, lbl),
            h('div.sr-ctl', {}, String(count(k)))));

        showDialog({
          title: 'Game review',
          body: h('div', { style: { minWidth: '360px', display: 'flex', flexDirection: 'column', gap: '10px' } },
            h('div.stat-strip', {},
              h('div.stat-cell', {}, h('div.sc-label', {}, 'White accuracy'), h('div.sc-value', {}, avgW != null ? avgW.toFixed(1) + '%' : '-')),
              h('div.stat-cell', {}, h('div.sc-label', {}, 'Black accuracy'), h('div.sc-value', {}, avgB != null ? avgB.toFixed(1) + '%' : '-'))),
            h('div.stat-strip', {},
              h('div.stat-cell', {}, h('div.sc-label', {}, 'White est. Elo'), h('div.sc-value', {}, eloW != null ? String(eloW) : '-')),
              h('div.stat-cell', {}, h('div.sc-label', {}, 'Black est. Elo'), h('div.sc-value', {}, eloB != null ? String(eloB) : '-'))),
            h('div', { style: { border: '1px solid var(--border)', borderRadius: 'var(--rad-1)', padding: '2px 12px' } }, ...catRow),
            h('p.muted', { style: { fontSize: 'var(--fs-12)' } },
              `Reviewed with ${engines.nameOf(engKey)} at ${perMove} ms/move. Estimated Elo is derived from average centipawn loss (ACPL ${acplW != null ? Math.round(acplW) : '-'}/${acplB != null ? Math.round(acplB) : '-'}) — a rough guide, not an official rating. Moves are classified in the notation; dots on the graph mark mistakes.`)),
        });
      } else {
        toast('Review aborted — engine unavailable', 'err');
      }
    } finally {
      this.reviewing = false;
      this.btnReview.disabled = false;
      this.reviewStatus.textContent = '';
    }
  },

  /* ================= io ================= */
  async loadPgnDialog() {
    const text = await promptDialog({ title: 'Load PGN', label: 'Paste PGN (single game, variations supported)', multiline: true, confirmLabel: 'Load' });
    if (!text) return;
    try {
      const doc = parsePgnGame(text);
      this.openDoc(doc);
      toast('PGN loaded', 'ok');
    } catch (e) { toast('PGN error: ' + e.message, 'err'); }
  },

  async loadFenDialog() {
    const text = await promptDialog({ title: 'Load FEN', label: 'Position (FEN)', placeholder: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' });
    if (!text) return;
    try {
      const probe = new Chess(text.trim());
      if (probe.kingSquare(1) < 0 || probe.kingSquare(-1) < 0) throw new Error('both kings are required');
      const doc = new GameDoc({ fen: probe.fen() });
      this.openDoc(doc, 'Position');
      toast('Position loaded', 'ok');
    } catch (e) { toast('Invalid FEN: ' + e.message, 'err'); }
  },

  async saveToDb() {
    const doc = this.doc();
    if (!doc || !doc.mainline().length) return toast('Nothing to save', 'warn');
    try {
      await gamedb.saveDoc('Local', doc);
      toast('Saved to Local database', 'ok');
    } catch (e) { toast('Save failed: ' + e.message, 'err'); }
  },
};
