import type { InteractableDef } from "@psx/shared";
import { CollisionGroup } from "@psx/shared";
import type { PhysicsWorld, SensorEvents } from "../engine/PhysicsWorld.js";
import type { FlagStore } from "./FlagStore.js";
import type { InventorySystem } from "./InventorySystem.js";

/**
 * Examine / pick up / use-item-on-object — GDD §6.4.
 *
 * Reach is a sensor volume rather than a distance check so that authoring can
 * shape it: a wide desk gets a wide trigger, a keyhole gets a narrow one.
 */

export const INTERACTABLE_PREFIX = "item:";

export type InteractionResult =
  | { type: "message"; text: string }
  | { type: "picked-up"; itemId: string; quantity: number; text: string }
  | { type: "blocked"; text: string }
  | { type: "flag-set"; flag: string; text: string }
  | { type: "transition"; roomId: string; spawnPointId: string };

export class InteractionSystem {
  private readonly definitions = new Map<string, InteractableDef>();
  private readonly inReach = new Set<string>();
  private readonly consumed = new Set<string>();

  constructor(
    private readonly physics: PhysicsWorld,
    private readonly inventory: InventorySystem,
    private readonly flags: FlagStore,
  ) {}

  load(defs: InteractableDef[]): void {
    for (const def of defs) {
      this.definitions.set(def.id, def);
      this.physics.createSensor({
        id: INTERACTABLE_PREFIX + def.id,
        position: def.position,
        halfExtents: def.halfExtents,
        group: CollisionGroup.TRIGGER,
      });
    }
  }

  clear(): void {
    this.definitions.clear();
    this.inReach.clear();
  }

  handleSensorEvents(events: SensorEvents, visitorId: string): void {
    for (const event of events.begin) {
      if (event.visitorId !== visitorId) continue;
      if (event.sensorId.startsWith(INTERACTABLE_PREFIX)) {
        this.inReach.add(event.sensorId.slice(INTERACTABLE_PREFIX.length));
      }
    }
    for (const event of events.end) {
      if (event.visitorId !== visitorId) continue;
      if (event.sensorId.startsWith(INTERACTABLE_PREFIX)) {
        this.inReach.delete(event.sensorId.slice(INTERACTABLE_PREFIX.length));
      }
    }
  }

  /** The interactable the prompt should name, or null when nothing is in reach. */
  get current(): InteractableDef | null {
    for (const id of this.inReach) {
      const def = this.definitions.get(id);
      if (!def) continue;
      if (this.consumed.has(id) && def.repeatable === false) continue;
      if (!this.flags.satisfies(def.requiresFlags)) continue;
      return def;
    }
    return null;
  }

  /** Runs the interaction under the cursor. Returns null if there is nothing to do. */
  activate(): InteractionResult | null {
    const def = this.current;
    return def ? this.run(def) : null;
  }

  /**
   * Runs a named interaction regardless of local reach.
   *
   * Used by the host on behalf of a guest: sensors only ever track the local
   * player, so the host verifies the guest's proximity itself before calling
   * this (see `Game.handleGuestInteract`).
   */
  activateById(interactableId: string): InteractionResult | null {
    const def = this.definitions.get(interactableId);
    if (!def) return null;
    if (this.consumed.has(def.id) && def.repeatable === false) return null;
    if (!this.flags.satisfies(def.requiresFlags)) return null;
    return this.run(def);
  }

  private run(def: InteractableDef): InteractionResult {
    const result = this.resolve(def);
    if (result.type !== "blocked") this.consumed.add(def.id);
    return result;
  }

  private resolve(def: InteractableDef): InteractionResult {
    const action = def.action;

    switch (action.kind) {
      case "examine":
        return { type: "message", text: action.text };

      case "pickup": {
        const quantity = action.quantity ?? 1;
        if (!this.inventory.add(action.itemId, quantity)) {
          return { type: "blocked", text: "You have no room to carry that." };
        }
        // Pickups vanish once taken unless explicitly marked repeatable.
        if (def.repeatable !== true) this.despawn(def.id);
        return {
          type: "picked-up",
          itemId: action.itemId,
          quantity,
          text: `Obtained ${def.label}.`,
        };
      }

      case "door": {
        if (action.requiresItemId && !this.inventory.has(action.requiresItemId)) {
          return { type: "blocked", text: "It's locked." };
        }
        return {
          type: "transition",
          roomId: action.targetRoomId,
          spawnPointId: action.spawnPointId,
        };
      }

      case "useItem": {
        if (!this.inventory.has(action.acceptsItemId)) {
          return { type: "blocked", text: action.text };
        }
        this.inventory.remove(action.acceptsItemId);
        this.flags.set(action.onUseFlag);
        this.despawn(def.id);
        return { type: "flag-set", flag: action.onUseFlag, text: `Used the ${def.label}.` };
      }
    }
  }

  private despawn(id: string): void {
    this.definitions.delete(id);
    this.inReach.delete(id);
    this.physics.remove(INTERACTABLE_PREFIX + id);
  }

  serialise(): string[] {
    return [...this.consumed];
  }

  restore(consumed: readonly string[]): void {
    this.consumed.clear();
    for (const id of consumed) this.consumed.add(id);
  }
}
