/**
 * FKTRY shared contracts — ORCHESTRATOR-OWNED.
 * Every lane imports from this file. NO lane edits it. If a contract blocks you,
 * write the problem in your lane doc's NOTES and stub around it locally.
 */

export const TICKS_PER_SECOND = 30;
export const TILE = 1; // world units per grid tile

export type Dir = 0 | 1 | 2 | 3; // N E S W (clockwise)
export interface Vec2 { x: number; y: number }

// ---------------------------------------------------------------- data schemas
// These mirror data/*.json (LANE-DATA owns the JSON, orchestrator owns the shape).

export interface ItemDef {
  id: string;          // kebab-case, matches codex name, e.g. "chroma-slurry"
  name: string;        // display, e.g. "CHROMA SLURRY"
  codex: string;       // section ref into docs/FKTRY_LORE.md, e.g. "2:chroma-slurry"
  tier: number;        // 0 raw, 1 refined, 2 product, 3 endgame
  color: string;       // hex, used by renderer placeholders + UI chips
}

export interface RecipeDef {
  id: string;
  machine: string;                  // MachineDef.id that can run it
  inputs: Record<string, number>;   // itemId -> count
  outputs: Record<string, number>;
  ticks: number;                    // duration at speed 1
  bandwidth: number;                // power draw while crafting (negative = generates)
  heat?: number;                    // heat per craft (software decoders etc.)
}

export type MachineKind =
  | 'extractor'   // pulls items from seam tiles, no inputs
  | 'crafter'     // recipe in -> out
  | 'belt'        // moves items
  | 'splitter'    // 1 in, N out round-robin
  | 'power'       // generates bandwidth
  | 'buffer'      // stores bandwidth smoothness (brownout protection)
  | 'shipper'     // consumes items => 'shipped' events => THE SCREEN
  | 'lab';        // v4: consumes science packs toward the active research

export interface MachineDef {
  id: string;
  name: string;
  codex: string;
  kind: MachineKind;
  footprint: Vec2;      // tiles occupied (w,h) before rotation
  recipes: string[];    // RecipeDef.ids this machine may run ([] for belts etc.)
  powerDraw: number;    // idle bandwidth draw
  powerGen?: number;
  beltSpeed?: number;   // tiles/sec, kind==='belt'
  asset: string;        // codex asset key for renderer mesh registry, e.g. "quantizer"
  color?: string;       // v3 RULING: CHASSIS/BODY color (codex §8 grime); the emissive
                        // accent stays derived from outputs. (Was mislabeled "accent".)
  flavor?: string;      // v2: codex one-liner for the inspector (DATA's material)
  heatPerTick?: number; // v2: heat while active. v4 RULING: this is GROSS heat — net
                        // climb = heatPerTick − coolPerTick; a machine whose
                        // heatPerTick ≤ its coolPerTick can never overheat.
  bufferCap?: number;   // v2: bandwidth storage in bandwidth-SECONDS (see SimSnapshot)
  coolPerTick?: number; // v3: per-machine cooling rate; sim default 0.004
  accent?: string;      // v4: art-directed emissive accent; fallback stays derived
                        // from first output item (renderer + UI share the derivation)
  coolRadius?: number;  // v4: spatial cooling aura in tiles (chebyshev from footprint).
                        // v5 CLARIFIED: auras are ADDITIVE — a machine in range cools at
                        // its own coolPerTick + sum of in-range auras, capped at the
                        // exported HEAT_COOL_MAX (see src/sim/constants.ts)
}

export interface TechDef {
  id: string;
  era: 'reel' | 'broadcast' | 'disc' | 'stream' | 'fortress';
  cost: Record<string, number>;     // science-pack itemId -> count
  unlocks: string[];                // machine/recipe ids
}

export interface CommissionDef {
  id: string;
  flavor: string;                   // the fax text — deadpan absurd
  wants: Record<string, number>;
  rewardBandwidth: number;
  repeat?: boolean;                 // v2: standing order — re-activates after completion
}

// v7: lifted verbatim from SIM's round-5 mirror — data/seams.json's shape.
export interface SeamDef {
  id: string;
  rect: { x: number; y: number; w: number; h: number };
  era?: string;
  resource?: string;
  richness?: number;
  hidden?: boolean;    // treasure — only the numbers station may reveal it
}
export interface SeamMap {
  /** DATA keeps this corner seam-free so the seed-derived relic always lands somewhere. */
  relicReserve?: { x: number; y: number; w: number; h: number };
  seams: SeamDef[];
}

export interface GameData {
  items: ItemDef[];
  machines: MachineDef[];
  recipes: RecipeDef[];
  tech: TechDef[];
  commissions: CommissionDef[];
  seams?: SeamMap;     // v7: wired by main.ts from data/seams.json; extractors need ore
}

// ---------------------------------------------------------------- live sim state

export interface EntityState {
  id: number;
  def: string;                      // MachineDef.id
  pos: Vec2;                        // tile coords of origin corner
  dir: Dir;
  recipe: string | null;
  progress: number;                 // 0..1 of current craft
  inputBuf: Record<string, number>;
  outputBuf: Record<string, number>;
  jammed: string | null;            // human-readable reason or null
  heat: number;                     // 0..1, throttles >0.7, scrams at 1
  scrammed?: boolean;               // v3: heat-scram latch (heat alone can't show the
                                    // hysteresis window; renderer/UI read this, not 0.5)
}

export interface BeltItem {
  item: string;
  entity: number;                   // belt entity id
  t: number;                        // 0..1 progress along the belt tile
  id: number;                       // v3: stable per-item id (required — sim always stamps)
}

export interface SimSnapshot {
  tick: number;
  paused: boolean;
  entities: ReadonlyArray<EntityState>;
  beltItems: ReadonlyArray<BeltItem>;
  // v3 RULING on units: gen/draw are bandwidth per SECOND; stored is bandwidth-seconds
  // (sim integrates surplus/deficit divided by TICKS_PER_SECOND each tick).
  // v4: capacity = summed bufferCap of placed tanks (0 if none) — SCREEN's 2nd dread axis.
  bandwidth: { gen: number; draw: number; stored: number; capacity?: number; brownout: boolean };
  shippedTotal: Record<string, number>;
  activeCommission: string | null;
  commissionQueue?: string[];       // v3: active first, then upcoming in activation
                                    // order. May alias a live array — read-only, never
                                    // retain. HARDENS IN v5: update fixtures this round.
  commissionProgress: Record<string, number>;
  research?: ResearchState;         // v4: research progress (hardens in v6)
  wildlife?: ReadonlyArray<WildlifeState>; // v5: piles & swarms (M2's living hazards)
  relics?: ReadonlyArray<RelicState>;      // v6: buried secrets (the hidden pipe)
}

export interface RelicState {
  id: number;
  kind: 'optical-fossil';           // v6: the severed audio bus — one per world
  pos: Vec2;
  found: boolean;                   // excavated; persists through save/load
}

export interface WildlifeState {
  id: number;
  kind: 'dust-pile' | 'mosquito-swarm' | 'parity-mite'; // v8: The Correction walks
  pos: Vec2;
  size: number;                     // pile: accumulation 0..1; swarm: severity 0..1
  target?: number;                  // swarm: entity id it is currently degrading
}

export interface ResearchState {
  active: string | null;                 // TechDef.id being researched
  progress: Record<string, number>;      // packs delivered toward active (itemId -> n)
  unlocked: string[];                    // completed TechDef.ids
}
// v4 gating rule: a machine/recipe id that appears in ANY TechDef.unlocks is locked
// until that tech completes; ids referenced by no tech are always available.

export type Command =
  | { kind: 'place'; def: string; pos: Vec2; dir: Dir }
  | { kind: 'remove'; pos: Vec2 }
  | { kind: 'rotate'; pos: Vec2 }
  | { kind: 'setRecipe'; entity: number; recipe: string | null }
  | { kind: 'setRecipeAt'; pos: Vec2; recipe: string | null }   // v3: id-free variant
  | { kind: 'setResearch'; tech: string | null }                // v4: pick the lab target
  | { kind: 'grantResearch'; tech: string[] }                   // v5: instant unlock — the
                                                                // sandbox/demo verb, also a
                                                                // legitimate creative mode
  | { kind: 'excavate'; pos: Vec2 }                             // v6: dig at a relic site
  | { kind: 'setPaused'; paused: boolean };

export type SimEvent =
  | { kind: 'shipped'; item: string; count: number; tick: number }
  | { kind: 'crafted'; entity: number; recipe: string; tick: number }
  | { kind: 'jammed'; entity: number; reason: string; tick: number }
  | { kind: 'brownout'; on: boolean; tick: number }
  | { kind: 'placed'; entity: number; def: string; tick: number }
  | { kind: 'removed'; entity: number; def: string; tick: number }
  | { kind: 'commissionDone'; commission: string; tick: number }
  | { kind: 'scram'; entity: number; on: boolean; tick: number } // v2: heat shutdown edge
  | { kind: 'researched'; tech: string; tick: number }           // v4: research complete
  | { kind: 'wildlife'; on: 'spawned' | 'cleared'; wildlife: 'dust-pile' | 'mosquito-swarm' | 'parity-mite';
      id: number; pos: Vec2; tick: number }                      // v5: hazard lifecycle
  | { kind: 'relicFound'; relic: 'optical-fossil'; id: number; tick: number } // v6
  | { kind: 'repaired'; item: string; pos: Vec2; tick: number }; // v8: a mite corrected your work

// ---------------------------------------------------------------- module interfaces

export interface Sim {
  init(data: GameData, seed: number): void;
  enqueue(cmd: Command): void;
  tick(): void;                       // advance exactly one tick
  snapshot(): SimSnapshot;            // read-only view; renderer/ui must not mutate
  drainEvents(): SimEvent[];          // events since last drain (main loop fans out)
  save(): string;                     // v4: HARDENED — full state, deterministic round-trip
  load(json: string): void;           // v4: HARDENED — restore a save() blob
}

/** v4: the typed selection protocol — kills the '__remove' magic-string sentinel. */
export type SelectionState =
  | { mode: 'build'; def: string; dir: Dir }
  | { mode: 'remove' }
  | { mode: 'inspect'; entity: number }   // v5: open inspector — renderer highlights it
  | null;

export interface Renderer {
  init(container: HTMLElement, data: GameData): Promise<void>;
  render(snap: SimSnapshot, alpha: number): void;  // alpha: 0..1 interp between ticks
  pickTile(clientX: number, clientY: number): Vec2 | null;
  /** v4: mode drives the ghost visual — 'build' = placement preview, 'remove' =
   * demolition tint. def is null in remove mode. The '__remove' sentinel is deprecated
   * and main.ts stops sending it in v5. */
  setGhost(def: string | null, pos: Vec2 | null, dir: Dir, mode?: 'build' | 'remove'): void;
}

export interface UIBus {
  dispatch(cmd: Command): void;
  /** current build selection; renderer ghost + click-to-place read this.
   * DEPRECATED in v4 (still served): use selection()/setSelection(). */
  selectedBuild(): { def: string; dir: Dir } | null;
  /** v2: world picking, plumbed from the renderer — UI's click-to-inspect */
  pickTile(clientX: number, clientY: number): Vec2 | null;
  /** v4: typed selection. main.ts owns the state; UI writes via setSelection.
   * In remove mode main.ts dispatches the `remove` command on click — the UI must
   * NOT also dispatch it (that was the round-3 double-fire). */
  selection?(): SelectionState;
  setSelection?(sel: SelectionState): void;
}

export interface UI {
  init(root: HTMLElement, data: GameData, bus: UIBus): void;
  update(snap: SimSnapshot, events: SimEvent[]): void;
}

/** v6: THE SPEAKER — everything audible. All sound is synthesized (Web Audio), no
 * asset files. Pre-discovery: minimal diegetic UI sounds only. After the optical
 * fossil is found (snapshot.relics / relicFound), the full band unlocks: tuner,
 * stations, world ambience. Owned by LANE-SCREEN (src/audio/**). */
export interface AudioFX {
  init(data: GameData): void;
  /** call on first user gesture — browsers gate AudioContext on interaction */
  unlock(): void;
  onEvents(events: SimEvent[]): void;
  frame(timeMs: number, snap?: SimSnapshot): void;
}

export interface ScreenFX {
  init(canvas: HTMLCanvasElement, data: GameData): void;
  onEvents(events: SimEvent[]): void;  // shipped items feed the corruption
  frame(timeMs: number, snap?: SimSnapshot): void; // v3: read-only peek for continuous
                                                   // strain (stored, heat); never retain
}
