import { Component, type EvalContext } from "../core/world";
import { isQuest, type Quest, type QuestStage, type RPGVariables } from "../types";
import { evaluateCondition } from "../utils/conditions";
import { component } from "../utils/decorators";

export type QuestStatus = 'inactive' | 'active' | 'completed' | 'failed';

export interface QuestRuntimeState {
    status: QuestStatus;
    stageIndex: number;
}

export interface QuestEntry {
    quest: Quest;
    state: QuestRuntimeState;
}

interface QuestLogState {
    quests: Record<string, Quest>;
    runtimeStates: Record<string, QuestRuntimeState>;
}

@component
export class QuestLog extends Component<QuestLogState> {
    #cachedVars: RPGVariables | null = null;

    constructor(quests: Quest[] = []) {
        const questsRecord: Record<string, Quest> = {};
        const runtimeStates: Record<string, QuestRuntimeState> = {};
        for (const q of quests) {
            questsRecord[q.id] = q;
            runtimeStates[q.id] = { status: 'inactive', stageIndex: 0 };
        }
        super({ quests: questsRecord, runtimeStates });
    }

    addQuest(quest: Quest): void {
        if (this.state.quests[quest.id]) {
            console.warn(`[QuestLog] quest '${quest.id}' is already registered, ignoring duplicate`);
            return;
        }
        this.state.quests[quest.id] = quest;
        this.state.runtimeStates[quest.id] = { status: 'inactive', stageIndex: 0 };
    }

    #invalidate() { this.#cachedVars = null; }

    #transition(op: string, questId: string, from: QuestStatus, to: QuestStatus, event: string): boolean {
        const runtimeState = this.state.runtimeStates[questId];
        if (!runtimeState) {
            console.warn(`[QuestLog] ${op}: quest '${questId}' is not registered`);
            return false;
        }
        if (runtimeState.status !== from) {
            console.warn(`[QuestLog] ${op}: quest '${questId}' cannot transition from status '${runtimeState.status}'`);
            return false;
        }
        runtimeState.status = to;
        if (to === 'active' || to === 'inactive') runtimeState.stageIndex = 0;
        this.#invalidate();
        this.emit(event, { questId });
        return true;
    }

    start(questId: string): boolean {
        return this.#transition('start', questId, 'inactive', 'active', 'started');
    }

    complete(questId: string): boolean {
        return this.#transition('complete', questId, 'active', 'completed', 'completed');
    }

    fail(questId: string): boolean {
        return this.#transition('fail', questId, 'active', 'failed', 'failed');
    }

    abandon(questId: string): boolean {
        return this.#transition('abandon', questId, 'active', 'inactive', 'abandoned');
    }

    getState(questId: string): QuestRuntimeState | undefined {
        return this.state.runtimeStates[questId];
    }

    getQuest(questId: string): Quest | undefined {
        return this.state.quests[questId];
    }

    isAvailable(questId: string, ctx: EvalContext): boolean {
        const quest = this.state.quests[questId];
        if (!quest) return false;
        if (!quest.conditions?.length) return true;
        return quest.conditions.every(c => evaluateCondition(c, ctx));
    }

    getStage(questId: string): QuestStage | undefined {
        const quest = this.state.quests[questId];
        const runtimeState = this.state.runtimeStates[questId];
        if (!quest || runtimeState?.status !== 'active') return undefined;
        return quest.stages[runtimeState.stageIndex];
    }

    getObjectiveProgress(
        questId: string,
        ctx: EvalContext,
    ): Array<{ id: string; description: string; done: boolean }> | undefined {
        const stage = this.getStage(questId);
        if (!stage) return undefined;
        return stage.objectives.map(obj => ({
            id: obj.id,
            description: obj.description,
            done: evaluateCondition(obj.condition, ctx),
        }));
    }

    /** @internal used by QuestSystem */
    *entries(): Generator<[string, QuestEntry], void, unknown> {
        for (const [id, quest] of Object.entries(this.state.quests)) {
            yield [id, { quest, state: this.state.runtimeStates[id]! }];
        }
    }

    /** @internal called by QuestSystem after stage actions complete */
    _advance(questId: string): void {
        const quest = this.state.quests[questId];
        const runtimeState = this.state.runtimeStates[questId];
        if (!quest || !runtimeState) return;
        this.#invalidate();
        if (runtimeState.stageIndex + 1 < quest.stages.length) {
            runtimeState.stageIndex++;
            this.emit('stage', { questId, index: runtimeState.stageIndex, stage: quest.stages[runtimeState.stageIndex] });
        } else {
            runtimeState.status = 'completed';
            this.emit('completed', { questId });
        }
    }

    override getActions() {
        const result = { ...super.getActions() };
        for (const [questId, runtimeState] of Object.entries(this.state.runtimeStates)) {
            if (runtimeState.status === 'inactive') {
                result[`${questId}.start`] = this.start.bind(this, questId);
            } else if (runtimeState.status === 'active') {
                result[`${questId}.complete`] = this.complete.bind(this, questId);
                result[`${questId}.fail`] = this.fail.bind(this, questId);
                result[`${questId}.abandon`] = this.abandon.bind(this, questId);
            }
        }
        return result;
    }

    override getVariables(): RPGVariables {
        if (this.#cachedVars) return this.#cachedVars;
        const result: RPGVariables = {};
        for (const [questId, runtimeState] of Object.entries(this.state.runtimeStates)) {
            result[`${questId}.status`] = runtimeState.status;
            result[`${questId}.stage`] = runtimeState.stageIndex;
        }
        this.#cachedVars = result;
        return result;
    }
}

export namespace Quests {
    export function validate(quest: unknown, actions: string[]): string[] {
        if (!isQuest(quest)) return ['invalid quest structure'];

        const errors: string[] = [];
        const actionSet = new Set(actions);

        for (const stage of quest.stages) {
            for (const action of stage.actions) {
                const type = typeof action === 'string' ? action : action.type;
                if (!actionSet.has(type)) {
                    errors.push(`stage '${stage.id}': unknown action type '${type}'`);
                }
            }
        }

        return errors;
    }

    export function isValid(v: unknown, actions: string[]): v is Quest {
        return validate(v, actions).length === 0;
    }
}
