/* modes/play.js — Play mode: human vs engine / two players.
   Time controls, increments, premoves, draw offers, resign confirmation,
   takebacks, move confirmation, autosave, post-game review handoff. */
'use strict';

import { h, clear, fmtClockMs } from '../ui/dom.js';
import { icon } from '../ui/icons.js';
import { showDialog, confirmDialog, toast, makePanel, emptyState } from '../ui/widgets.js';
import { Dock, loadLayout, saveLayout } from '../ui/dock.js';
import { GameDoc } from '../core/doc.js';
import { openingName } from '../core/openings.js';
import { settings } from '../svc/settings.js';
import { engines, whiteScore } from '../svc/engines.js';
import { sound } from '../svc/sound.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 { setStatusText, downloadText } from '../ui/appbus.js';
import { builtinAI } from '../svc/localai.js';

const RUSH_LEVELS = [
  { name: 'Beginner', depth: 1, aiDepth: 1, aiMs: 300 }, { name: 'Casual', depth: 3, aiDepth: 2, aiMs: 500 },
  { name: 'Novice', depth: 5, aiDepth: 2, aiMs: 800 }, { name: 'Club', depth: 8, aiDepth: 3, aiMs: 1200 },
  { name: 'Strong', depth: 10, aiDepth: 3, aiMs: 1600 }, { name: 'Expert', movetime: 1200, aiDepth: 4, aiMs: 2000 },
  { name: 'Master', movetime: 3000, aiDepth: 4, aiMs: 2600 }, { name: 'Maximum', movetime: 6000, aiDepth: 5, aiMs: 3200 },
];
const SF_LEVELS = [
  { name: 'Elo 1320', elo: 1320 }, { name: 'Elo 1500', elo: 1500 }, { name: 'Elo 1700', elo: 1700 },
  { name: 'Elo 1900', elo: 1900 }, { name: 'Elo 2100', elo: 2100 }, { name: 'Elo 2400', elo: 2400 },
  { name: 'Elo 2700', elo: 2700 }, { name: 'Full strength', elo: null },
];
const TIME_CONTROLS = [
  { id: '', label: 'Unlimited' }, { id: '1+0', label: 'Bullet 1+0' }, { id: '3+2', label: 'Blitz 3+2' },
  { id: '5+0', label: 'Blitz 5+0' }, { id: '10+0', label: 'Rapid 10+0' }, { id: '15+10', label: 'Rapid 15+10' },
  { id: '30+0', label: 'Classical 30+0' },
];

export const playMode = {
  id: 'play', title: 'Play', icon: 'board', kbd: 'Ctrl+1',

  mount(root, ctx) {
    this.ctx = ctx;
    this.root = root;
    this.cfg = JSON.parse(localStorage.getItem('rc.play.cfg') || 'null') || {
      opponent: 'engine', engineKey: 'rush', level: 3, color: 'w', tc: '10+0',
    };
    this.doc = new GameDoc();
    this.gameActive = false;
    this.thinking = false;
    this.gen = 0;
    this.clock = null;
    this._clockTimer = null;
    this._lowWarned = {};

    // toolbar
    this.toolbar = h('div.toolbar', {},
      h('button.btn.btn-primary.btn-sm', { html: icon('plus') + '<span>New game</span>', onclick: () => this.openSetup() }),
      h('div.toolbar-sep'),
      this.btnDraw = h('button.btn.btn-sm', { html: icon('minus') + '<span>Offer draw</span>', onclick: () => this.offerDraw(), disabled: true }),
      this.btnResign = h('button.btn.btn-sm', { html: icon('flag') + '<span>Resign</span>', onclick: () => this.resign(), disabled: true }),
      this.btnTakeback = h('button.btn.btn-sm', { html: icon('undo') + '<span>Takeback</span>', onclick: () => this.takeback(), disabled: true }),
      h('div.toolbar-sep'),
      h('button.btn.btn-ghost.btn-sm', { html: icon('flip') + '<span>Flip</span>', onclick: () => { this.session.board.flip(); this.session.updateBars(); } }),
      this.btnReview = h('button.btn.btn-sm', { html: icon('microscope') + '<span>Review game</span>', onclick: () => this.toReview(), disabled: true }),
      h('div.toolbar-spacer'),
      this.statusEl = h('span.mode-toolbar-title', {}, 'Set up a new game to begin'),
    );

    // board center
    this.session = new BoardSession({
      doc: this.doc,
      userTurn: () => this.userTurn(),
      onUserMove: uci => this.onUserMove(uci),
      allowPremove: () => this.gameActive && this.cfg.opponent === 'engine',
    });

    // panels
    this.notation = createNotationPanel(this.doc, {});
    const infoBody = h('div', { style: { padding: '10px', display: 'flex', flexDirection: 'column', gap: '6px', fontSize: 'var(--fs-12)' } });
    this.infoBody = infoBody;
    const infoPanel = makePanel({ id: 'gameinfo', title: 'Game', icon: 'info', body: infoBody });

    const defaults = { left: { size: 220, panels: [] }, right: { size: 320, panels: ['notation', 'gameinfo'] }, bottom: { size: 160, panels: [] }, hidden: [] };
    this.dock = new Dock(root, {
      registry: {
        notation: { title: 'Notation', icon: 'list', create: () => this.notation.panel },
        gameinfo: { title: 'Game', icon: 'info', create: () => infoPanel },
      },
      layout: loadLayout('play', defaults), defaults,
      onChange: l => saveLayout('play', l),
    });

    const center = h('div.dock-col', { style: { flex: 1, height: '100%' } }, this.toolbar, this.session.el);
    this.dock.centerEl.append(center);
    this.dock.render();
    this.renderInfo();

    commands.registerAll([
      { id: 'play.new', title: 'New game…', category: 'Play', icon: 'plus', kbd: 'Ctrl+N', run: () => { ctx.switchMode('play'); this.openSetup(); } },
      { id: 'play.resign', title: 'Resign', category: 'Play', icon: 'flag', when: () => this.gameActive, run: () => this.resign() },
      { id: 'play.draw', title: 'Offer draw', category: 'Play', icon: 'minus', when: () => this.gameActive, run: () => this.offerDraw() },
      { id: 'play.takeback', title: 'Takeback', category: 'Play', icon: 'undo', when: () => this.gameActive, run: () => this.takeback() },
    ]);
  },

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

  /* ================= setup ================= */
  openSetup() {
    const c = this.cfg;
    const avail = engines.available();
    const engineOpts = avail.length ? avail : [];
    const levelsFor = k => k === 'stockfish' ? SF_LEVELS : RUSH_LEVELS;

    const oppRow = h('div.radio-row', {},
      h('button.seg', { class: c.opponent === 'engine' ? 'active' : '', onclick: e => setOpp('engine', e) }, 'Engine'),
      h('button.seg', { class: c.opponent === 'human' ? 'active' : '', onclick: e => setOpp('human', e) }, 'Two players'));
    const setOpp = (v, e) => {
      c.opponent = v;
      oppRow.querySelectorAll('.seg').forEach(x => x.classList.toggle('active', x === e.target));
      engineFields.style.display = v === 'engine' ? '' : 'none';
    };

    const engineSel = h('select.select', { onchange: () => { c.engineKey = engineSel.value; paintLevels(); } },
      ...engineOpts.map(a => h('option', { value: a.key, selected: c.engineKey === a.key }, a.name)));
    const levelSel = h('select.select', { onchange: () => { c.level = +levelSel.value; } });
    const paintLevels = () => {
      clear(levelSel);
      levelsFor(engineSel.value || c.engineKey).forEach((l, i) =>
        levelSel.append(h('option', { value: i, selected: c.level === i }, l.name)));
      c.level = Math.min(c.level, 7);
    };
    paintLevels();

    const colorRow = h('div.radio-row', {},
      ...[['w', 'White'], ['b', 'Black'], ['r', 'Random']].map(([v, l]) =>
        h('button.seg', { class: c.color === v ? 'active' : '', onclick: e => {
          c.color = v;
          colorRow.querySelectorAll('.seg').forEach(x => x.classList.toggle('active', x === e.target));
        } }, l)));

    const tcSel = h('select.select', { onchange: () => { c.tc = tcSel.value; } },
      ...TIME_CONTROLS.map(t => h('option', { value: t.id, selected: c.tc === t.id }, t.label)));

    const engineFields = h('div', { style: { display: c.opponent === 'engine' ? '' : 'none', flexDirection: 'column', gap: '10px' } },
      h('div.form-grid', {},
        h('label', {}, 'Engine'), avail.length ? engineSel
          : h('span.faint', { title: 'Start server.py for full-strength engines' }, 'Built-in AI (engines offline)'),
        h('label', {}, 'Strength'), levelSel,
        h('label', {}, 'Your color'), colorRow));

    const dlg = showDialog({
      title: 'New game',
      body: h('div', { style: { display: 'flex', flexDirection: 'column', gap: '12px', minWidth: '360px' } },
        h('div.form-grid', {}, h('label', {}, 'Opponent'), oppRow, h('label', {}, 'Time control'), tcSel),
        engineFields),
      footer: [
        h('button.btn', { onclick: () => dlg.close() }, 'Cancel'),
        h('button.btn.btn-primary', {
          onclick: async () => {
            if (this.gameActive && settings.get('general.confirmNewGame')) {
              const ok = await confirmDialog({ title: 'Abandon game?', message: 'The current game is still in progress. Abandoning counts as unfinished (not saved).', confirmLabel: 'Abandon', danger: true });
              if (!ok) return;
            }
            dlg.close();
            localStorage.setItem('rc.play.cfg', JSON.stringify(c));
            this.startGame();
          },
        }, 'Start game'),
      ],
    });
  },

  startGame(fen) {
    this.gen++;
    const c = this.cfg;
    this.humanColor = c.opponent === 'human' ? null : (c.color === 'r' ? (Math.random() < .5 ? 'w' : 'b') : c.color);
    this.doc = new GameDoc({ fen });
    const engName = engines.available().length ? engines.nameOf(c.engineKey) : 'Built-in AI';
    const lvlName = (c.engineKey === 'stockfish' ? SF_LEVELS : RUSH_LEVELS)[c.level].name;
    if (c.opponent === 'engine') {
      const eng = `${engName} (${lvlName})`;
      this.doc.headers.White = this.humanColor === 'w' ? 'You' : eng;
      this.doc.headers.Black = this.humanColor === 'b' ? 'You' : eng;
      this.doc.headers.Event = 'Casual game vs ' + engName;
    } else {
      this.doc.headers.Event = 'Two-player game';
    }
    this.doc.headers.TimeControl = c.tc || '-';
    this.gameActive = true;
    this.resultInfo = null;
    this._lowWarned = {};
    this.session.bindDoc(this.doc);
    this.notation.bind(this.doc);
    this.session.board.setOrientation(this.humanColor || 'w');
    this.session.setPlayers({
      w: { name: this.doc.headers.White },
      b: { name: this.doc.headers.Black },
    });
    // clock
    if (c.tc) {
      const [base, inc] = c.tc.split('+').map(Number);
      this.clock = { w: base * 60000, b: base * 60000, inc: (inc || 0) * 1000, active: null, last: 0 };
      this.session.setClocks({ w: this.clock.w, b: this.clock.b, active: null });
      this._startClockTimer();
    } else {
      this.clock = null;
      this.session.setClocks(null);
    }
    engines.newgame();
    this.paintState();
    this.session.update(false);
    this.session.board.focus();
    if (this.engineToMove()) this.engineMove();
  },

  setupFromFen(fen) {
    this.cfg.tc = '';
    this.startGame(fen);
  },

  /* ================= game flow ================= */
  userTurn() {
    if (!this.gameActive || !this.doc.isAtTipOfMainline()) return null;
    if (this.thinking) return null;
    if (this.cfg.opponent === 'human') return this.doc.position.turn === 1 ? 'w' : 'b';
    return this.humanColor;
  },

  engineToMove() {
    if (!this.gameActive || this.cfg.opponent !== 'engine') return false;
    const stm = this.doc.position.turn === 1 ? 'w' : 'b';
    return stm !== this.humanColor;
  },

  async onUserMove(uci) {
    if (!this.gameActive) return;
    if (settings.get('input.moveConfirm')) {
      const pos = this.doc.position;
      const m = pos.findUci(uci);
      const ok = await confirmDialog({ title: 'Confirm move', message: `Play ${pos.san(m)}?`, confirmLabel: 'Play' });
      if (!ok) return;
    }
    this.applyMove(uci);
    if (this.gameActive && this.engineToMove()) this.engineMove();
  },

  applyMove(uci) {
    const pos = this.doc.position;
    const m = pos.findUci(uci);
    if (!m) return false;
    this.session.playSoundFor(m);
    const mover = pos.turn === 1 ? 'w' : 'b';
    this.doc.playMove(m);
    if (this.clock) {
      this.clock[mover] += this.clock.inc;
      this.clock.active = mover === 'w' ? 'b' : 'w';
      this.clock.last = Date.now();
      this._syncClocks();
    }
    this.paintState();
    this.checkGameEnd();
    return true;
  },

  async engineMove() {
    if (!this.engineToMove()) return;
    const gen = this.gen;
    this.thinking = true;
    this.paintState();
    const c = this.cfg;
    const lvl = (c.engineKey === 'stockfish' ? SF_LEVELS : RUSH_LEVELS)[c.level];
    const t0 = Date.now();
    let res = null;
    if (engines.available().length) {
      const params = {
        engine: c.engineKey, instance: 'play',
        fen: this.doc.startFen, moves: this.doc.path().map(n => n.uci),
      };
      if (lvl.elo !== undefined && lvl.elo) params.elo = lvl.elo;
      if (this.clock) {
        const side = this.doc.position.turn === 1 ? 'w' : 'b';
        const rem = Math.max(0, this.clock[side]);
        params.movetime = Math.round(Math.max(120, Math.min(rem / 25 + this.clock.inc * 0.7, rem * 0.3, lvl.elo === null ? 5000 : 1500)));
      } else if (lvl.depth) params.depth = lvl.depth;
      else params.movetime = lvl.movetime || 1000;
      res = await engines.go(params);
    } else {
      // offline: built-in AI keeps Play mode working
      const aiLvl = RUSH_LEVELS[Math.min(c.level, RUSH_LEVELS.length - 1)];
      await new Promise(r => setTimeout(r, 30));   // let the UI paint
      const ai = builtinAI(this.doc.position.fen(), aiLvl.aiDepth, aiLvl.aiMs);
      if (ai) res = { bestmove: ai.uci };
    }
    if (gen !== this.gen) return;
    this.thinking = false;
    if (!res || !res.bestmove) {
      this.paintState();
      if (this.gameActive) toast('Engine unavailable — try again or start server.py', 'err');
      return;
    }
    // eval bar for free: reuse the score from the engine's own search
    if (res.scoreCp != null || res.scoreMate != null) {
      const stm = this.doc.position.turn === 1 ? 'w' : 'b';
      this.session.setEval(whiteScore(res, stm));
    }
    const minDelay = settings.get('engine.minMoveMs');
    const wait = Math.max(0, minDelay - (Date.now() - t0));
    setTimeout(() => {
      if (gen !== this.gen || !this.gameActive) return;
      this.applyMove(res.bestmove);
      // consume premove
      if (this.gameActive && !this.engineToMove()) {
        const pm = this.session.takePremove();
        if (pm) {
          this.applyMove(pm);
          if (this.gameActive && this.engineToMove()) this.engineMove();
        }
      }
    }, wait);
  },

  checkGameEnd() {
    const r = this.doc.result();
    if (r) this.endGame(r.result, r.reason);
  },

  async endGame(result, reason) {
    if (!this.gameActive) return;
    this.gameActive = false;
    this.gen++;
    this.thinking = false;
    if (this.clock) { this.clock.active = null; this._syncClocks(); }
    this._stopClockTimer();
    this.doc.headers.Result = result;
    this.doc.headers.Termination = reason;
    sound.end();
    this.paintState();
    this.resultInfo = { result, reason };
    if (settings.get('general.autosaveGames') && this.doc.mainline().length > 3) {
      try { await gamedb.saveDoc('Local', this.doc); }
      catch (e) { console.error(e); toast('Autosave failed: ' + e.message, 'err'); }
    }
    const draw = result === '1/2-1/2';
    const winnerName = result === '1-0' ? this.doc.headers.White : this.doc.headers.Black;
    const dlg = showDialog({
      title: draw ? 'Draw' : `${winnerName} wins`,
      body: h('div', { style: { minWidth: '280px' } },
        h('p', {}, 'By ' + reason + '.'),
        h('p.muted', { style: { marginTop: '6px', fontSize: 'var(--fs-12)' } },
          settings.get('general.autosaveGames') ? 'Game saved to the Local database.' : '')),
      footer: [
        h('button.btn', { onclick: () => { dlg.close(); } }, 'Close'),
        h('button.btn', { onclick: () => { dlg.close(); this.startGame(); } }, 'Rematch'),
        h('button.btn.btn-primary', { onclick: () => { dlg.close(); this.toReview(); } }, 'Review game'),
      ],
    });
  },

  async resign() {
    if (!this.gameActive) return;
    if (settings.get('general.confirmResign')) {
      const ok = await confirmDialog({ title: 'Resign', message: 'Resign this game?', confirmLabel: 'Resign', danger: true });
      if (!ok) return;
    }
    const loser = this.cfg.opponent === 'human' ? (this.doc.position.turn === 1 ? 'w' : 'b') : this.humanColor;
    this.endGame(loser === 'w' ? '0-1' : '1-0', 'resignation');
  },

  async offerDraw() {
    if (!this.gameActive) return;
    if (this.cfg.opponent === 'human') {
      const ok = await confirmDialog({ title: 'Draw offer', message: 'Does the opponent accept the draw?', confirmLabel: 'Accept draw' });
      if (ok) this.endGame('1/2-1/2', 'mutual agreement');
      return;
    }
    // engine decides: accepts when the position is balanced and enough moves were played
    this.btnDraw.disabled = true;
    const res = await engines.go({
      engine: this.cfg.engineKey, instance: 'play',
      fen: this.doc.startFen, moves: this.doc.path().map(n => n.uci), movetime: 400,
    });
    this.btnDraw.disabled = false;
    const stm = this.doc.position.turn === 1 ? 'w' : 'b';
    const engineSide = this.humanColor === 'w' ? 'b' : 'w';
    let cpEngine = null;
    if (res && res.scoreCp != null) cpEngine = (stm === engineSide ? 1 : -1) * res.scoreCp;
    if (res && res.scoreMate == null && cpEngine != null && cpEngine < 60 && this.doc.ply() >= 40) {
      this.endGame('1/2-1/2', 'mutual agreement');
    } else {
      toast(engines.nameOf(this.cfg.engineKey) + ' declines the draw offer', 'info');
    }
  },

  takeback() {
    if (!this.gameActive || !this.doc.mainline().length) return;
    this.gen++;
    this.thinking = false;
    this.session.clearPremove();
    const tip = this.doc.tip();
    this.doc.goTo(tip);
    // vs engine: undo back to the user's turn (2 plies if needed)
    let undo = 1;
    if (this.cfg.opponent === 'engine') {
      const stm = this.doc.position.turn === 1 ? 'w' : 'b';
      undo = stm === this.humanColor ? 2 : 1;
    }
    for (let i = 0; i < undo && this.doc.current !== this.doc.root; i++)
      this.doc.deleteNode(this.doc.current);
    if (this.clock) { this.clock.active = this.doc.position.turn === 1 ? 'w' : 'b'; this.clock.last = Date.now(); }
    this.paintState();
    if (this.engineToMove()) this.engineMove();
  },

  toReview() {
    this.ctx.openInAnalysis(this.doc);
  },

  /* ================= clock ================= */
  _startClockTimer() {
    this._stopClockTimer();
    this._clockTimer = setInterval(() => {
      if (!this.gameActive || !this.clock || !this.clock.active) return;
      const now = Date.now();
      const side = this.clock.active;
      this.clock[side] -= now - (this.clock.last || now);
      this.clock.last = now;
      if (this.clock[side] < 30000 && !this._lowWarned[side] && side === this.humanColor) {
        this._lowWarned[side] = true;
        sound.lowTime();
      }
      if (this.clock[side] <= 0) {
        this.clock[side] = 0;
        this._syncClocks();
        this.endGame(side === 'w' ? '0-1' : '1-0', 'time forfeit');
        return;
      }
      this._syncClocks();
    }, 100);
  },
  _stopClockTimer() { if (this._clockTimer) { clearInterval(this._clockTimer); this._clockTimer = null; } },
  _syncClocks() {
    if (!this.clock) return;
    this.session.setClocks({ w: this.clock.w, b: this.clock.b, active: this.gameActive ? this.clock.active : null });
  },

  /* ================= ui state ================= */
  paintState() {
    const active = this.gameActive;
    this.btnDraw.disabled = !active;
    this.btnResign.disabled = !active;
    this.btnTakeback.disabled = !active || !this.doc.mainline().length;
    this.btnReview.disabled = active || !this.doc.mainline().length;
    const pos = this.doc.position;
    const stm = pos.turn === 1 ? 'w' : 'b';
    let txt;
    if (!active && this.resultInfo) txt = `${this.resultInfo.result} - ${this.resultInfo.reason}`;
    else if (!active) txt = 'Set up a new game to begin';
    else if (this.thinking) txt = engines.nameOf(this.cfg.engineKey) + ' is thinking…';
    else txt = (stm === 'w' ? 'White' : 'Black') + ' to move' + (pos.inCheck(pos.turn) ? ' — check' : '');
    this.statusEl.textContent = txt;
    if (settings.get('general.openingNames')) {
      const name = openingName(this.doc.mainline().map(n => n.san));
      setStatusText(name || '');
    }
    this.renderInfo();
    // start clock on first move
    if (this.clock && active) {
      const played = this.doc.mainline().length;
      this.clock.active = played >= 1 ? (this.doc.position.turn === 1 ? 'w' : 'b') : null;
      if (!this.clock.last) this.clock.last = Date.now();
    }
  },

  renderInfo() {
    clear(this.infoBody);
    const hdr = this.doc.headers;
    const rows = [
      ['White', hdr.White], ['Black', hdr.Black],
      ['Event', hdr.Event], ['Time control', hdr.TimeControl || '-'],
      ['Result', hdr.Result === '*' ? 'in progress' : hdr.Result + (hdr.Termination ? ` (${hdr.Termination})` : '')],
    ];
    const name = openingName(this.doc.mainline().map(n => n.san));
    if (name) rows.push(['Opening', name]);
    for (const [k, v] of rows)
      this.infoBody.append(h('div', { style: { display: 'flex', gap: '8px' } },
        h('span.faint', { style: { width: '86px', flex: 'none' } }, k),
        h('span.selectable.grow', {}, v || '-')));
  },
};
