/**
 * Рантайм обхода диалоговых графов (перенос схемы v1), отделённый от
 * отрисовки: игра рисует реплику/варианты своим view и зовёт advance/pick.
 * Узел с текстом ждёт игрока (advance → next, pick → вариант); узел без
 * текста («действие») проваливается дальше сам. Состояние — DialogueState
 * (флаги/переменные игры); игровые эффекты из do[] движок только эмитит
 * (EffectSink), исполнение — игра.
 */
import {
    evalConditions, hasText,
    type DialogueGraph, type DialogueNode, type DialogueChoice,
    type DialogueEffects, type DialogueResult, type DialogueState, type DialogueWorld, type DialogueEffectOp,
} from './graph';

/** Вид: игра рисует реплику и варианты своими средствами (DOM и т.п.). */
export interface DialogueView {
    /** Показать реплику; choices пуст, если выбора нет. */
    show(node: { speaker?: string; text: string; choices: { index: number; text: string }[] }): void;
    /** Скрыть (диалог завершён). */
    hide(): void;
}

const MAX_STEPS = 1000; // защита от цикла в графе без текста

export class DialogueRunner {
    /** Вызывается по завершении: граф + как он был пройден. */
    onFinish: ((graph: DialogueGraph, result: DialogueResult) => void) | null = null;
    /** Игровые эффекты из do[]; at — где сработали: узел или выбор. */
    onEffect: ((op: DialogueEffectOp, at: { nodeId: string; choice?: number }) => void) | null = null;

    private graph: DialogueGraph | null = null;
    private view: DialogueView | null = null;
    private readonly state: DialogueState;
    private readonly world?: DialogueWorld;
    private shownChoices: { index: number; text: string }[] = [];
    private currentId: string | null = null;
    private pendingNext: string | undefined;
    private awaitingChoice = false;
    private walkedPath: string[] = [];
    private madePicks: { nodeId: string; index: number; text: string }[] = [];
    private walkedResult: DialogueResult | null = null;

    constructor(state: DialogueState, view?: DialogueView, world?: DialogueWorld) {
        this.state = state;
        this.view = view ?? null;
        this.world = world;
    }

    /** Подключить/заменить view (после пересоздания UI). */
    setView(view: DialogueView): void {
        this.view = view;
    }

    get active(): boolean {
        return this.graph !== null;
    }

    // --- снапшоты для агентного моста/отладки ---

    /** id текущего узла (или null — не активен/завершается). */
    get nodeId(): string | null {
        return this.currentId;
    }

    get node(): DialogueNode | null {
        if (!this.graph || this.currentId === null) return null;
        return this.graph.nodes[this.currentId] ?? null;
    }

    /** Показанные варианты (index — позиция в узле графа). */
    get choices(): { index: number; text: string }[] {
        return this.shownChoices;
    }

    /** Ожидается ли выбор варианта. */
    get waitingForChoice(): boolean {
        return this.awaitingChoice;
    }

    /** Показанные узлы по порядку (текущий прогон). */
    get path(): string[] {
        return this.walkedPath;
    }

    /** Итог последнего завершённого диалога (null — не завершался). */
    get result(): DialogueResult | null {
        return this.walkedResult;
    }

    /** Запустить диалог. */
    start(graph: DialogueGraph): void {
        this.graph = graph;
        this.awaitingChoice = false;
        this.pendingNext = undefined;
        this.walkedPath = [];
        this.madePicks = [];
        this.walkedResult = null;
        this.enterNode(graph.start);
    }

    /** «Дальше»: во время выбора варианта игнорируется. */
    advance(): void {
        if (!this.graph || this.awaitingChoice) return;
        if (this.pendingNext === undefined) {
            this.finish();
            return;
        }
        this.enterNode(this.pendingNext);
    }

    /** Выбрать вариант ответа (индекс в показанном списке). */
    pick(index: number): void {
        if (!this.graph || !this.awaitingChoice) return;
        const shown = this.shownChoices[index];
        const choice = this.node?.choices?.[shown?.index ?? -1];
        if (!shown || !choice) return;
        const atNode = this.currentId;
        this.awaitingChoice = false;
        this.shownChoices = [];
        if (atNode !== null) this.madePicks.push({ nodeId: atNode, index: shown.index, text: shown.text });
        this.applyEffects(choice, atNode, shown.index);
        if (!this.graph) return; // эффект реентерабельно завершил диалог (abort) — уже finish
        if (choice.next === undefined) this.finish();
        else this.enterNode(choice.next);
    }

    /** Завершить диалог досрочно (результат всё равно записывается). */
    abort(): void {
        if (this.graph) this.finish();
    }

    /** Вход в узел: условия → пропуск, эффекты, показ или провал в next. */
    private enterNode(id: string): void {
        const g = this.graph;
        if (!g) return;
        let cur = id;
        for (let i = 0; i < MAX_STEPS; i++) {
            if (this.graph !== g) return; // эффект реентерабельно погасил/перезапустил диалог
            const node = g.nodes[cur];
            if (!node) {
                this.finish(); // битая ссылка — валидатор ловит заранее
                return;
            }
            if (!evalConditions(node, this.state, this.world)) {
                if (node.next === undefined) {
                    this.finish(); // условный узел закрыт и обхода нет
                    return;
                }
                cur = node.next;
                continue;
            }
            this.applyEffects(node, cur);
            if (this.graph !== g) return; // onEffect звал abort/start — не продолжаем по старому графу
            this.walkedPath.push(cur);
            if (!hasText(node)) {
                if (node.next === undefined) {
                    this.finish(); // узел-действие без next — конец
                    return;
                }
                cur = node.next;
                continue;
            }
            this.show(node);
            return;
        }
        throw new Error('Диалог: превышен MAX_STEPS — цикл в графе без текста');
    }

    /** Показ узла: выборы фильтруются условиями; index — позиция в узле. */
    private show(node: DialogueNode): void {
        this.currentId = this.walkedPath[this.walkedPath.length - 1] ?? null;
        this.pendingNext = node.next;
        this.shownChoices = (node.choices ?? [])
            .map((c: DialogueChoice, i: number) => ({ c, i }))
            .filter(({ c }) => evalConditions(c, this.state, this.world))
            .map(({ c, i }) => ({ index: i, text: c.text }));
        this.view?.show({
            speaker: node.speaker,
            text: node.text ?? '',
            choices: this.shownChoices,
        });
        if (this.shownChoices.length > 0) this.awaitingChoice = true;
    }

    private applyEffects(src: DialogueEffects, nodeId: string | null, choice?: number): void {
        for (const f of src.setFlags ?? []) this.state.setFlag(f);
        for (const f of src.clearFlags ?? []) this.state.clearFlag(f);
        for (const [k, v] of Object.entries(src.setVars ?? {})) this.state.setVar(k, v);
        for (const op of src.do ?? []) this.onEffect?.(op, { nodeId: nodeId ?? '', choice });
    }

    private finish(): void {
        const graph = this.graph;
        this.graph = null;
        this.currentId = null;
        this.awaitingChoice = false;
        this.pendingNext = undefined;
        this.shownChoices = [];
        this.view?.hide();
        if (!graph) return;
        this.walkedResult = {
            lastNodeId: this.walkedPath[this.walkedPath.length - 1] ?? null,
            path: [...this.walkedPath],
            picks: [...this.madePicks],
        };
        this.onFinish?.(graph, this.walkedResult);
    }
}