import {
    EngineAgent,
    dayNightFactor,
    type AgentHost,
    type Invariant,
    type JsonValue,
    type SceneAgent,
    type SnapshotLayer
} from '@rpg/engine';
import type { Game } from '../Game';
import {
    AGENT_SNAPSHOT_KEYS,
    gameLayer,
    type DialogueSnapshot,
    type GameSnapshot,
    type InteractableSnapshot
} from './snapshot';
import { validateContent } from '../data/validate';
import { usedFlag } from '../data/ids';

/**
 * Агентный мост игры: поверх движкового каркаса (EngineAgent) добавляет
 * контентные слои (флаги/вары/инвентарь/сцена) и помощники высокого уровня
 * (ходьба по маршруту, диалоги, «новая игра»). Регистрируется как
 * `window.__agent` (только DEV) — инструменты в apps/game/tools/ ходят через него.
 */

export interface AgentApi {
    version: 1;
    snapshot(): GameSnapshot;
    invariants(): Invariant[];
    /** n фиксированных шагов (детерминированно, без реального ожидания). */
    step(n?: number, opts?: { render?: boolean }): { tick: number };
    /** pred — строка-выражение над снапшотом (из браузера функцию не передать). */
    waitFor(
        pred: string,
        opts?: { timeoutTicks?: number; render?: boolean }
    ): Promise<WaitResult>;
    /** Клик в виртуальных пикселях (480×270). */
    tapVirtual(vx: number, vy: number): void;
    /** Реальный DOM-клик (Pixi-кнопки меню). */
    uiTap(vx: number, vy: number): void;
    /** Действие по маппингу (advance/attack/...). */
    press(action: string, holdTicks?: number): void;
    /** Сырой код клавиши (e.code). */
    key(code: string): void;
    /** Whitelist-команда сцены (scene:sleepAll, scene:teleport, ...). */
    command(name: string, args?: JsonValue): JsonValue;
    /** Пойти в тайл по маршруту A* (в сцене). false — путь не найден/не дошёл. */
    walkTo(tx: number, ty: number, opts?: { timeoutTicks?: number }): Promise<boolean>;
    /** Подойти к тайлу (цель может быть непроходима). null — соседей нет/не дошёл. */
    approach(tx: number, ty: number, opts?: { timeoutTicks?: number }): Promise<{ x: number; y: number } | null>;
    /** Дождаться и долистать активный диалог. false — диалог не открылся. */
    runDialogue(timeoutTicks?: number): Promise<boolean>;
    /** Дождаться выбора в диалоге и выбрать вариант по тексту. false — не дождались/нет варианта. */
    pickChoiceByText(text: string, timeoutTicks?: number): Promise<boolean>;
    /** «Новая игра» из меню. */
    newGame(): Promise<void>;
    /** id текущей локации (null — сцена не локация). */
    currentArea(): string | null;
    /** Масштаб и позиция worldRoot (проверки зума). */
    worldRootView(): { scale: number; position: { x: number; y: number } };
}

/** Действие «листать диалог» (маппится и на Space, и на Enter). */
const ADVANCE = 'advance';

/** Итог waitFor: error заполняется только при отклонённом предикате. */
export interface WaitResult {
    ok: boolean;
    snapshot: GameSnapshot;
    ticks: number;
    /** Предикат ссылался на неизвестный ключ (опечатка) — исполнение не начато. */
    error?: string;
}

/** Top-level обращения `s.<ключ>` в строке-предикате (вложенные пути не трогаем). */
export function predKeys(pred: string): string[] {
    const re = /(?:^|[^.\w$])s\.([A-Za-z_$][\w$]*)/g;
    const keys = new Set<string>();
    let m: RegExpExecArray | null;
    while ((m = re.exec(pred)) !== null) keys.add(m[1]!);
    return [...keys];
}

export class GameAgent {
    private engineAgent: EngineAgent;

    constructor(private game: Game) {
        this.engineAgent = new EngineAgent(
            game.engine as unknown as AgentHost,
            () => (game.scenes.current as unknown as SceneAgent | undefined) ?? null
        );
    }

    /** Масштаб и позиция worldRoot (проверки зума из tools/checks/zoom.mjs). */
    worldRootView(): { scale: number; position: { x: number; y: number } } {
        const root = this.game.worldRoot;
        return { scale: root.scale.x, position: { x: root.position.x, y: root.position.y } };
    }

    snapshot(): GameSnapshot {
        const s = this.engineAgent.snapshot() as GameSnapshot;
        // Слой игры поверх движкового и сценического.
        Object.assign(s, gameLayer({
            flags: this.game.state.allFlags,
            vars: { ...this.game.state.serialize().vars },
            inventory: this.game.inventory.all,
            time: {
                minutes: this.game.clock.minutes,
                label: this.game.clock.label,
                night: dayNightFactor(this.game.clock.hours)
            }
        }));
        return s;
    }

    invariants(): Invariant[] {
        // Контентная валидация живёт здесь (не в сцене), чтобы работать и в меню.
        const list: Invariant[] = [...this.engineAgent.invariants(), ...validateContent(this.game.mapFiles)];
        // used-флаги и снапшот идут разными путями — сверяем их на месте.
        const s = this.snapshot();
        const flags = new Set(s.flags ?? []);
        for (const o of (s.interactables ?? []) as InteractableSnapshot[]) {
            if (flags.has(usedFlag(o.id)) !== o.used) {
                list.push({
                    id: 'interact-used-consistent',
                    severity: 'error',
                    message: `объект ${o.id}: флаг used=${flags.has(usedFlag(o.id))}, снапшот used=${o.used}`,
                    where: 'agent/GameAgent'
                });
            }
        }
        return list;
    }

    step(n = 1, opts?: { render?: boolean }): { tick: number } {
        return this.engineAgent.step(n, opts);
    }

    async waitFor(
        pred: string,
        opts?: { timeoutTicks?: number; render?: boolean }
    ): Promise<WaitResult> {
        // Опечатка в ключе предиката — тихий false на весь таймаут. Отклоняем
        // сразу: unknown key -> { ok: false, error }, шаги не тратятся.
        const unknown = predKeys(pred).filter((k) => !AGENT_SNAPSHOT_KEYS.includes(k));
        if (unknown.length > 0) {
            return {
                ok: false,
                snapshot: this.snapshot(),
                ticks: 0,
                error: `неизвестный ключ s.${unknown[0]} в предикате (см. AGENT_SNAPSHOT_KEYS)`
            };
        }
        // Строка-выражение приходит из apps/game/tools/ (доверенная среда репозитория).
        const fn = new Function('s', `"use strict"; return (${pred});`) as (s: SnapshotLayer) => boolean;
        const limit = opts?.timeoutTicks ?? 600;
        const render = opts?.render ?? false;
        // Ждём по ПОЛНОМУ снапшоту игры (flags/vars/inventory поверх сцены):
        // engineAgent.waitFor видит только движковый + сценический слои.
        for (let i = 0; i < limit; i++) {
            this.step(1, { render });
            const s = this.snapshot();
            let ok = false;
            try {
                ok = fn(s);
            } catch {
                // предикат упал — трактуем как «не совпало»
            }
            if (ok) return { ok: true, snapshot: s, ticks: i + 1 };
        }
        return { ok: false, snapshot: this.snapshot(), ticks: limit };
    }

    tapVirtual(vx: number, vy: number): void {
        this.engineAgent.tapVirtual(vx, vy);
    }

    uiTap(vx: number, vy: number): void {
        this.engineAgent.uiTap(vx, vy);
    }

    press(action: string, holdTicks = 1): void {
        this.engineAgent.press(action, holdTicks);
    }

    key(code: string): void {
        this.engineAgent.key(code);
    }

    command(name: string, args?: JsonValue): JsonValue {
        return this.engineAgent.command(name, args) as JsonValue;
    }

    /**
     * Идти в тайл: сцена строит A* и ведёт героя сама (scene:walk), мост ждёт
     * тайла цели. Без `!moving`: step-переход срабатывает, как только округлённый
     * тайл героя становится целевым (сцена замирает ещё в moving), — окна «на
     * тайле и стоит» может не быть. false — пути нет.
     */
    async walkTo(tx: number, ty: number, opts?: { timeoutTicks?: number }): Promise<boolean> {
        const started = this.command('scene:walk', { x: tx, y: ty });
        if (started !== true) return false;
        const wait = await this.waitFor(
            `s.hero && s.hero.tile && s.hero.tile.x === ${tx} && s.hero.tile.y === ${ty}`,
            { timeoutTicks: opts?.timeoutTicks ?? 600 }
        );
        return wait.ok;
    }

    /**
     * Подойти к тайлу (цель может быть непроходима: стены, объекты, NPC-тайлы,
     * тела врагов). Сцена ведёт к ближайшему проходимому соседу (scene:approach).
     * Возврат — тайл прибытия или null (соседей нет / не дошёл).
     */
    async approach(
        tx: number,
        ty: number,
        opts?: { timeoutTicks?: number }
    ): Promise<{ x: number; y: number } | null> {
        const dest = this.command('scene:approach', { x: tx, y: ty });
        const d = dest as { x?: unknown; y?: unknown } | null;
        if (!d || typeof d.x !== 'number' || typeof d.y !== 'number') return null;
        const wait = await this.waitFor(
            `s.hero && s.hero.tile && s.hero.tile.x === ${d.x} && s.hero.tile.y === ${d.y} && !s.hero.moving`,
            { timeoutTicks: opts?.timeoutTicks ?? 600 }
        );
        return wait.ok ? { x: d.x, y: d.y } : null;
    }

    /** Долистать активный диалог: жать advance, пока он открыт. */
    async runDialogue(timeoutTicks = 600): Promise<boolean> {
        const opened = await this.waitFor('s.dialogue != null', { timeoutTicks });
        if (!opened.ok) return false;
        for (let i = 0; i < 50; i++) {
            const d = this.snapshot().dialogue as DialogueSnapshot | null;
            if (!d) return true; // диалог закрылся — завершён
            if (d.waitingForChoice) {
                this.command('scene:pickChoice', { index: 0 });
            } else {
                this.press(ADVANCE);
            }
            this.step(2);
        }
        return !this.snapshot().dialogue;
    }

    /**
     * Дождаться выбора в диалоге и выбрать вариант по тексту (через
     * scene:pickChoiceByText — порядок вариантов сценарию знать не надо).
     */
    async pickChoiceByText(text: string, timeoutTicks = 600): Promise<boolean> {
        const opened = await this.waitFor('s.dialogue != null && s.dialogue.waitingForChoice', {
            timeoutTicks
        });
        if (!opened.ok) return false;
        const picked = this.command('scene:pickChoiceByText', { text });
        this.step(2);
        return picked === true;
    }

    /** «Новая игра» из меню (команда сцены — без координат кнопок). */
    async newGame(): Promise<void> {
        // begin() молча отбрасывает команды во время fade-перехода — ждём его конца.
        await this.waitFor('!s.transitioning', { timeoutTicks: 1200 });
        this.command('menu:newGame');
        await this.waitFor('s.scene === "location"', { timeoutTicks: 1200 });
    }

    currentArea(): string | null {
        const s = this.snapshot();
        return typeof s.area === 'string' ? s.area : null;
    }
}

/** Зарегистрировать window.__agent (только DEV). */
export function registerAgent(game: Game): AgentApi {
    const agent = new GameAgent(game);
    const api: AgentApi = {
        version: 1,
        snapshot: () => agent.snapshot(),
        invariants: () => agent.invariants(),
        step: (n, opts) => agent.step(n, opts),
        waitFor: (pred, opts) => agent.waitFor(pred, opts),
        tapVirtual: (vx, vy) => agent.tapVirtual(vx, vy),
        uiTap: (vx, vy) => agent.uiTap(vx, vy),
        press: (action, holdTicks) => agent.press(action, holdTicks),
        key: (code) => agent.key(code),
        command: (name, args) => agent.command(name, args),
        walkTo: (tx, ty, opts) => agent.walkTo(tx, ty, opts),
        approach: (tx, ty, opts) => agent.approach(tx, ty, opts),
        runDialogue: (timeoutTicks) => agent.runDialogue(timeoutTicks),
        pickChoiceByText: (text, timeoutTicks) => agent.pickChoiceByText(text, timeoutTicks),
        newGame: () => agent.newGame(),
        currentArea: () => agent.currentArea(),
        worldRootView: () => agent.worldRootView()
    };
    (window as unknown as { __agent?: AgentApi }).__agent = api;
    return api;
}