/* svc/enginegame.js — run one engine-vs-engine game with adjudication.
   Used by Tournament, Engine Match and Broadcast modes. */
'use strict';

import { GameDoc } from '../core/doc.js';
import { engines, whiteScore } from './engines.js';

export const MINI_BOOK = [
  { name: 'Italian', ucis: ['e2e4', 'e7e5', 'g1f3', 'b8c6', 'f1c4'] },
  { name: 'Ruy Lopez', ucis: ['e2e4', 'e7e5', 'g1f3', 'b8c6', 'f1b5'] },
  { name: 'Sicilian Najdorf', ucis: ['e2e4', 'c7c5', 'g1f3', 'd7d6', 'd2d4', 'c5d4', 'f3d4', 'g8f6', 'b1c3', 'a7a6'] },
  { name: 'French', ucis: ['e2e4', 'e7e6', 'd2d4', 'd7d5'] },
  { name: 'Caro-Kann', ucis: ['e2e4', 'c7c6', 'd2d4', 'd7d5'] },
  { name: 'QGD', ucis: ['d2d4', 'd7d5', 'c2c4', 'e7e6'] },
  { name: 'Slav', ucis: ['d2d4', 'd7d5', 'c2c4', 'c7c6'] },
  { name: 'KID', ucis: ['d2d4', 'g8f6', 'c2c4', 'g6g6'.replace('g6g6', 'g7g6'), 'b1c3', 'f8g7'] },
  { name: 'English', ucis: ['c2c4', 'e7e5', 'b1c3', 'g8f6'] },
  { name: 'London', ucis: ['d2d4', 'd7d5', 'c1f4'] },
];

/**
 * cfg: {
 *   white/black: {engine, instance, elo?, depth?, movetime?},
 *   openingUcis?: [], startFen?, maxPlies=300,
 *   adjResignCp=800, adjResignMoves=4, adjDrawCp=15, adjDrawMoves=8, adjDrawAfter=60,
 *   onProgress(doc, info)?, shouldStop()?
 * }
 * returns {doc, result, reason, crashed}
 */
export async function runEngineGame(cfg) {
  const doc = new GameDoc({ fen: cfg.startFen });
  doc.headers.White = engines.nameOf(cfg.white.engine) + (cfg.white.elo ? ` (${cfg.white.elo})` : '');
  doc.headers.Black = engines.nameOf(cfg.black.engine) + (cfg.black.elo ? ` (${cfg.black.elo})` : '');
  const maxPlies = cfg.maxPlies || 300;
  let resignRun = { w: 0, b: 0 };
  let drawRun = 0;

  for (const uci of cfg.openingUcis || []) {
    if (!doc.playMove(uci)) break;
  }
  cfg.onProgress && cfg.onProgress(doc, null);

  while (true) {
    if (cfg.shouldStop && cfg.shouldStop()) return { doc, result: '*', reason: 'stopped', crashed: false };
    const r = doc.result();
    if (r) { doc.headers.Result = r.result; return { doc, result: r.result, reason: r.reason, crashed: false }; }
    if (doc.ply() >= maxPlies) {
      doc.headers.Result = '1/2-1/2';
      return { doc, result: '1/2-1/2', reason: 'move limit', crashed: false };
    }
    const stm = doc.position.turn === 1 ? 'w' : 'b';
    const side = cfg[stm === 'w' ? 'white' : 'black'];
    const params = {
      engine: side.engine, instance: side.instance || ('t' + stm),
      fen: doc.startFen, moves: doc.path().map(n => n.uci),
    };
    if (side.elo) params.elo = side.elo;
    if (side.depth) params.depth = side.depth;
    else params.movetime = side.movetime || 500;
    const res = await engines.go(params);
    if (!res || !res.bestmove || !doc.playMove(res.bestmove)) {
      const result = stm === 'w' ? '0-1' : '1-0';
      doc.headers.Result = result;
      return { doc, result, reason: 'engine failure', crashed: true };
    }
    const node = doc.current;
    const sc = whiteScore(res, stm);
    if (sc) { node.evalCp = sc.cp ?? null; node.evalMate = sc.mate ?? null; }

    // adjudication bookkeeping (white-centric cp; mate counts as huge)
    const cp = sc ? (sc.mate != null ? Math.sign(sc.mate) * 10000 : sc.cp) : 0;
    if (cp >= (cfg.adjResignCp ?? 800)) { resignRun.w++; resignRun.b = 0; }
    else if (cp <= -(cfg.adjResignCp ?? 800)) { resignRun.b++; resignRun.w = 0; }
    else resignRun = { w: 0, b: 0 };
    if (Math.abs(cp) <= (cfg.adjDrawCp ?? 15) && doc.ply() >= (cfg.adjDrawAfter ?? 60)) drawRun++;
    else drawRun = 0;

    cfg.onProgress && cfg.onProgress(doc, res);

    if (resignRun.w >= (cfg.adjResignMoves ?? 4)) {
      doc.headers.Result = '1-0';
      return { doc, result: '1-0', reason: 'adjudication (win)', crashed: false };
    }
    if (resignRun.b >= (cfg.adjResignMoves ?? 4)) {
      doc.headers.Result = '0-1';
      return { doc, result: '0-1', reason: 'adjudication (win)', crashed: false };
    }
    if (drawRun >= (cfg.adjDrawMoves ?? 8)) {
      doc.headers.Result = '1/2-1/2';
      return { doc, result: '1/2-1/2', reason: 'adjudication (draw)', crashed: false };
    }
  }
}

/** Elo estimate from a score fraction with a simple std error. */
export function eloEstimate(wins, draws, losses) {
  const n = wins + draws + losses;
  if (!n) return { elo: 0, err: 0, score: 0 };
  const score = (wins + draws / 2) / n;
  const s = Math.min(0.999, Math.max(0.001, score));
  const elo = -400 * Math.log10(1 / s - 1);
  // std error via draws-aware variance
  const variance = (wins * (1 - s) ** 2 + draws * (0.5 - s) ** 2 + losses * (0 - s) ** 2) / n;
  const se = Math.sqrt(variance / n);
  const errElo = se > 0 ? Math.abs(-400 * Math.log10(1 / Math.min(0.999, s + 1.96 * se) - 1) - elo) : 0;
  return { elo: Math.round(elo), err: Math.round(errElo), score };
}
