import { Component } from "../core/world";
import type { RPGVariables } from "../types";
import { action, component, ComponentTag, getComponentTags } from "../utils/decorators";
import { Effect } from "./effect";

// ── Equippable ────────────────────────────────────────────────────────────────

interface EquippableState {
    slotType: string;  // e.g. 'weapon', 'armor', 'accessory'
}

/**
 * Add to an item entity to make it equippable.
 * `slotType` must match the target slot's type; slots with no type accept anything.
 */
@component
export class Equippable extends Component<EquippableState> {
    constructor(slotType: string) {
        super({ slotType });
    }

    get slotType(): string { return this.state.slotType; }
}

// ── Equipment ─────────────────────────────────────────────────────────────────

interface SlotRecord {
    slotName: string;
    type: string | null;       // null = generic (accepts any Equippable)
    itemId: string | null;
    appliedEffectKeys: string[];
}

interface EquipmentState {
    slots: Record<string, SlotRecord>;
}

type SlotDefinition =
    | string                                    // generic slot
    | { slotName: string; type?: string };      // typed slot

export type SlotInput = SlotDefinition | SlotDefinition[];

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

    constructor(...slots: SlotInput[]) {
        const record: Record<string, SlotRecord> = {};
        for (const s of slots.flat()) {
            const slotName = typeof s === 'string' ? s : s.slotName;
            const type = typeof s === 'object' && s.type ? s.type : null;
            record[slotName] = { slotName, type, itemId: null, appliedEffectKeys: [] };
        }
        super({ slots: record });
    }

    #slot(slotName: string): SlotRecord | undefined {
        return this.state.slots[slotName];
    }

    /** ItemId in the named slot, or null if empty. */
    getItem(slotName: string): string | null {
        return this.#slot(slotName)?.itemId ?? null;
    }

    /**
     * Find the first empty slot compatible with `slotType`.
     * Typed slots that match take priority over generic (untyped) slots.
     * Returns `null` if no compatible empty slot exists.
     */
    findCompatibleSlot(slotType: string): string | null {
        let genericFallback: string | null = null;
        for (const slot of Object.values(this.state.slots)) {
            if (slot.itemId !== null) continue;
            if (slot.type === null) { genericFallback ??= slot.slotName; continue; }
            if (slot.type === slotType) return slot.slotName;
        }
        return genericFallback;
    }

    /** All currently equipped `{ slotName, itemId }` pairs. */
    getEquipped(): { slotName: string; itemId: string }[] {
        return Object.values(this.state.slots)
            .filter((s): s is SlotRecord & { itemId: string } => s.itemId !== null)
            .map(({ slotName, itemId }) => ({ slotName, itemId }));
    }

    /**
     * Equip an item into a named slot.
     *
     * - The item entity must have an `Equippable` component.
     * - If the slot has a type, `Equippable.slotType` must match.
     * - If the slot is occupied the existing item is unequipped first.
     * - All `Effect` components on the item entity are cloned onto the owner.
     */
    @action
    equip({ slotName, itemId }: { slotName: string; itemId: string }): boolean {
        const slot = this.#slot(slotName);
        if (!slot) return false;

        const itemEntity = this.entity.world.getEntity(itemId);
        if (!itemEntity) return false;

        const equippable = itemEntity.get(Equippable);
        if (!equippable) return false;

        if (slot.type !== null && equippable.slotType !== slot.type) return false;

        this.unequip(slotName);

        slot.itemId = itemId;
        this.#cachedVars = null;

        let id = 0;
        for (const [key, component] of itemEntity) {
            if (!getComponentTags(component).has(ComponentTag.Equippable)) continue;

            const effectKey = `__equip_${slotName}_${key}_${id++}`;
            this.entity.clone(component, effectKey);
            slot.appliedEffectKeys.push(effectKey);
        }

        this.emit('equip', { slotName, itemId });
        return true;
    }

    /**
     * Remove the item from a named slot, reversing all stat effects.
     * Returns `false` if the slot is empty or does not exist.
     */
    @action
    unequip(slotName: string): boolean {
        const slot = this.#slot(slotName);
        if (!slot || slot.itemId === null) return false;

        const itemId = slot.itemId;
        this.#removeEffects(slot);
        slot.itemId = null;
        this.#cachedVars = null;

        this.emit('unequip', { slotName, itemId });
        return true;
    }

    #removeEffects(slot: SlotRecord): void {
        for (const key of slot.appliedEffectKeys) {
            this.entity.remove(key);
        }
        slot.appliedEffectKeys = [];
    }

    override getVariables(): RPGVariables {
        if (this.#cachedVars) return this.#cachedVars;

        const result: RPGVariables = {};
        for (const { slotName, itemId } of Object.values(this.state.slots)) {
            if (itemId) {
                result[slotName] = itemId;
            }
        }
        this.#cachedVars = result;

        return result;
    }
}
