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

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

export interface DialogueChoice extends DialogueEffects, DialogueConditions {
    text: string;
    /** Следующий узел (по умолчанию — конец диалога). */
    next?: string;
}

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

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

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

    private graph: DialogueGraph | null = null;
    private view: DialogueView | null = null;
    private shownChoices: { index: number; text: string }[] = [];
    private pendingNext: string | undefined;
    private awaitingChoice = false;

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

    /** Подключить/заменить 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 {
        return this.currentNode;
    }

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

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

    /** Запустить диалог. */
    start(graph: DialogueGraph): void {
        this.graph = graph;
        this.awaitingChoice = false;
        this.pendingNext = undefined;
        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;

        this.awaitingChoice = false;
        this.shownChoices = [];
        this.applyEffects(choice);
        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.onFinish?.(graph);
    }

    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 (n.when || n.whenNot || n.whenVar) {
                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);

            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 (n.text !== undefined) {
                    this.show(n, choices);
                } else if (choices.length > 0) {
                    // выбор без реплики: показываем только варианты
                    this.show({ text: '' }, choices);
                } else {
                    // нет ни текста, ни доступных вариантов — конец
                    this.currentId = null;
                    this.finish();
                    return;
                }
                return;
            }

            if (n.text !== undefined) {
                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();
    }

    private show(
        n: { speaker?: string; text?: string },
        choices: { index: number; text: string }[],
        textlessChoice = false
    ): void {
        this.shownChoices = choices;
        this.awaitingChoice = choices.length > 0;
        this.view?.show({
            speaker: n.speaker,
            text: textlessChoice ? '' : (n.text ?? ''),
            choices
        });
    }

    private applyEffects(e: DialogueEffects): 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);
    }

    private checkConditions(c: DialogueConditions): boolean {
        for (const f of c.when ?? []) {
            if (!this.state.hasFlag(f)) return false;
        }
        for (const f of c.whenNot ?? []) {
            if (this.state.hasFlag(f)) return false;
        }
        if (c.whenVar) {
            const v = this.state.getVar(c.whenVar.key);
            const { op, value } = c.whenVar;
            switch (op) {
                case 'eq':
                    if (v !== value) return false;
                    break;
                case 'ne':
                    if (v === value) return false;
                    break;
                case 'gt':
                    if (!(Number(v) > Number(value))) return false;
                    break;
                case 'lt':
                    if (!(Number(v) < Number(value))) return false;
                    break;
                case 'ge':
                    if (!(Number(v) >= Number(value))) return false;
                    break;
                case 'le':
                    if (!(Number(v) <= Number(value))) return false;
                    break;
            }
        }
        return true;
    }
}