/**
 * CLI-диспетчер агентных инструментов. Вывод по умолчанию — JSON
 * (машинночитаемый), --pretty — для человека.
 *
 *   node apps/game/tools/agent.mjs check [--only a,b] [--skip a,b] [--pretty]
 *   node apps/game/tools/agent.mjs run <apps/game/tools/checks/xxx.mjs> [--pretty]
 *   node apps/game/tools/agent.mjs snapshot [--new-game] [--out файл] [--pretty]
 *   node apps/game/tools/agent.mjs screenshot [--out файл] [--steps N] [--pretty]
 *   node apps/game/tools/agent.mjs dev [--port N]
 */
import { spawnSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { parseArgs, startDevServer, openGame } from './lib.mjs';

/** Корень репозитория (тулзы живут в apps/game/tools, скрипты npm — в корне). */
const ROOT = new URL('../../..', import.meta.url);

const args = parseArgs();
const cmd = args._[0] ?? 'check';

/** Один «серверный» шаг проверки: результат + ms + детали (fn может быть async). */
async function stepResult(name, fn) {
    const t0 = Date.now();
    try {
        const details = await fn();
        return { name, ok: true, ms: Date.now() - t0, details: details ?? null };
    } catch (err) {
        return { name, ok: false, ms: Date.now() - t0, details: String(err?.message ?? err) };
    }
}

/** Существующие .map равны генераторам (gen.test.ts сравнивает сиды с файлами). */
const mapsFresh = () => stepResult('maps:fresh', () => {
    const gen = spawnSync('npx', ['vitest', 'run', 'apps/game/tools/maps/gen.test.ts'], { encoding: 'utf8', cwd: ROOT.pathname });
    if (gen.status !== 0) throw new Error(gen.stdout.split('\n').filter((l) => l.includes('FAIL')).join('; ') || 'gen.test.ts не зелёный');
    return 'файлы карт равны генераторам';
});

/** Браузерная проверка invariants — сценарий checks/agent-invariants.mjs (единый источник). */
const agentInvariants = () => runScenario('agent:invariants', 'apps/game/tools/checks/agent-invariants.mjs');

/** Сценарий из apps/game/tools/checks/ как шаг check: exit code -> результат. */
async function runScenario(name, file) {
    return stepResult(name, async () => {
        const mod = await import(pathToFileURL(resolve(ROOT.pathname, file)));
        const code = await mod.default({ args, pretty: args.pretty });
        if (code !== 0) throw new Error(`сценарий ${file} не зелёный (exit ${code})`);
        return null;
    });
}

const commands = {
    /** Полный прогон проверок. */
    async check() {
        const all = [
            { name: 'typecheck', run: () => stepResult('typecheck', () => {
                const r = spawnSync('npm', ['run', 'typecheck'], { encoding: 'utf8', cwd: ROOT.pathname });
                if (r.status !== 0) throw new Error(r.stdout.split('\n').filter((l) => l.includes('error TS')).join('\n'));
                return null;
            }) },
            { name: 'guard:boundary', run: () => stepResult('guard:boundary', () => {
                const r = spawnSync('npm', ['run', 'guard'], { encoding: 'utf8', cwd: ROOT.pathname });
                if (r.status !== 0) throw new Error(r.stdout.split('\n').filter((l) => l.includes('"rule"') || l.includes('"ok": false')).join('\n') || 'граница движок/игра нарушена');
                return null;
            }) },
            { name: 'tests', run: () => stepResult('tests', () => {
                const r = spawnSync('npm', ['test'], { encoding: 'utf8', cwd: ROOT.pathname });
                if (r.status !== 0) throw new Error(r.stdout.split('\n').filter((l) => l.includes('FAIL')).join('; ') || 'тесты не зелёные');
                return null;
            }) },
            { name: 'maps:fresh', run: mapsFresh },
            { name: 'agent:invariants', run: agentInvariants },
            { name: 'transitions', run: () => runScenario('transitions', 'apps/game/tools/checks/transitions.mjs') },
            { name: 'interact', run: () => runScenario('interact', 'apps/game/tools/checks/interact.mjs') },
            { name: 'interact-world', run: () => runScenario('interact-world', 'apps/game/tools/checks/interact-world.mjs') },
            { name: 'quest-bells', run: () => runScenario('quest-bells', 'apps/game/tools/checks/quest-bells.mjs') },
            { name: 'lighting', run: () => runScenario('lighting', 'apps/game/tools/checks/lighting.mjs') },
            { name: 'time', run: () => runScenario('time', 'apps/game/tools/checks/time.mjs') },
            { name: 'collision', run: () => runScenario('collision', 'apps/game/tools/checks/collision.mjs') },
            { name: 'ai', run: () => runScenario('ai', 'apps/game/tools/checks/ai.mjs') },
            { name: 'audio', run: () => runScenario('audio', 'apps/game/tools/checks/audio.mjs') },
            { name: 'synth', run: () => runScenario('synth', 'apps/game/tools/checks/synth.mjs') },
            { name: 'music', run: () => runScenario('music', 'apps/game/tools/checks/music.mjs') }
        ];
        const only = args.only ?? all.map((p) => p.name);
        const skip = args.skip ?? [];
        const report = { name: 'check', ok: true, ms: 0, results: [] };
        const t0 = Date.now();
        for (const probe of all) {
            if (!only.includes(probe.name) || skip.includes(probe.name)) continue;
            let res;
            try {
                res = await probe.run();
            } catch (err) {
                res = { name: probe.name, ok: false, ms: 0, details: String(err?.message ?? err) };
            }
            report.results.push(res);
            report.ok = report.ok && res.ok;
        }
        report.ms = Date.now() - t0;
        console.log(args.pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report));
        return report.ok ? 0 : 1;
    },

    /** Один сценарий из apps/game/tools/checks/. */
    async run() {
        const file = args._[1];
        if (!file) throw new Error('укажи путь к сценарию: node apps/game/tools/agent.mjs run apps/game/tools/checks/xxx.mjs');
        const mod = await import(pathToFileURL(file).href);
        if (typeof mod.default !== 'function') throw new Error(`${file}: нет export default (async ({args}) => exitCode)`);
        return mod.default({ args, pretty: args.pretty });
    },

    /** Бенчмарк производительности (не входит в check — замер, а не гейт). */
    async perf() {
        await runScenario('perf', 'apps/game/tools/checks/perf.mjs');
        return 0;
    },

    /** Снимок снапшота в файл/stdout. */
    async snapshot() {
        const server = await startDevServer({ port: args.port ?? 5199 });
        try {
            const ctx = await openGame({ url: server.url, newGame: args.newGame ?? false });
            const snap = await ctx.agent.snapshot();
            if (args.steps) await ctx.agent.step(args.steps);
            if (args.out) writeFileSync(args.out, JSON.stringify(snap, null, 2));
            console.log(args.out ? `снапшот: ${args.out}` : JSON.stringify(snap, null, 2));
            await ctx.browser.close();
            return 0;
        } finally { server.stop(); }
    },

    /** Скриншот + консоль (для визуальных проверок, когда снапшота мало). */
    async screenshot() {
        const server = await startDevServer({ port: args.port ?? 5199 });
        try {
            const ctx = await openGame({ url: server.url, newGame: args.newGame ?? true });
            if (args.steps) await ctx.agent.step(args.steps);
            const out = args.out ?? '/tmp/rpg_agent.png';
            await ctx.agent.screenshot(out);
            console.log(JSON.stringify({ screenshot: out, logs: ctx.consoleLogs.slice(-10) }));
            await ctx.browser.close();
            return 0;
        } finally { server.stop(); }
    },

    /** Dev-сервер + URL для ручной работы (не гасит сервер до Ctrl+C). */
    async dev() {
        const server = await startDevServer({ port: args.port ?? 5199 });
        console.log(`dev: ${server.url}`);
        process.on('SIGINT', () => { server.stop(); process.exit(0); });
        await new Promise(() => {});
    }
};

const fn = commands[cmd];
if (!fn) {
    console.error(`Неизвестная команда «${cmd}». Доступны: ${Object.keys(commands).join(', ')}`);
    process.exit(2);
}
process.exit(await fn());