import { GameState } from '../core/GameState';

/**
 * Рантайм диалоговых графов, отделённый от отрисовки.
 * Граф — данные (TS/JSON): узлы с репликами, выборами и условиями по GameState.
 * Игра рисует диалог своим view (например DialogueBox) и вызывает advance()/pick().
 */

/** Условие по переменной состояния. */
export interface VarCondition {
    key: string;
    op: 'eq' | 'ne' | 'gt' | 'lt' | 'ge' | 'le';
    value: number | string;
}

/** Действия, применяемые к GameState при входе в узел/выборе. */
export interface DialogueEffects {
    setFlags?: string[];
    clearFlags?: string[];
    setVars?: Record<string, number | string | boolean>;
    /**
     * Игровые эффекты как данные: движок только эмитит их через onEffect,
     * исполнение — на стороне игры (EffectSink). Неизвестный kind — ошибка
     * валидатора, рантайм его игнорирует.
     */
    do?: DialogueEffectOp[];
}

/** Одна игровая операция эффекта (что именно она значит — знает игра). */
export interface DialogueEffectOp {
    kind: 'giveItem' | 'takeItem' | 'sound' | 'toast' | 'custom';
    /** id предмета (give/take), звука (sound) или имя сюжетного эффекта (custom). */
    id?: string;
    /** Количество (give/take), по умолчанию 1. */
    count?: number;
    /** Текст всплывашки (toast). */
    text?: string;
    /** Свободные параметры (custom). */
    payload?: Record<string, number | string | boolean>;
}

/** Итог пройденного диалога: где остановились, как дошли, что выбрали. */
export interface DialogueResult {
    /** id последнего показанного узла (null — ни одного). */
    lastNodeId: string | null;
    /** ids узлов по порядку показа. */
    path: string[];
    /** Выборы игрока: узел, индекс в узле, текст варианта. */
    picks: { nodeId: string; index: number; text: string }[];
}

/** Хуки мира: предикаты условий и резолв ключей строк (локализация). */
export interface DialogueHooks {
    world?: DialogueWorld;
    /** Ключ строки → текст (textKey); нет резолва — ключ и есть текст. */
    resolve?: (key: string) => string;
}

/**
 * Предикаты мира, которых движок знать не может (сумка, репутация...).
 * Игра передаёт реализацию в раннер; без неё hasItem-условия ложны.
 */
export interface DialogueWorld {
    hasItem(id: string): boolean;
}

/** Условия показа узла/варианта (все перечисленные группы — AND). */
export interface DialogueConditions {
    /** Все эти флаги должны быть установлены. */
    when?: string[];
    /** Ни один из этих флагов не должен быть установлен. */
    whenNot?: string[];
    /** Условие по переменной. */
    whenVar?: VarCondition;
    /** Несколько условий по переменным, AND. */
    whenVars?: VarCondition[];
    /** Все эти предметы должны быть в сумке (резолв — DialogueWorld). */
    hasItem?: string[];
}

export interface DialogueChoice extends DialogueEffects, DialogueConditions {
    text: string;
    /** Следующий узел (по умолчанию — конец диалога). */
    next?: string;
    /** Настроение реплики (игра мапит на цвет/портрет). */
    mood?: string;
    /** Свободные метки для агента/инструментов (на геймплей не влияют). */
    tags?: string[];
    /** Ключ строки в таблице локализации; переопределяет inline text. */
    textKey?: string;
}

export interface DialogueNode extends DialogueEffects, DialogueConditions {
    /** Имя говорящего (опционально). */
    speaker?: string;
    /** Ключ имени говорящего (локализация, как textKey). */
    speakerKey?: string;
    /** Текст реплики. Узел без текста — «действие»: применяет эффекты и уходит в next. */
    text?: string;
    /** Варианты ответа игрока. */
    choices?: DialogueChoice[];
    /** Следующий узел. */
    next?: string;
    /** Явный конец диалога (для конечных узлов без choices/next). */
    end?: boolean;
    /** Настроение реплики (игра мапит на цвет/портрет). */
    mood?: string;
    /** Свободные метки для агента/инструментов (на геймплей не влияют). */
    tags?: string[];
    /** Ключ строки в таблице локализации; переопределяет inline text. */
    textKey?: string;
}

export interface DialogueGraph {
    /** id стартового узла. */
    start: string;
    nodes: Record<string, DialogueNode>;
}

/** Есть ли в условиях хоть что-то для проверки. */
export function hasConditions(c: DialogueConditions): boolean {
    return Boolean(
        c.when?.length || c.whenNot?.length || c.whenVar || c.whenVars?.length || c.hasItem?.length
    );
}

/** Узел-реплика: есть inline text или ключ строки. */
function hasText(n: { text?: string; textKey?: string }): boolean {
    return n.text !== undefined || n.textKey !== undefined;
}

/** Сравнение переменной по op; undefined (переменной нет) не проходит gt/lt/ge/le. */
function checkVar(state: GameState, cond: VarCondition): boolean {
    const v = state.getVar(cond.key);
    switch (cond.op) {
        case 'eq':
            return v === cond.value;
        case 'ne':
            return v !== cond.value;
        case 'gt':
            return Number(v) > Number(cond.value);
        case 'lt':
            return Number(v) < Number(cond.value);
        case 'ge':
            return Number(v) >= Number(cond.value);
        case 'le':
            return Number(v) <= Number(cond.value);
    }
}

/**
 * Чистая проверка условий узла/выбора без рантайма: удобна для квестовых
 * стадий, dry-run и валидатора. world не задан → hasItem ложен.
 */
export function evalConditions(c: DialogueConditions, state: GameState, world?: DialogueWorld): boolean {
    for (const f of c.when ?? []) {
        if (!state.hasFlag(f)) return false;
    }
    for (const f of c.whenNot ?? []) {
        if (state.hasFlag(f)) return false;
    }
    if (c.whenVar && !checkVar(state, c.whenVar)) return false;
    for (const cond of c.whenVars ?? []) {
        if (!checkVar(state, cond)) return false;
    }
    if (c.hasItem) {
        if (!world) return false;
        for (const id of c.hasItem) {
            if (!world.hasItem(id)) return false;
        }
    }
    return true;
}

/** Вид: игра рисует реплику и варианты своими средствами. */
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;

    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 {
        return this.currentNode;
    }

    /** Показанные варианты выбора (индексы — в узел графа). */
    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.currentNode?.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 get currentNode(): DialogueNode | null {
        if (!this.graph || this.currentId === null) return null;
        return this.graph.nodes[this.currentId] ?? null;
    }

    private currentId: string | null = null;

    /** Войти в узел: проверить условия, применить эффекты, показать или продолжить. */
    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)) {
                if (!this.checkConditions(n)) {
                    // условие не прошло — уходим по next или заканчиваем
                    if (n.next !== undefined) {
                        const nextNode = this.graph.nodes[n.next];
                        if (!nextNode) {
                            this.currentId = null;
                            this.finish();
                            return;
                        }
                        current = { id: n.next, node: nextNode };
                        continue;
                    }
                    this.currentId = null;
                    this.finish();
                    return;
                }
            }

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