import { GameState } from '../core/GameState';
import {
    hasConditions,
    hasText,
    evalConditions,
    type DialogueChoice,
    type DialogueConditions,
    type DialogueEffects,
    type DialogueEffectOp,
    type DialogueGraph,
    type DialogueHooks,
    type DialogueNode,
    type DialogueResult
} from './graph';

/**
 * Рантайм обхода диалоговых графов, отделённый от отрисовки и от модели
 * (типы графа и чистые предикаты — dialogue/graph.ts). Игра рисует диалог
 * своим view (например DialogueBox) и вызывает advance()/pick().
 */

/** Вид: игра рисует реплику и варианты своими средствами. */
export interface DialogueView {
    /** Показать реплику. choices пуст, если выбора нет. */
    show(node: {
        speaker?: string;
        text: string;
        mood?: string;
        tags?: 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[]`: движок только сообщает, исполнение — игра
     * (EffectSink). `at` — где сработал: узел или выбор в узле.
     */
    onEffect: ((op: DialogueEffectOp, at: { nodeId: string; choice?: number }) => void) | null =
        null;

    private graph: DialogueGraph | null = null;
    private view: DialogueView | null = null;
    private hooks: DialogueHooks | undefined;
    private shownChoices: { index: number; text: string }[] = [];
    private pendingNext: string | undefined;
    private awaitingChoice = false;
    private walkedPath: string[] = [];
    private madePicks: { nodeId: string; index: number; text: string }[] = [];
    private walkedResult: DialogueResult | null = null;
    private currentId: string | null = null;

    constructor(
        private state: GameState,
        view?: DialogueView,
        hooks?: DialogueHooks
    ) {
        this.view = view ?? null;
        this.hooks = hooks;
    }

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

    /** Подключить/заменить хуки мира (сумка, локализация строк). */
    setHooks(hooks: DialogueHooks): void {
        this.hooks = hooks;
    }

    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;
    }

    /** Показанные варианты выбора (индексы — в узел графа). */
    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;
        const next = this.pendingNext;
        if (next === undefined) {
            this.finish();
            return;
        }
        this.enterNode(next);
    }

    /** Выбрать вариант ответа. */
    pick(index: number): void {
        if (!this.graph || !this.awaitingChoice) return;
        const shown = this.shownChoices[index];
        if (!shown) return;
        const choice = this.node?.choices?.[shown.index];
        if (!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 (choice.next === undefined) {
            this.finish();
        } else {
            this.enterNode(choice.next);
        }
    }

    /** Завершить диалог досрочно. */
    finish(): void {
        const graph = this.graph;
        this.graph = null;
        this.awaitingChoice = false;
        this.shownChoices = [];
        this.pendingNext = undefined;
        this.view?.hide();
        if (graph) {
            this.walkedResult = {
                lastNodeId: this.walkedPath.length > 0 ? this.walkedPath[this.walkedPath.length - 1] : null,
                path: [...this.walkedPath],
                picks: [...this.madePicks]
            };
            this.onFinish?.(graph, this.walkedResult);
        }
    }

    /** Войти в узел: проверить условия, применить эффекты, показать или продолжить. */
    private enterNode(id: string): void {
        if (!this.graph) return;
        const node = this.graph.nodes[id];
        if (!node) {
            // несуществующий next трактуем как конец
            this.finish();
            return;
        }

        // Шагаем через «пустые» узлы (не прошедшие условия или без текста),
        // пока не встретим реплику с выбором, реплику с ожиданием или конец.
        let current: { id: string; node: DialogueNode } | null = { id, node };
        for (let steps = 0; current && steps < MAX_STEPS; steps++) {
            const n: DialogueNode = current.node;

            if (hasConditions(n) && !this.checkConditions(n)) {
                // условие не прошло — уходим по next или заканчиваем
                if (n.next === undefined) {
                    this.currentId = null;
                    this.finish();
                    return;
                }
                const nextNode = this.graph.nodes[n.next];
                if (!nextNode) {
                    this.currentId = null;
                    this.finish();
                    return;
                }
                current = { id: n.next, node: nextNode };
                continue;
            }

            this.currentId = current.id;
            this.applyEffects(n, this.currentId);

            if (n.choices && n.choices.length > 0) {
                const choices: { index: number; text: string }[] = n.choices
                    .map((c: DialogueChoice, index: number) => ({ c, index }))
                    .filter(({ c }) => this.checkConditions(c))
                    .map(({ c, index }) => ({ index, text: c.text }));
                if (hasText(n)) {
                    this.show(n, choices);
                } else if (choices.length > 0) {
                    // выбор без реплики: показываем только варианты
                    this.show({ text: '' }, choices);
                } else {
                    // нет ни текста, ни доступных вариантов — конец
                    this.currentId = null;
                    this.finish();
                    return;
                }
                return;
            }

            if (hasText(n)) {
                this.pendingNext = n.next;
                this.show(n, []);
                return;
            }

            // Действие без текста: продолжаем по next или заканчиваем
            if (n.next === undefined || n.end) {
                this.currentId = null;
                this.finish();
                return;
            }
            const nextNode = this.graph.nodes[n.next];
            if (!nextNode) {
                this.currentId = null;
                this.finish();
                return;
            }
            current = { id: n.next, node: nextNode };
        }

        // Защита сработала — принудительный конец
        this.currentId = null;
        this.finish();
    }

    /** Текст реплики: textKey резолвится хуком (fallback — сам ключ). */
    private resolveText(n: { text?: string; textKey?: string }): string {
        if (n.textKey !== undefined) return this.hooks?.resolve?.(n.textKey) ?? n.textKey;
        return n.text ?? '';
    }

    private resolveSpeaker(n: { speaker?: string; speakerKey?: string }): string | undefined {
        if (n.speakerKey !== undefined) return this.hooks?.resolve?.(n.speakerKey) ?? n.speakerKey;
        return n.speaker;
    }

    private show(
        n: DialogueNode,
        choices: { index: number; text: string }[],
        textlessChoice = false
    ): void {
        if (this.currentId !== null) this.walkedPath.push(this.currentId);
        this.shownChoices = choices;
        this.awaitingChoice = choices.length > 0;
        this.view?.show({
            speaker: this.resolveSpeaker(n),
            text: textlessChoice ? '' : this.resolveText(n),
            mood: n.mood,
            tags: n.tags,
            choices
        });
    }

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

    private checkConditions(c: DialogueConditions): boolean {
        return evalConditions(c, this.state, this.hooks?.world);
    }
}