/**
 * Модель диалоговых графов (перенос архитектуры v1): граф — данные
 * (JSON/TS): узлы с репликами, выборами, условиями и эффектами.
 * Чистые предикаты над графом — без рантайма; рантайм — DialogueRunner,
 * отрисовка — игра (view). Валидация числовая — агент правит графы текстом.
 */

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

/** Действия, применяемые к состоянию при входе в узел/выбор. */
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 {
    lastNodeId: string | null;
    path: string[];
    picks: { nodeId: string; index: number; text: string }[];
}

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

/** Условия показа узла/варианта (все перечисленные группы — AND). */
export interface DialogueConditions {
    when?: string[];
    whenNot?: string[];
    whenVar?: VarCondition;
    whenVars?: VarCondition[];
    hasItem?: string[];
}

/** Интерфейс состояния для условий/эффектов (игра реализует поверх своего стейта). */
export interface DialogueState {
    hasFlag(flag: string): boolean;
    setFlag(flag: string): void;
    clearFlag(flag: string): void;
    getVar(key: string): number | string | boolean | undefined;
    setVar(key: string, value: number | string | boolean): void;
}

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

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

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

const CMP = {
    eq: (a: unknown, b: unknown) => a === b,
    ne: (a: unknown, b: unknown) => a !== b,
    gt: (a: number, b: number) => a > b,
    lt: (a: number, b: number) => a < b,
    ge: (a: number, b: number) => a >= b,
    le: (a: number, b: number) => a <= b,
} as const;

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

/** Есть ли текст (inline или ключ). */
export function hasText(n: { text?: string }): boolean {
    return typeof n.text === 'string' && n.text.length > 0;
}

function cmpVars(c: VarCondition, state: DialogueState): boolean {
    const actual = state.getVar(c.key);
    const value = c.value;
    if (c.op === 'eq') return actual === value;
    if (c.op === 'ne') return actual !== value;
    if (typeof actual !== 'number' || typeof value !== 'number') return false; // нет/не число — сравнение ложно
    const cmp = CMP[c.op];
    return cmp(actual, value);
}

/** Все группы условий — AND; мир (сумка) — через DialogueWorld. */
export function evalConditions(c: DialogueConditions, state: DialogueState, world?: DialogueWorld): boolean {
    if (c.when && !c.when.every((f) => state.hasFlag(f))) return false;
    if (c.whenNot && c.whenNot.some((f) => state.hasFlag(f))) return false;
    if (c.whenVar && !cmpVars(c.whenVar, state)) return false;
    if (c.whenVars && !c.whenVars.every((v) => cmpVars(v, state))) return false;
    if (c.hasItem && !(world && c.hasItem.every((id) => world.hasItem(id)))) return false;
    return true;
}

/** Валидация графа: старт, ссылки next, структура узлов и выборов. */
export function validateDialogue(g: DialogueGraph): string[] {
    const errs: string[] = [];
    const ids = Object.keys(g.nodes);
    if (!g.nodes[g.start]) errs.push(`start: узел «${g.start}» не существует`);
    for (const id of ids) {
        const n = g.nodes[id];
        const refs: Array<[string | undefined, string]> = [
            [n.next, 'next'], ...(n.choices ?? []).map((c): [string | undefined, string] => [c.next, 'choice.next']),
        ];
        for (const [ref, what] of refs) {
            if (ref !== undefined && !g.nodes[ref]) errs.push(`${id}: ${what} → «${ref}» не существует`);
        }
        if (!hasText(n) && !n.next && !n.choices?.length && !n.end) {
            errs.push(`${id}: узел без текста, next, choices и end — тупик-«действие»`);
        }
        for (const c of n.choices ?? []) {
            if (!c.text) errs.push(`${id}: выбор без текста`);
            for (const op of c.do ?? []) {
                if (!(op.kind in { giveItem: 1, takeItem: 1, sound: 1, toast: 1, custom: 1 })) {
                    errs.push(`${id}: неизвестный kind «${String((op as { kind: string }).kind)}»`);
                }
            }
        }
    }
    return errs;
}