/* modes/match.js — Engine match: A vs B over N games with opening book,
   color alternation, adjudication, live board, Elo estimate, PGN export,
   crash detection, resume, and per-game result table. */
'use strict';

import { h, clear } from '../ui/dom.js';
import { icon } from '../ui/icons.js';
import { showDialog, confirmDialog, toast, makePanel, emptyState } from '../ui/widgets.js';
import { GameDoc, parsePgnGame } from '../core/doc.js';
import { engines } from '../svc/engines.js';
import { runEngineGame, eloEstimate, MINI_BOOK } from '../svc/enginegame.js';
import { gamedb } from '../svc/gamedb.js';
import { idb } from '../svc/idb.js';
import { commands } from '../svc/commands.js';
import { BoardSession } from '../panels/session.js';
import { createNotationPanel } from '../panels/notation.js';
import { downloadText, setStatusText } from '../ui/appbus.js';

const LEVELS = {
  rush: [{ label: 'depth 4', depth: 4 }, { label: 'depth 6', depth: 6 }, { label: 'depth 8', depth: 8 }, { label: '300 ms', movetime: 300 }, { label: '800 ms', movetime: 800 }],
  stockfish: [{ label: 'Elo 1500', elo: 1500, movetime: 400 }, { label: 'Elo 1900', elo: 1900, movetime: 400 }, { label: 'Elo 2400', elo: 2400, movetime: 400 }, { label: 'Elo 2700', elo: 2700, movetime: 400 }, { label: 'full, 300 ms', movetime: 300 }, { label: 'full, 800 ms', movetime: 800 }],
};

export const matchMode = {
  id: 'match', title: 'Engine match', icon: 'swords', kbd: 'Ctrl+7',

  mount(root, ctx) {
    this.ctx = ctx;
    this.running = false;
    this.match = null;          // {cfg, games:[{no, white, black, result, reason, pgn}], stopRequested}
    this.doc = new GameDoc();

    this.setupEl = h('div', { style: { flex: 1, overflowY: 'auto', padding: '16px', maxWidth: '640px', margin: '0 auto', width: '100%' } });
    this.liveEl = h('div', { style: { flex: 1, display: 'none', minHeight: 0 } });
    root.append(h('div', { style: { display: 'flex', flexDirection: 'column', height: '100%' } }, this.setupEl, this.liveEl));

    commands.register({ id: 'match.new', title: 'New engine match', category: 'Match', icon: 'swords', run: () => { ctx.switchMode('match'); this.showSetup(); } });

    this.restoreOrSetup();
  },

  activate() { this.session?.update(false); },
  deactivate() {},
  getDoc() { return this.doc; },
  getSession() { return this.session || null; },

  async restoreOrSetup() {
    const saved = await idb.get('matches', 1);
    if (saved && saved.games?.length && saved.games.length < saved.cfg.games) {
      this.match = saved;
      this.showSetup(true);
    } else {
      this.match = saved || null;
      this.showSetup();
    }
  },

  showSetup(resumable = false) {
    this.liveEl.style.display = 'none';
    this.setupEl.style.display = '';
    clear(this.setupEl);
    const avail = engines.available();
    if (avail.length === 0) {
      this.setupEl.append(emptyState({ icon: 'cpu', title: 'Engines offline', hint: 'Engine matches need the engine server. Run "python server.py".' }));
      return;
    }
    const cfg = this.match?.cfg || {
      a: { engine: 'rush', level: 4 }, b: { engine: avail.some(e => e.key === 'stockfish') ? 'stockfish' : 'rush', level: 0 },
      games: 10, book: 'mini', maxPlies: 240,
    };

    const engineRow = side => {
      const eSel = h('select.select', { onchange: () => { cfg[side].engine = eSel.value; paintLv(); } },
        ...avail.map(a => h('option', { value: a.key, selected: cfg[side].engine === a.key }, a.name)));
      const lSel = h('select.select', { onchange: () => { cfg[side].level = +lSel.value; } });
      const paintLv = () => {
        clear(lSel);
        (LEVELS[eSel.value] || LEVELS.rush).forEach((l, i) =>
          lSel.append(h('option', { value: i, selected: cfg[side].level === i }, l.label)));
        cfg[side].level = Math.min(cfg[side].level, (LEVELS[eSel.value] || LEVELS.rush).length - 1);
      };
      paintLv();
      return h('div.side-by-side', {}, eSel, lSel);
    };

    const gamesIn = h('input.input', { type: 'number', value: cfg.games, min: 2, max: 200, style: { width: '80px' }, onchange: () => cfg.games = Math.max(2, +gamesIn.value || 10) });
    const bookSel = h('select.select', { onchange: () => cfg.book = bookSel.value },
      h('option', { value: 'mini', selected: cfg.book === 'mini' }, 'Built-in 10-opening book'),
      h('option', { value: 'none', selected: cfg.book === 'none' }, 'No book (startpos)'));

    this.setupEl.append(
      h('div', { style: { fontSize: 'var(--fs-16)', fontWeight: 700, marginBottom: '12px' } }, 'Engine match'),
      h('div', { style: { border: '1px solid var(--border)', background: 'var(--bg-panel)', padding: '14px', display: 'flex', flexDirection: 'column', gap: '12px' } },
        h('div.field', {}, h('label', {}, 'Engine A'), engineRow('a')),
        h('div.field', {}, h('label', {}, 'Engine B'), engineRow('b')),
        h('div.form-grid', {},
          h('label', {}, 'Games (colors alternate)'), gamesIn,
          h('label', {}, 'Opening book'), bookSel),
        h('div.sr-hint', {}, 'Adjudication: win at >= 8.00 for 4 evals, draw at <= 0.15 for 8 evals after move 30, 240-move limit. Games run sequentially (each engine is a single process). Results autosave; a match can be resumed after closing.'),
        h('div', { style: { display: 'flex', gap: '8px' } },
          h('button.btn.btn-primary', { html: icon('play') + '<span>Start match</span>', onclick: () => this.startMatch(cfg) }),
          resumable ? h('button.btn', { html: icon('refresh') + `<span>Resume (${this.match.games.length}/${this.match.cfg.games})</span>`, onclick: () => this.resumeMatch() }) : null)),
    );
    if (this.match?.games?.length) {
      const summary = h('div', { style: { marginTop: '14px' } });
      const { w, d, l } = this.scoreOfA(this.match);
      const { elo, err } = eloEstimate(w, d, l);
      summary.append(h('div.stat-strip', {},
        h('div.stat-cell', {}, h('div.sc-label', {}, 'Last match (A)'), h('div.sc-value', {}, `+${w} =${d} -${l}`)),
        h('div.stat-cell', {}, h('div.sc-label', {}, 'Elo (A-B)'), h('div.sc-value', {}, `${elo >= 0 ? '+' : ''}${elo} ± ${err || '?'}`))));
      this.setupEl.append(summary);
    }
  },

  buildLive() {
    clear(this.liveEl);
    this.liveEl.style.display = 'flex';
    this.setupEl.style.display = 'none';
    this.doc = new GameDoc();
    this.session = new BoardSession({ doc: this.doc, userTurn: () => null, evalBar: true });
    this.notation = createNotationPanel(this.doc, {});
    this.progressEl = h('div', { style: { padding: '8px 10px', fontSize: 'var(--fs-12)', borderBottom: '1px solid var(--border)' } });
    this.tableEl = h('div', { style: { flex: 1, overflowY: 'auto' } });
    const side = h('div.dock-side', { style: { width: '340px', borderLeft: '1px solid var(--border)' } },
      makePanel({ id: 'mprog', title: 'Match', icon: 'swords', body: h('div', { style: { display: 'flex', flexDirection: 'column', height: '100%' } }, this.progressEl, this.tableEl), collapsible: false }).el,
      this.notation.panel.el);
    const toolbar = h('div.toolbar', {},
      this.btnStop = h('button.btn.btn-danger.btn-sm', { html: icon('stop') + '<span>Stop match</span>', onclick: () => { if (this.match) this.match.stopRequested = true; toast('Stopping after this game…', 'info'); } }),
      h('div.toolbar-sep'),
      h('button.btn.btn-ghost.btn-sm', { html: icon('download') + '<span>Export PGN</span>', onclick: () => this.exportPgn() }),
      h('button.btn.btn-ghost.btn-sm', { html: icon('database') + '<span>Save to DB</span>', onclick: () => this.saveAllToDb() }),
      h('div.toolbar-spacer'),
      this.liveStatus = h('span.mode-toolbar-title'));
    this.liveEl.append(
      h('div.dock-center', {}, h('div.dock-col', { style: { flex: 1, height: '100%' } }, toolbar, this.session.el)),
      side);
  },

  async startMatch(cfg) {
    this.match = { id: 1, cfg, games: [], stopRequested: false };
    await idb.put('matches', this.match);
    this.runMatch();
  },
  async resumeMatch() {
    this.match.stopRequested = false;
    this.runMatch();
  },

  async runMatch() {
    if (this.running) return;
    this.running = true;
    const m = this.match;
    this.buildLive();
    try {
      while (m.games.length < m.cfg.games && !m.stopRequested) {
        const no = m.games.length + 1;
        const aIsWhite = no % 2 === 1;
        const la = LEVELS[m.cfg.a.engine][m.cfg.a.level];
        const lb = LEVELS[m.cfg.b.engine][m.cfg.b.level];
        const white = aIsWhite ? { ...la, engine: m.cfg.a.engine, instance: 'mA' } : { ...lb, engine: m.cfg.b.engine, instance: 'mB' };
        const black = aIsWhite ? { ...lb, engine: m.cfg.b.engine, instance: 'mB' } : { ...la, engine: m.cfg.a.engine, instance: 'mA' };
        const opening = m.cfg.book === 'mini' ? MINI_BOOK[Math.floor((no - 1) / 2) % MINI_BOOK.length] : null;
        this.liveStatus.textContent = `Game ${no}/${m.cfg.games}` + (opening ? ` · ${opening.name}` : '');
        this.paintProgress();

        const out = await runEngineGame({
          white, black,
          openingUcis: opening?.ucis,
          maxPlies: m.cfg.maxPlies,
          adjDrawAfter: 60,
          onProgress: (doc, res) => {
            if (this.docBound !== doc) {
              this.docBound = doc;
              this.doc = doc;
              this.session.bindDoc(doc);
              this.notation.bind(doc);
              this.session.setPlayers({ w: { name: doc.headers.White }, b: { name: doc.headers.Black } });
            }
            this.session.update(false);
            const cur = doc.current;
            if (cur.evalCp != null || cur.evalMate != null)
              this.session.setEval(cur.evalMate != null ? { mate: cur.evalMate } : { cp: cur.evalCp });
          },
          shouldStop: () => m.stopRequested,
        });
        if (out.reason === 'stopped') break;
        out.doc.headers.Event = `Engine match: ${engines.nameOf(m.cfg.a.engine)} vs ${engines.nameOf(m.cfg.b.engine)}`;
        out.doc.headers.Round = String(no);
        if (opening) out.doc.headers.Opening = opening.name;
        m.games.push({
          no, white: out.doc.headers.White, black: out.doc.headers.Black,
          aWhite: aIsWhite, result: out.result, reason: out.reason,
          crashed: out.crashed, pgn: out.doc.pgn(),
        });
        await idb.put('matches', m);
        this.paintProgress();
        if (out.crashed) toast(`Game ${no}: engine failure — adjudicated`, 'warn');
      }
    } finally {
      this.running = false;
      this.liveStatus.textContent = m.stopRequested ? 'Match stopped' : 'Match finished';
      this.paintProgress(true);
      setStatusText('');
    }
  },

  scoreOfA(m) {
    let w = 0, d = 0, l = 0;
    for (const g of m.games) {
      const aWon = (g.result === '1-0') === g.aWhite;
      if (g.result === '1/2-1/2') d++;
      else if (aWon && g.result !== '*') w++;
      else if (g.result !== '*') l++;
    }
    return { w, d, l };
  },

  paintProgress(final = false) {
    const m = this.match;
    if (!m || !this.progressEl) return;
    const { w, d, l } = this.scoreOfA(m);
    const { elo, err } = eloEstimate(w, d, l);
    clear(this.progressEl);
    this.progressEl.append(
      h('div', { style: { fontWeight: 600 } }, `${engines.nameOf(m.cfg.a.engine)} (A) vs ${engines.nameOf(m.cfg.b.engine)} (B)`),
      h('div.tnum', { style: { marginTop: '4px' } }, `A: +${w} =${d} -${l}  (${m.games.length}/${m.cfg.games} games)`),
      h('div.tnum.muted', {}, m.games.length >= 2 ? `Elo(A-B): ${elo >= 0 ? '+' : ''}${elo} ± ${err || '?'}` : 'Elo estimate after 2+ games'),
    );
    clear(this.tableEl);
    for (const g of [...m.games].reverse()) {
      this.tableEl.append(h('div.list-row', {
        onclick: () => this.openGame(g),
        title: 'Click to load on the board',
      },
        h('span.faint.tnum', { style: { width: '26px' } }, '#' + g.no),
        h('span.grow', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, `${g.white} - ${g.black}`),
        g.crashed ? h('span', { html: icon('warning', 'ic-sm'), style: { color: 'var(--warn-text)' }, title: 'engine failure' }) : null,
        h('span.mono', {}, g.result)));
    }
    if (final && m.games.length) this.showSummary();
  },

  openGame(g) {
    try {
      const doc = parsePgnGame(g.pgn);
      this.doc = doc;
      this.docBound = doc;
      this.session.bindDoc(doc);
      this.notation.bind(doc);
      this.session.setPlayers({ w: { name: doc.headers.White }, b: { name: doc.headers.Black } });
      doc.goEnd();
    } catch (e) { toast('Could not load game: ' + e.message, 'err'); }
  },

  showSummary() {
    const m = this.match;
    const { w, d, l } = this.scoreOfA(m);
    const { elo, err, score } = eloEstimate(w, d, l);
    const crashes = m.games.filter(g => g.crashed).length;
    showDialog({
      title: 'Match result',
      body: h('div', { style: { minWidth: '340px', display: 'flex', flexDirection: 'column', gap: '10px' } },
        h('div.stat-strip', {},
          h('div.stat-cell', {}, h('div.sc-label', {}, 'A wins'), h('div.sc-value', {}, String(w))),
          h('div.stat-cell', {}, h('div.sc-label', {}, 'Draws'), h('div.sc-value', {}, String(d))),
          h('div.stat-cell', {}, h('div.sc-label', {}, 'B wins'), h('div.sc-value', {}, String(l)))),
        h('div.stat-strip', {},
          h('div.stat-cell', {}, h('div.sc-label', {}, 'Score (A)'), h('div.sc-value', {}, (score * 100).toFixed(1) + '%')),
          h('div.stat-cell', {}, h('div.sc-label', {}, 'Elo (A-B)'), h('div.sc-value', {}, `${elo >= 0 ? '+' : ''}${elo} ± ${err || '?'}`)),
          h('div.stat-cell', {}, h('div.sc-label', {}, 'Crashes'), h('div.sc-value', {}, String(crashes)))),
        h('div', { style: { display: 'flex', gap: '8px' } },
          h('button.btn', { onclick: () => this.exportPgn() }, 'Export PGN'),
          h('button.btn', { onclick: () => this.saveAllToDb() }, 'Save games to DB'),
          h('button.btn', { onclick: e => { e.target.closest('.veil').remove(); this.showSetup(); } }, 'New match'))),
    });
  },

  exportPgn() {
    const m = this.match;
    if (!m?.games?.length) return;
    downloadText('engine_match.pgn', m.games.map(g => g.pgn.trim()).join('\n\n') + '\n');
    toast('Match PGN exported', 'ok');
  },

  async saveAllToDb() {
    const m = this.match;
    if (!m?.games?.length) return;
    const text = m.games.map(g => g.pgn.trim()).join('\n\n');
    const res = await gamedb.importPgn('Engine matches', text, { skipDups: false });
    toast(`Saved ${res.added} games to "Engine matches"`, 'ok');
  },
};
