/* modes/tournament.js — Tournaments: round-robin (single/double), Swiss,
   knockout. Engine participants play automatically; human games get
   result entry. Standings with Buchholz / Sonneborn-Berger, crosstable,
   round management, PGN export, resume. */
'use strict';

import { h, clear } from '../ui/dom.js';
import { icon } from '../ui/icons.js';
import { showDialog, promptDialog, confirmDialog, toast, makePanel, emptyState, showMenu } from '../ui/widgets.js';
import { GameDoc, parsePgnGame } from '../core/doc.js';
import { engines } from '../svc/engines.js';
import { runEngineGame, 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 { downloadText } from '../ui/appbus.js';

const ENGINE_PRESETS = [
  { label: 'RushChess d4', engine: 'rush', depth: 4 },
  { label: 'RushChess d6', engine: 'rush', depth: 6 },
  { label: 'RushChess d8', engine: 'rush', depth: 8 },
  { label: 'RushChess 500ms', engine: 'rush', movetime: 500 },
  { label: 'Stockfish 1500', engine: 'stockfish', elo: 1500, movetime: 300 },
  { label: 'Stockfish 1900', engine: 'stockfish', elo: 1900, movetime: 300 },
  { label: 'Stockfish 2400', engine: 'stockfish', elo: 2400, movetime: 300 },
  { label: 'Stockfish full 300ms', engine: 'stockfish', movetime: 300 },
];

export const tournamentMode = {
  id: 'tournament', title: 'Tournament', icon: 'trophy', kbd: 'Ctrl+6',

  mount(root, ctx) {
    this.ctx = ctx;
    this.t = null;               // active tournament
    this.running = false;
    this.listEl = h('div', { style: { flex: 1, overflowY: 'auto', padding: '16px', maxWidth: '860px', margin: '0 auto', width: '100%' } });
    this.viewEl = h('div', { style: { flex: 1, display: 'none', minHeight: 0 } });
    root.append(h('div', { style: { display: 'flex', flexDirection: 'column', height: '100%' } }, this.listEl, this.viewEl));
    commands.register({ id: 'tournament.new', title: 'New tournament', category: 'Tournament', icon: 'trophy', run: () => { ctx.switchMode('tournament'); this.newDialog(); } });
    this.showList();
  },

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

  /* ================= list ================= */
  async showList() {
    this.viewEl.style.display = 'none';
    this.listEl.style.display = '';
    clear(this.listEl);
    const all = await idb.getAll('tournaments');
    this.listEl.append(
      h('div', { style: { display: 'flex', alignItems: 'center', marginBottom: '12px' } },
        h('span', { style: { fontSize: 'var(--fs-16)', fontWeight: 700 } }, 'Tournaments'),
        h('span.grow'),
        h('button.btn.btn-primary', { html: icon('plus') + '<span>New tournament</span>', onclick: () => this.newDialog() })));
    if (!all.length) {
      this.listEl.append(emptyState({
        icon: 'trophy', title: 'No tournaments yet',
        hint: 'Create a round-robin, Swiss or knockout event with engine or human participants.',
      }));
      return;
    }
    for (const t of all.sort((a, b) => b.created - a.created)) {
      const played = t.games.filter(g => g.result !== '*').length;
      this.listEl.append(h('div.list-row', { style: { height: '34px' }, onclick: () => this.open(t) },
        h('span', { html: icon('trophy', 'ic-sm'), style: { color: 'var(--warn-text)' } }),
        h('span.grow', { style: { fontWeight: 600 } }, t.name),
        h('span.muted', {}, `${t.format} · ${t.players.length} players`),
        h('span.faint.tnum', {}, `${played}/${t.games.length} games`),
        h('button.icon-btn', { html: icon('trash'), title: 'Delete', onclick: async e => {
          e.stopPropagation();
          if (await confirmDialog({ title: 'Delete tournament', message: `Delete "${t.name}"?`, confirmLabel: 'Delete', danger: true }))
            { await idb.del('tournaments', t.id); this.showList(); }
        } })));
    }
  },

  /* ================= creation ================= */
  newDialog() {
    const players = [];
    const avail = engines.available();
    const nameIn = h('input.input', { placeholder: 'e.g. Club championship' });
    const formatSel = h('select.select', {},
      h('option', { value: 'round-robin' }, 'Round robin'),
      h('option', { value: 'double-round-robin' }, 'Double round robin'),
      h('option', { value: 'swiss' }, 'Swiss'),
      h('option', { value: 'knockout' }, 'Knockout'));
    const roundsIn = h('input.input', { type: 'number', value: 5, min: 1, max: 15, style: { width: '70px' } });
    const listEl = h('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px', maxHeight: '180px', overflowY: 'auto' } });
    const paintPlayers = () => {
      clear(listEl);
      players.forEach((p, i) => listEl.append(h('div.list-row', { style: { cursor: 'default' } },
        h('span', { html: icon(p.type === 'engine' ? 'cpu' : 'users', 'ic-sm') }),
        h('span.grow', {}, p.label),
        h('button.icon-btn', { html: icon('close'), onclick: () => { players.splice(i, 1); paintPlayers(); } }))));
      if (!players.length) listEl.append(h('div.faint', { style: { padding: '6px' } }, 'Add at least 2 participants'));
    };
    paintPlayers();

    const addEngineBtn = h('button.btn.btn-sm', {
      html: icon('cpu') + '<span>Add engine</span>',
      disabled: !avail.length,
      title: avail.length ? '' : 'Engines offline',
      onclick: e => {
        const presets = ENGINE_PRESETS.filter(p => avail.some(a => a.key === p.engine));
        // custom engines dropped into gui/engine/ get generic time presets
        for (const a of avail) {
          if (ENGINE_PRESETS.some(p => p.engine === a.key)) continue;
          presets.push({ label: `${a.name} 300ms`, engine: a.key, movetime: 300 });
          presets.push({ label: `${a.name} 800ms`, engine: a.key, movetime: 800 });
        }
        showMenu(presets.map(p => ({
          label: p.label,
          action: () => { players.push({ type: 'engine', ...p }); paintPlayers(); },
        })), e.clientX, e.clientY);
      },
    });
    const addHumanBtn = h('button.btn.btn-sm', {
      html: icon('users') + '<span>Add player</span>',
      onclick: async () => {
        const name = await promptDialog({ title: 'Add player', label: 'Player name' });
        if (name && name.trim()) { players.push({ type: 'human', label: name.trim() }); paintPlayers(); }
      },
    });

    const dlg = showDialog({
      title: 'New tournament',
      body: h('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px', minWidth: '420px' } },
        h('div.field', {}, h('label', {}, 'Name'), nameIn),
        h('div.form-grid', {},
          h('label', {}, 'Format'), formatSel,
          h('label', {}, 'Swiss rounds'), roundsIn),
        h('div.field', {}, h('label', {}, 'Participants'), listEl),
        h('div', { style: { display: 'flex', gap: '8px' } }, addEngineBtn, addHumanBtn)),
      footer: [
        h('button.btn', { onclick: () => dlg.close() }, 'Cancel'),
        h('button.btn.btn-primary', { onclick: async () => {
          if (players.length < 2) return toast('Add at least 2 participants', 'warn');
          if (!nameIn.value.trim()) return toast('Give the tournament a name', 'warn');
          dlg.close();
          const t = {
            name: nameIn.value.trim(), format: formatSel.value,
            swissRounds: Math.max(1, +roundsIn.value || 5),
            players: players.map((p, i) => ({ ...p, id: i })),
            games: [], round: 1, created: Date.now(),
          };
          this.generateGames(t);
          t.id = await idb.add('tournaments', t);
          await idb.put('tournaments', t);
          this.open(t);
        } }, 'Create'),
      ],
    });
  },

  /* ================= scheduling ================= */
  generateGames(t) {
    if (t.format === 'round-robin' || t.format === 'double-round-robin') {
      const ids = t.players.map(p => p.id);
      if (ids.length % 2) ids.push(-1);        // bye
      const rounds = ids.length - 1;
      let no = 1;
      for (let r = 0; r < rounds; r++) {
        for (let i = 0; i < ids.length / 2; i++) {
          const a = ids[i], b = ids[ids.length - 1 - i];
          if (a === -1 || b === -1) continue;
          const white = r % 2 === 0 ? a : b, black = r % 2 === 0 ? b : a;
          t.games.push({ no: no++, round: r + 1, white, black, result: '*', reason: '', pgn: null });
        }
        ids.splice(1, 0, ids.pop());
      }
      if (t.format === 'double-round-robin') {
        const first = [...t.games];
        for (const g of first)
          t.games.push({ no: no++, round: g.round + rounds, white: g.black, black: g.white, result: '*', reason: '', pgn: null });
      }
    } else if (t.format === 'swiss') {
      this.pairSwissRound(t);                   // first round; later rounds paired on demand
    } else if (t.format === 'knockout') {
      const ids = t.players.map(p => p.id);
      let no = 1;
      for (let i = 0; i + 1 < ids.length; i += 2)
        t.games.push({ no: no++, round: 1, white: ids[i], black: ids[i + 1], result: '*', reason: '', pgn: null });
      if (ids.length % 2) t.byes = [ids[ids.length - 1]]; else t.byes = [];
    }
  },

  pairSwissRound(t) {
    const round = t.games.length ? Math.max(...t.games.map(g => g.round)) + 1 : 1;
    if (round > t.swissRounds) return false;
    const scores = this.scores(t);
    const sorted = [...t.players].sort((a, b) => (scores[b.id]?.pts || 0) - (scores[a.id]?.pts || 0));
    const played = new Set(t.games.map(g => [g.white, g.black].sort().join(':')));
    const used = new Set();
    let no = t.games.length + 1;
    for (const p of sorted) {
      if (used.has(p.id)) continue;
      const opp = sorted.find(q => q.id !== p.id && !used.has(q.id) && !played.has([p.id, q.id].sort().join(':')))
        || sorted.find(q => q.id !== p.id && !used.has(q.id));
      if (!opp) continue;
      used.add(p.id); used.add(opp.id);
      const whiteFirst = Math.random() < 0.5;
      t.games.push({ no: no++, round, white: whiteFirst ? p.id : opp.id, black: whiteFirst ? opp.id : p.id, result: '*', reason: '', pgn: null });
    }
    return true;
  },

  advanceKnockout(t) {
    const round = Math.max(...t.games.map(g => g.round));
    const roundGames = t.games.filter(g => g.round === round);
    if (roundGames.some(g => g.result === '*')) return false;
    const winners = roundGames.map(g => g.result === '1-0' ? g.white : g.result === '0-1' ? g.black : (Math.random() < .5 ? g.white : g.black));
    const pool = [...(t.byes || []), ...winners];
    t.byes = [];
    if (pool.length < 2) return false;
    let no = t.games.length + 1;
    for (let i = 0; i + 1 < pool.length; i += 2)
      t.games.push({ no: no++, round: round + 1, white: pool[i], black: pool[i + 1], result: '*', reason: '', pgn: null });
    if (pool.length % 2) t.byes = [pool[pool.length - 1]];
    return true;
  },

  /* ================= view ================= */
  open(t) {
    this.t = t;
    this.listEl.style.display = 'none';
    this.viewEl.style.display = 'flex';
    clear(this.viewEl);

    this.liveDoc = new GameDoc();
    this.session = new BoardSession({ doc: this.liveDoc, userTurn: () => null });
    this.standingsEl = h('div', { style: { overflowY: 'auto', flex: 1 } });
    this.gamesEl = h('div', { style: { overflowY: 'auto', flex: 1 } });

    const toolbar = h('div.toolbar', {},
      h('button.btn.btn-ghost.btn-sm', { html: icon('chevronL') + '<span>All tournaments</span>', onclick: () => { this.stopRequested = true; this.showList(); } }),
      h('span.mode-toolbar-title', {}, t.name + ' · ' + t.format),
      h('div.toolbar-sep'),
      this.btnRun = h('button.btn.btn-primary.btn-sm', { html: icon('play') + '<span>Run engine games</span>', onclick: () => this.runEngineGames() }),
      this.btnStop = h('button.btn.btn-sm', { html: icon('stop') + '<span>Stop</span>', style: { display: 'none' }, onclick: () => { this.stopRequested = true; } }),
      h('div.toolbar-sep'),
      h('button.btn.btn-ghost.btn-sm', { html: icon('grid') + '<span>Crosstable</span>', onclick: () => this.crosstable() }),
      h('button.btn.btn-ghost.btn-sm', { html: icon('download') + '<span>Export PGN</span>', onclick: () => this.exportPgn() }),
      h('div.toolbar-spacer'),
      this.statusEl = h('span.mode-toolbar-title'));

    this.viewEl.append(
      h('div.dock-center', {}, h('div.dock-col', { style: { flex: 1, height: '100%' } }, toolbar, this.session.el)),
      h('div.dock-side', { style: { width: '360px', borderLeft: '1px solid var(--border)' } },
        makePanel({ id: 'standings', title: 'Standings', icon: 'trophy', body: this.standingsEl, collapsible: false }).el,
        makePanel({ id: 'tgames', title: 'Games', icon: 'list', body: this.gamesEl, collapsible: false }).el));
    this.paint();
  },

  playerName(id) { return this.t.players.find(p => p.id === id)?.label || '?'; },

  scores(t) {
    const sc = {};
    for (const p of t.players) sc[p.id] = { pts: 0, games: 0, opps: [], wins: 0 };
    for (const g of t.games) {
      if (g.result === '*') continue;
      const wPts = g.result === '1-0' ? 1 : g.result === '1/2-1/2' ? 0.5 : 0;
      sc[g.white].pts += wPts; sc[g.black].pts += 1 - wPts;
      sc[g.white].games++; sc[g.black].games++;
      sc[g.white].opps.push({ opp: g.black, got: wPts });
      sc[g.black].opps.push({ opp: g.white, got: 1 - wPts });
      if (g.result === '1-0') sc[g.white].wins++;
      if (g.result === '0-1') sc[g.black].wins++;
    }
    for (const p of t.players) {
      const s = sc[p.id];
      s.buchholz = s.opps.reduce((a, o) => a + sc[o.opp].pts, 0);
      s.sb = s.opps.reduce((a, o) => a + o.got * sc[o.opp].pts, 0);
    }
    return sc;
  },

  paint() {
    const t = this.t;
    const sc = this.scores(t);
    clear(this.standingsEl);
    const table = h('table.data-table');
    table.append(h('thead', {}, h('tr', {},
      h('th', {}, '#'), h('th', {}, 'Player'), h('th', {}, 'Pts'), h('th', { title: 'Buchholz' }, 'Bh'), h('th', { title: 'Sonneborn-Berger' }, 'SB'))));
    const tbody = h('tbody');
    [...t.players]
      .sort((a, b) => sc[b.id].pts - sc[a.id].pts || sc[b.id].buchholz - sc[a.id].buchholz || sc[b.id].sb - sc[a.id].sb)
      .forEach((p, i) => tbody.append(h('tr', {},
        h('td', {}, String(i + 1)),
        h('td', {}, p.label),
        h('td.tnum', {}, sc[p.id].pts.toFixed(1)),
        h('td.tnum', {}, sc[p.id].buchholz.toFixed(1)),
        h('td.tnum', {}, sc[p.id].sb.toFixed(2)))));
    table.append(tbody);
    this.standingsEl.append(table);

    clear(this.gamesEl);
    let lastRound = 0;
    for (const g of t.games) {
      if (g.round !== lastRound) {
        lastRound = g.round;
        this.gamesEl.append(h('div.menu-head', {}, 'Round ' + g.round));
      }
      const wp = this.t.players.find(p => p.id === g.white);
      const bp = this.t.players.find(p => p.id === g.black);
      const bothEngines = wp?.type === 'engine' && bp?.type === 'engine';
      this.gamesEl.append(h('div.list-row', {
        onclick: () => { if (g.pgn) this.loadGame(g); },
        title: g.pgn ? 'Click to view game' : (bothEngines ? 'Will be played automatically' : 'Right-click to enter result'),
        oncontextmenu: e => {
          e.preventDefault();
          showMenu([
            { head: `${this.playerName(g.white)} - ${this.playerName(g.black)}` },
            ...['1-0', '1/2-1/2', '0-1', '*'].map(r => ({
              label: r === '*' ? 'Clear result' : r, checked: g.result === r,
              action: async () => { g.result = r; g.reason = 'entered manually'; await idb.put('tournaments', t); this.paint(); },
            })),
            g.pgn ? { label: 'Open in Analysis', icon: 'microscope', action: () => this.openInAnalysis(g) } : null,
          ].filter(Boolean), e.clientX, e.clientY);
        },
      },
        h('span.faint.tnum', { style: { width: '24px' } }, '#' + g.no),
        h('span.grow', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, `${this.playerName(g.white)} - ${this.playerName(g.black)}`),
        h('span.mono', { style: { color: g.result === '*' ? 'var(--text-faint)' : 'var(--text)' } }, g.result)));
    }
    const open = t.games.filter(g => g.result === '*').length;
    this.statusEl.textContent = open ? `${open} games remaining` : 'All games finished';
    if (!open && t.format === 'swiss' && Math.max(0, ...t.games.map(g => g.round)) < t.swissRounds) {
      this.gamesEl.append(h('div', { style: { padding: '8px' } },
        h('button.btn.btn-primary.btn-sm', { onclick: async () => { this.pairSwissRound(t); await idb.put('tournaments', t); this.paint(); } }, 'Pair next round')));
    }
    if (!open && t.format === 'knockout' && t.games.filter(g => g.round === Math.max(...t.games.map(x => x.round))).length > 1) {
      this.gamesEl.append(h('div', { style: { padding: '8px' } },
        h('button.btn.btn-primary.btn-sm', { onclick: async () => { this.advanceKnockout(t); await idb.put('tournaments', t); this.paint(); } }, 'Advance bracket')));
    }
  },

  async runEngineGames() {
    if (this.running) return;
    const t = this.t;
    this.running = true;
    this.stopRequested = false;
    this.btnRun.style.display = 'none';
    this.btnStop.style.display = '';
    try {
      for (const g of t.games) {
        if (this.stopRequested) break;
        if (g.result !== '*') continue;
        const wp = t.players.find(p => p.id === g.white);
        const bp = t.players.find(p => p.id === g.black);
        if (wp.type !== 'engine' || bp.type !== 'engine') continue;
        this.statusEl.textContent = `Playing game #${g.no}: ${wp.label} - ${bp.label}`;
        const opening = MINI_BOOK[g.no % MINI_BOOK.length];
        const out = await runEngineGame({
          white: { ...wp, instance: 'tW' }, black: { ...bp, instance: 'tB' },
          openingUcis: opening.ucis, maxPlies: 220,
          onProgress: doc => {
            if (this.liveDocBound !== doc) {
              this.liveDocBound = doc;
              this.liveDoc = doc;
              this.session.bindDoc(doc);
              this.session.setPlayers({ w: { name: wp.label }, b: { name: bp.label } });
            }
            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: () => this.stopRequested,
        });
        if (out.reason === 'stopped') break;
        out.doc.headers.Event = t.name;
        out.doc.headers.Round = String(g.round);
        out.doc.headers.White = wp.label;
        out.doc.headers.Black = bp.label;
        g.result = out.result;
        g.reason = out.reason;
        g.pgn = out.doc.pgn();
        await idb.put('tournaments', t);
        this.paint();
      }
    } finally {
      this.running = false;
      this.btnRun.style.display = '';
      this.btnStop.style.display = 'none';
      this.paint();
    }
  },

  loadGame(g) {
    try {
      const doc = parsePgnGame(g.pgn);
      this.liveDoc = doc;
      this.liveDocBound = doc;
      this.session.bindDoc(doc);
      this.session.setPlayers({ w: { name: this.playerName(g.white) }, b: { name: this.playerName(g.black) } });
      doc.goEnd();
    } catch (e) { toast('Could not load: ' + e.message, 'err'); }
  },

  openInAnalysis(g) {
    try { this.ctx.openInAnalysis(parsePgnGame(g.pgn)); }
    catch (e) { toast(e.message, 'err'); }
  },

  crosstable() {
    const t = this.t;
    const table = h('table.data-table');
    const head = h('tr', {}, h('th', {}, ''));
    t.players.forEach((p, i) => head.append(h('th', { title: p.label }, String(i + 1))));
    head.append(h('th', {}, 'Pts'));
    table.append(h('thead', {}, head));
    const sc = this.scores(t);
    const tbody = h('tbody');
    t.players.forEach((p, i) => {
      const row = h('tr', {}, h('td', {}, `${i + 1}. ${p.label}`));
      t.players.forEach(q => {
        if (p.id === q.id) { row.append(h('td', {}, '-')); return; }
        const gs = t.games.filter(g => (g.white === p.id && g.black === q.id) || (g.white === q.id && g.black === p.id));
        const cell = gs.map(g => {
          if (g.result === '*') return '·';
          const mine = g.white === p.id ? g.result : g.result === '1-0' ? '0-1' : g.result === '0-1' ? '1-0' : g.result;
          return mine === '1-0' ? '1' : mine === '0-1' ? '0' : '½';
        }).join(' ');
        row.append(h('td.tnum', {}, cell || ''));
      });
      row.append(h('td.tnum', { style: { fontWeight: 700 } }, sc[p.id].pts.toFixed(1)));
      tbody.append(row);
    });
    table.append(tbody);
    showDialog({ title: 'Crosstable — ' + t.name, body: h('div', { style: { overflowX: 'auto' } }, table), width: 'min(800px, 94vw)' });
  },

  exportPgn() {
    const games = this.t.games.filter(g => g.pgn);
    if (!games.length) return toast('No played engine games to export', 'warn');
    downloadText(this.t.name.replace(/[^\w-]+/g, '_') + '.pgn', games.map(g => g.pgn.trim()).join('\n\n') + '\n');
    toast('Tournament PGN exported', 'ok');
  },
};
