import type {
    AgentHost,
    EngineSnapshot,
    Invariant,
    SceneAgent,
    SnapshotLayer,
    WaitForOptions
} from './types';
import { mergeInvariants } from './invariants';

/**
 * Top-level ключи движкового слоя снапшота (см. snapshot()). Игра расширяет
 * его своими слоями (AGENT_SNAPSHOT_KEYS) — валидатор предикатов waitFor
 * сверяет обращения `s.<ключ>` с объединённым списком.
 */
export const ENGINE_SNAPSHOT_KEYS = [
    'tick',
    'fps',
    'fixedStep',
    'scenes',
    'transitioning',
    'camera',
    'pointer'
] as const;

/**
 * Движковый агентный мост: снапшоты, детерминированные шаги, инъекция ввода.
 * Никогда не бросает: каждый доступ — через safe() с try/catch, ошибка
 * становится слоем снапшота `{ error }`.
 */
export class EngineAgent {
    constructor(
        private host: AgentHost,
        /** Фабрика сцены-агента: сцена может отсутствовать (меню, переход). */
        private sceneAgent: () => SceneAgent | null
    ) {}

    /** Полный снапшот: движковый слой + контентный слой текущей сцены. */
    snapshot(): SnapshotLayer {
        const base = this.safe('snapshot', (): EngineSnapshot => {
            const h = this.host;
            return {
                tick: h.tickCount,
                fps: h.fps,
                fixedStep: h.fixedStep,
                scenes: [h.scenes.current?.constructor.name ?? null].filter(
                    (s): s is string => s !== null
                ),
                transitioning: h.scenes.transitioning,
                camera: {
                    x: num(h.camera.x),
                    y: num(h.camera.y),
                    shaking: h.camera.shaking
                },
                pointer: this.pointerLayer()
            };
        }) as SnapshotLayer;
        const scene = this.sceneAgent();
        if (scene) {
            const layer = this.safe('scene.snapshot', () => scene.agentSnapshot());
            if (layer) Object.assign(base, layer);
        }
        return base;
    }

    /** Нарушенные инварианты: движковые + сценические. */
    invariants(): Invariant[] {
        const engine: Invariant[] = this.safe('invariants', () => {
            const cam = this.host.camera;
            return mergeInvariants(
                checkNum('camera.x', cam.x, 'engine/camera'),
                checkNum('camera.y', cam.y, 'engine/camera'),
                this.pointerInvariants()
            );
        }) ?? [];
        const scene = this.sceneAgent();
        const sceneInvs = scene
            ? (this.safe('scene.invariants', () => scene.agentInvariants()) ?? [])
            : [];
        return [...engine, ...(Array.isArray(sceneInvs) ? sceneInvs : [])];
    }

    /** n фиксированных шагов без реального ожидания (детерминированно). */
    step(n = 1, opts?: { render?: boolean }): { tick: number } {
        const render = opts?.render ?? false;
        // Серия целиком в ручном режиме: реальный rAF-цикл между шагами не
        // тикает и не очищает «just pressed» инъекции середины серии.
        try {
            this.host.stepTicks(n, render);
        } catch {
            return { tick: this.host.tickCount };
        }
        return { tick: this.host.tickCount };
    }

    /**
     * Крутить шаги, пока предикат не станет истинным (или лимит шагов).
     * pred вызывается после каждого шага на полном снапшоте.
     */
    async waitFor(
        pred: (s: SnapshotLayer) => boolean,
        opts?: WaitForOptions
    ): Promise<{ ok: boolean; snapshot: SnapshotLayer; ticks: number }> {
        const limit = opts?.timeoutTicks ?? 600;
        const render = opts?.render ?? false;
        for (let i = 0; i < limit; i++) {
            this.step(1, { render });
            let s: SnapshotLayer;
            try {
                s = this.snapshot();
            } catch {
                continue;
            }
            let ok = false;
            try {
                ok = pred(s);
            } catch {
                // предикат упал — трактуем как «не совпало»
            }
            if (ok) return { ok: true, snapshot: s, ticks: i + 1 };
        }
        return { ok: false, snapshot: this.snapshot(), ticks: limit };
    }

    /** Ввод в виртуальных пикселях: эквивалент pointerdown/up на канвасе. */
    /**
     * Клик в виртуальных пикселях. Инъекция и один шаг логики — атомарно
     * (один JS-тик): между evaluate rAF-кадр успел бы очистить «just pressed».
     */
    tapVirtual(vx: number, vy: number): void {
        this.safe('tapVirtual', () => {
            this.host.input.injectPointerDown(vx, vy);
            this.host.input.injectPointerUp(vx, vy);
            this.host.stepTick();
        });
    }

    /** Действие через маппинг действий (isActionJustPressed внутри этого же тика). */
    press(action: string, holdTicks = 1): void {
        this.safe('press', () => {
            this.host.input.injectAction(action);
            this.host.stepTick(); // действие видно сцене в этом же тике
            for (let i = 1; i < holdTicks; i++) this.host.stepTick();
            this.host.input.injectActionRelease(action);
        });
    }

    /** Сырой код клавиши (e.code) — атомарно с одним шагом логики. */
    key(code: string): void {
        this.safe('key', () => {
            this.host.input.injectKeyCode(code);
            this.host.stepTick();
            this.host.input.injectKeyCodeUp(code);
        });
    }

    /**
     * Реальный DOM-клик по канвасу (виртуальные px → CSS): нужен для Pixi-кнопок,
     * если сцена слушает DOM напрямую. В геймплее предпочитай действия (press).
     */
    uiTap(vx: number, vy: number): void {
        this.safe('uiTap', () => {
            const canvas = document.querySelector('canvas');
            if (!canvas) return;
            const rect = canvas.getBoundingClientRect();
            const sx = rect.left + (vx / this.host.virtualWidth) * rect.width;
            const sy = rect.top + (vy / this.host.virtualHeight) * rect.height;
            canvas.dispatchEvent(new MouseEvent('pointerdown', {
                clientX: sx, clientY: sy, bubbles: true
            }));
            canvas.dispatchEvent(new MouseEvent('pointerup', {
                clientX: sx, clientY: sy, bubbles: true
            }));
        });
    }

    /** Команда сцены (whitelist в реализации SceneAgent). */
    command(name: string, args?: unknown): unknown {
        const scene = this.sceneAgent();
        if (!scene?.agentCommand) return null;
        return this.safe('command', () => scene.agentCommand!(name, args as never)) ?? null;
    }

    // --- внутреннее ---

    private pointerLayer(): SnapshotLayer {
        const p = this.host.input.getPointer();
        return {
            x: num(p.x), y: num(p.y), down: p.down,
            justPressed: p.justPressed, downTicks: p.downTicks
        };
    }

    private pointerInvariants(): Invariant[] {
        const p = this.host.input.getPointer();
        return mergeInvariants(
            checkNum('pointer.x', p.x, 'engine/pointer'),
            checkNum('pointer.y', p.y, 'engine/pointer')
        );
    }

    /** Выполнить fn, поймав исключение: ошибка -> слой { error }, а не бросок. */
    private safe<T>(label: string, fn: () => T): T | null {
        try {
            return fn();
        } catch (e) {
            return { error: `${label}: ${e instanceof Error ? e.message : String(e)}` } as unknown as T;
        }
    }
}

function num(v: number): number {
    return Number.isFinite(v) ? Math.round(v * 1000) / 1000 : String(v) as unknown as number;
}

function checkNum(name: string, v: number, where: string): Invariant | null {
    if (!Number.isFinite(v)) {
        return { id: 'pos-nan', severity: 'error', message: `${name} = ${v}`, where };
    }
    return null;
}