import type { CombinationDef, InventoryState, ItemDef, ItemStack } from "@psx/shared";

/**
 * Classic list-style inventory — GDD §6.3.
 *
 * Fixed slot count, because running out of space is a design lever in this
 * genre rather than a limitation. Key items are held in a separate unbounded
 * pouch so a full inventory can never soft-lock a puzzle.
 */

export type InventoryEvent =
  | { type: "added"; itemId: string; quantity: number }
  | { type: "removed"; itemId: string; quantity: number }
  | { type: "full"; itemId: string }
  | { type: "combined"; inputs: [string, string]; output: ItemStack };

export class InventorySystem {
  private state: InventoryState;
  private readonly keyItems = new Set<string>();
  private readonly listeners = new Set<(event: InventoryEvent) => void>();

  constructor(
    private readonly catalogue: ReadonlyMap<string, ItemDef>,
    private readonly combinations: readonly CombinationDef[] = [],
    capacity = 8,
  ) {
    this.state = { slots: Array<ItemStack | null>(capacity).fill(null), capacity, keyItems: [] };
  }

  subscribe(listener: (event: InventoryEvent) => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private emit(event: InventoryEvent): void {
    for (const listener of this.listeners) listener(event);
  }

  add(itemId: string, quantity = 1): boolean {
    const def = this.catalogue.get(itemId);
    if (!def) throw new Error(`Unknown item: ${itemId}`);

    if (def.isKeyItem) {
      this.keyItems.add(itemId);
      this.emit({ type: "added", itemId, quantity: 1 });
      return true;
    }

    let remaining = quantity;

    if (def.stackable) {
      const maxStack = def.maxStack ?? 99;
      for (const slot of this.state.slots) {
        if (remaining <= 0) break;
        if (slot?.itemId !== itemId || slot.quantity >= maxStack) continue;
        const room = maxStack - slot.quantity;
        const moved = Math.min(room, remaining);
        slot.quantity += moved;
        remaining -= moved;
      }
    }

    while (remaining > 0) {
      const free = this.state.slots.indexOf(null);
      if (free === -1) {
        this.emit({ type: "full", itemId });
        return false;
      }
      const moved = def.stackable ? Math.min(def.maxStack ?? 99, remaining) : 1;
      this.state.slots[free] = { itemId, quantity: moved };
      remaining -= moved;
    }

    this.emit({ type: "added", itemId, quantity });
    return true;
  }

  has(itemId: string, quantity = 1): boolean {
    if (this.keyItems.has(itemId)) return true;
    return this.count(itemId) >= quantity;
  }

  count(itemId: string): number {
    return this.state.slots.reduce(
      (total, slot) => (slot?.itemId === itemId ? total + slot.quantity : total),
      0,
    );
  }

  remove(itemId: string, quantity = 1): boolean {
    if (this.keyItems.has(itemId)) {
      // Key items are consumed only by explicit puzzle logic, never by capacity
      // pressure — see the soft-lock note above.
      this.keyItems.delete(itemId);
      this.emit({ type: "removed", itemId, quantity: 1 });
      return true;
    }

    if (this.count(itemId) < quantity) return false;

    let remaining = quantity;
    for (let i = 0; i < this.state.slots.length && remaining > 0; i++) {
      const slot = this.state.slots[i];
      if (slot?.itemId !== itemId) continue;
      const taken = Math.min(slot.quantity, remaining);
      slot.quantity -= taken;
      remaining -= taken;
      if (slot.quantity === 0) this.state.slots[i] = null;
    }

    this.emit({ type: "removed", itemId, quantity });
    return true;
  }

  /** GDD §6.3 item combination. Input order does not matter. */
  combine(itemA: string, itemB: string): ItemStack | null {
    const recipe = this.combinations.find(
      ({ inputs }) =>
        (inputs[0] === itemA && inputs[1] === itemB) ||
        (inputs[0] === itemB && inputs[1] === itemA),
    );
    if (!recipe || !this.has(itemA) || !this.has(itemB)) return null;

    // `consumes` is expressed in recipe order, which may be swapped from ours.
    const swapped = recipe.inputs[0] !== itemA;
    const consumes = recipe.consumes ?? [true, true];
    const consumeA = swapped ? consumes[1] : consumes[0];
    const consumeB = swapped ? consumes[0] : consumes[1];

    if (consumeA) this.remove(itemA);
    if (consumeB) this.remove(itemB);

    this.add(recipe.output.itemId, recipe.output.quantity);
    this.emit({ type: "combined", inputs: [itemA, itemB], output: recipe.output });
    return recipe.output;
  }

  /** Snapshot for the HUD and for cloud saves. Key items appear first. */
  list(): Array<{ def: ItemDef; quantity: number; slot: number | null }> {
    const entries: Array<{ def: ItemDef; quantity: number; slot: number | null }> = [];

    for (const itemId of this.keyItems) {
      const def = this.catalogue.get(itemId);
      if (def) entries.push({ def, quantity: 1, slot: null });
    }
    this.state.slots.forEach((slot, index) => {
      if (!slot) return;
      const def = this.catalogue.get(slot.itemId);
      if (def) entries.push({ def, quantity: slot.quantity, slot: index });
    });

    return entries;
  }

  get capacity(): number {
    return this.state.capacity;
  }

  get usedSlots(): number {
    return this.state.slots.filter((slot) => slot !== null).length;
  }

  serialise(): InventoryState {
    return {
      capacity: this.state.capacity,
      slots: this.state.slots.map((slot) => (slot ? { ...slot } : null)),
      keyItems: [...this.keyItems],
    };
  }

  restore(state: InventoryState): void {
    this.state = {
      capacity: state.capacity,
      slots: state.slots.map((slot) => (slot ? { ...slot } : null)),
      keyItems: [...(state.keyItems ?? [])],
    };
    this.keyItems.clear();
    for (const itemId of state.keyItems ?? []) {
      if (this.catalogue.has(itemId)) this.keyItems.add(itemId);
    }
  }
}
