import { System, type Entity, type World, type EvalContext } from "../core/world";
import { parseCondition, evaluateCondition } from "../utils/conditions";
import { resolveVariable, executeAction } from "../utils/variables";
import { QuestLog } from "../components/questLog";
import type { QuestStage, RPGVariables } from "../types";

interface EntityTracking {
    /** varName → last resolved value */
    snapshot: Map<string, RPGVariables[string]>;
    /** varName → set of questIds that reference it in the current stage */
    varToQuests: Map<string, Set<string>>;
    /** removes questlog event subscriptions for this entity */
    unsubscribe: () => void;
}

export class QuestSystem extends System {
    #tracking = new Map<string, EntityTracking>();

    override onAdd(world: World): void {
        for (const [entity, key, questLog] of world.query(QuestLog)) {
            this.#initTracking(entity, key, questLog);
        }
    }

    override onRemove(_world: World): void {
        for (const t of this.#tracking.values()) t.unsubscribe();
        this.#tracking.clear();
    }

    override update(world: World) {
        for (const [entity, key, questLog] of world.query(QuestLog)) {
            if (!this.#tracking.has(entity.id)) {
                this.#initTracking(entity, key, questLog);
            }
            this.#diffAndCheck(entity, world);
        }

        // Prune tracking for entities that no longer exist
        for (const entityId of this.#tracking.keys()) {
            if (!world.getEntity(entityId)) {
                this.#tracking.get(entityId)!.unsubscribe();
                this.#tracking.delete(entityId);
            }
        }
    }

    /** Force a full re-evaluation of all active quests. Use as an escape hatch. */
    triggerCheck(world: World) {
        for (const [entity] of world.query(QuestLog)) {
            this.#checkEntity(entity, world, 'all');
        }
    }

    #initTracking(entity: Entity, key: string, questLog: QuestLog): EntityTracking {
        const existing = this.#tracking.get(entity.id);
        if (existing) existing.unsubscribe();

        const tracking: EntityTracking = {
            snapshot: new Map(),
            varToQuests: new Map(),
            unsubscribe: () => { },
        };

        for (const [questId, { quest, state }] of questLog.entries()) {
            if (state.status !== 'active') continue;
            const stage = quest.stages[state.stageIndex];
            if (stage) this.#addQuestVars(tracking, questId, stage);
        }

        // Keep tracking fresh as quest state changes
        const onStarted = ({ data }: { data?: unknown }) => {
            const { questId } = data as { questId: string };
            const quest = questLog.getQuest(questId);
            const state = questLog.getState(questId);
            const stage = quest?.stages[state?.stageIndex ?? 0];
            if (stage) {
                this.#addQuestVars(tracking, questId, stage);
                // Evaluate immediately — conditions may already be satisfied at start
                this.#checkEntity(entity, entity.world, new Set([questId]));
            }
        };

        const onStage = ({ data }: { data?: unknown }) => {
            const { questId, stage } = data as { questId: string; stage: QuestStage };
            this.#removeQuestVars(tracking, questId);
            this.#addQuestVars(tracking, questId, stage);
            this.#checkEntity(entity, entity.world, new Set([questId]));
        };

        const onDone = ({ data }: { data?: unknown }) => {
            this.#removeQuestVars(tracking, (data as { questId: string }).questId);
        };

        const u1 = entity.on(`${key}.started`, onStarted);
        const u2 = entity.on(`${key}.stage`, onStage);
        const u3 = entity.on(`${key}.completed`, onDone);
        const u4 = entity.on(`${key}.failed`, onDone);
        const u5 = entity.on(`${key}.abandoned`, onDone);
        tracking.unsubscribe = () => { u1(); u2(); u3(); u4(); u5(); };

        this.#tracking.set(entity.id, tracking);
        return tracking;
    }

    #addQuestVars(tracking: EntityTracking, questId: string, stage: QuestStage): void {
        const conditions = [
            ...stage.objectives.map(o => o.condition),
            ...(stage.failConditions ?? []),
        ];
        for (const cond of conditions) {
            const varName = parseCondition(cond).variable;
            // Do NOT seed the snapshot here. #diffAndCheck treats "not in snapshot"
            // as dirty on first encounter, ensuring a fresh eval after every init or start.
            if (!tracking.varToQuests.has(varName)) {
                tracking.varToQuests.set(varName, new Set());
            }
            tracking.varToQuests.get(varName)!.add(questId);
        }
    }

    #removeQuestVars(tracking: EntityTracking, questId: string): void {
        for (const [varName, questIds] of tracking.varToQuests) {
            questIds.delete(questId);
            if (questIds.size === 0) {
                tracking.varToQuests.delete(varName);
                tracking.snapshot.delete(varName);
            }
        }
    }

    #diffAndCheck(entity: Entity, world: World) {
        const tracking = this.#tracking.get(entity.id);
        if (!tracking || tracking.varToQuests.size === 0) return;

        const dirty = new Set<string>();

        for (const [varName, questIds] of tracking.varToQuests) {
            const curr = resolveVariable(varName, entity);
            const prev = tracking.snapshot.get(varName);
            // Treat "not yet in snapshot" as dirty — catches conditions already met at init
            if (!tracking.snapshot.has(varName) || curr !== prev) {
                tracking.snapshot.set(varName, curr);
                for (const questId of questIds) dirty.add(questId);
            }
        }

        if (dirty.size > 0) this.#checkEntity(entity, world, dirty);
    }

    #checkEntity(entity: Entity, world: World, filter: Set<string> | 'all') {
        const questLog = entity.get(QuestLog);
        if (!questLog) return;

        for (const [questId, { quest, state }] of questLog.entries()) {
            if (state.status !== 'active') continue;
            if (filter !== 'all' && !filter.has(questId)) continue;

            const stage = quest.stages[state.stageIndex];
            if (!stage) {
                console.warn(`[QuestSystem] quest '${questId}' active but missing stage ${state.stageIndex}`);
                continue;
            }

            if (stage.failConditions?.some(c => evaluateCondition(c, entity))) {
                questLog.fail(questId);
                continue;
            }

            if (!stage.objectives.every(o => evaluateCondition(o.condition, entity))) continue;

            for (const action of stage.actions) executeAction(action, entity);
            questLog._advance(questId);
        }
    }
}
