/**
 * LANE-RENDER — dev-only mock snapshot. NOT part of the game.
 *
 * LANE-SIM builds in parallel and hasn't landed heat, splitters or `BeltItem.id` yet,
 * so there is no live source for most of what this lane renders. This fabricates a
 * factory that exercises every visual path — both corner handednesses, a merge, a heat
 * ramp through throttle into scram, a filling buffer, stable cargo ids — purely so the
 * renderer can be checked with real eyes before the sim catches up.
 *
 * Gated behind `?devscene` AND `import.meta.env.DEV`, and only used while the real sim
 * has no entities: the moment LANE-SIM produces anything, this yields automatically.
 * It never fabricates over live sim data.
 *
 * Cargo advances along the belt CHAIN (handing off between belt entities) rather than
 * by wrapping `t` on one belt, because that's what the real sim does — and it's what
 * exercises the renderer's cross-belt interpolation and corner pathing honestly.
 */
import {
  TICKS_PER_SECOND,
  type Dir, type EntityState, type GameData, type RelicState, type SimSnapshot,
  type WildlifeState,
} from '../contracts';

/**
 * Dev builds only. The `import.meta.env.DEV` guard is load-bearing, not decoration:
 * without it `?devscene` would fabricate a fake factory in a shipped build and expose
 * the renderer internals to anyone with a query string. It also lets Vite drop this
 * whole module from the production bundle.
 */
export const devSceneEnabled = (): boolean =>
  import.meta.env.DEV && new URLSearchParams(location.search).has('devscene');

/** `?devscene=stress` builds the standing-rule load: 500 entities + 2,000 belt items. */
export const devSceneStress = (): boolean =>
  import.meta.env.DEV && new URLSearchParams(location.search).get('devscene') === 'stress';

interface Segment {
  ids: number[]; // belt entity ids, in travel order
  item: string;
  count: number;
  tag: number; // id namespace so cargo ids stay stable and unique
}

export class DevScene {
  private entities: EntityState[] = [];
  private segments: Segment[] = [];
  private speed = 2;
  private ok = false;
  private nextId = 1;
  /** The mock's own scram latch — see snapshot(). */
  private scrammed = false;

  constructor(private data: GameData, private stress = false) {
    this.build();
  }

  get usable(): boolean {
    return this.ok;
  }

  private has(id: string): boolean {
    return this.data.machines.some((m) => m.id === id);
  }

  private machine(def: string, x: number, y: number, dir: Dir = 1, recipe: string | null = null): number {
    const id = this.nextId++;
    this.entities.push({
      id, def, pos: { x, y }, dir, recipe,
      progress: 0, inputBuf: {}, outputBuf: {}, jammed: null, heat: 0,
    });
    return id;
  }

  /** A run of belts along explicit tiles; returns their ids in travel order. */
  private belts(tiles: Array<[number, number, Dir]>): number[] {
    return tiles.map(([x, y, d]) => this.machine('belt', x, y, d));
  }

  private segment(ids: number[], item: string, count: number): void {
    if (this.data.items.some((i) => i.id === item)) {
      this.segments.push({ ids, item, count, tag: this.segments.length + 1 });
    }
  }

  private build(): void {
    if (!this.has('belt')) return;
    this.speed = this.data.machines.find((m) => m.id === 'belt')?.beltSpeed ?? 2;

    if (this.stress) {
      const items = ['mdat-ore', 'luma-bars', 'coefficient-pack', 'melt'];
      const machines = ['quantizer', 'demuxer', 'mosh-reactor', 'decode-asic'].filter((m) => this.has(m));
      for (let r = 0; r < 20; r++) {
        const y = -28 + r * 3;
        const tiles: Array<[number, number, Dir]> = [];
        for (let x = -30; x <= -7; x++) tiles.push([x, y, 1]); // 24 belts x 20 = 480
        this.segment(this.belts(tiles), items[r % items.length], 100); // 2,000 items
        if (machines.length) this.machine(machines[r % machines.length], -5, y, 1); // +20 = 500
      }
      // The round-5 perf bar: floor + 50 piles + 10 swarms on top of 500 entities.
      const ents = this.entities.filter((e) => e.def !== 'belt');
      for (let i = 0; i < 50; i++) {
        this.wildlife.push({
          id: 1000 + i, kind: 'dust-pile',
          pos: { x: -30 + (i % 10) * 3, y: -26 + Math.floor(i / 10) * 12 },
          size: ((i * 37) % 100) / 100,
        });
      }
      for (let i = 0; i < 10; i++) {
        const host = ents[i % Math.max(1, ents.length)];
        this.wildlife.push({
          id: 2000 + i, kind: 'mosquito-swarm',
          pos: host ? { x: host.pos.x, y: host.pos.y } : { x: -5, y: -20 + i * 5 },
          size: 0.5 + (i % 5) * 0.1,
          target: host?.id,
        });
      }
      this.ok = true;
      return;
    }

    // --- the M1 line, bent so cargo rides BOTH corner handednesses -----------------
    if (this.has('seam-extractor')) this.machine('seam-extractor', -14, 0, 1, 'extract-mdat');

    // Main run east along y=0. The belt at x=-6 is fed from behind AND from the spur
    // above it, so topology should classify it a merge.
    const main: Array<[number, number, Dir]> = [];
    for (let x = -12; x <= -3; x++) main.push([x, 0, 1]);
    this.segment(this.belts(main), 'mdat-ore', 8);

    // Spur dropping south into the main line -> forces the merge piece.
    this.segment(this.belts([[-6, -3, 2], [-6, -2, 2], [-6, -1, 2]]), 'chroma-slurry', 3);

    // One chain: corner right (fed from W, turns S), south run, corner left (fed from
    // N, turns E), east run. Cargo sweeps both arcs on the way to the shipper.
    const bend: Array<[number, number, Dir]> = [
      [-2, 0, 2], // cornerA — fed from local +x
      [-2, 1, 2], [-2, 2, 2], [-2, 3, 2], [-2, 4, 2],
      [-2, 5, 1], // cornerB — fed from local -x
      [-1, 5, 1], [0, 5, 1], [1, 5, 1], [2, 5, 1],
    ];
    this.segment(this.belts(bend), 'melt', 6);
    if (this.has('shipper')) this.machine('shipper', 3, 5, 1);

    // Showroom row for the round-2 machine visuals, clear of the belt path.
    if (this.has('lane-splitter')) this.machine('lane-splitter', -12, 8, 1);
    if (this.has('buffer-tank')) this.machine('buffer-tank', -8, 8, 1);
    if (this.has('software-decoder')) this.machine('software-decoder', -4, 8, 1);
    if (this.has('quantizer')) this.machine('quantizer', 0, 8, 1, 'quantize');
    if (this.has('mosh-reactor')) this.machine('mosh-reactor', 4, 8, 1, 'mosh');
    // v4/v5 kinds, so inspect-aura and the lab silhouette are reachable in the scaffold.
    if (this.has('asic-cooler')) this.machine('asic-cooler', 9, 8, 1);
    if (this.has('archaeology-lab')) this.machine('archaeology-lab', 13, 8, 1);

    // A few hazards so wildlife is visible without the stress rig: a growing dust drift
    // beside the quantizer (which is what sheds HF dust, §2) and a swarm on the demuxer.
    const demuxer = this.entities.find((e) => e.def === 'demuxer');
    this.wildlife.push(
      { id: 900, kind: 'dust-pile', pos: { x: 1, y: 11 }, size: 0.35 },
      { id: 901, kind: 'dust-pile', pos: { x: 2, y: 12 }, size: 0.8 },
      { id: 902, kind: 'dust-pile', pos: { x: 0, y: 12 }, size: 0.15 },
      { id: 910, kind: 'mosquito-swarm', pos: { x: -8, y: 0 }, size: 0.7, target: demuxer?.id },
    );

    this.ok = true;
  }

  snapshot(tick: number): SimSnapshot {
    // Heat ramp: normal -> throttled (>0.7) -> scram (1.0) -> restart, on a ~10s loop
    // so all three states are reachable in a short verification session.
    const heat = Math.min(1, (tick % 300) / 200);
    const brownout = Math.floor(tick / 300) % 4 === 3;
    // The mock owns its own latch, the way the real sim owns its own: scram at 1.0 and
    // stay dead while cooling. The renderer must read `scrammed` and never infer it —
    // that's the whole point of the v3 field.
    if (heat >= 1) this.scrammed = true;
    else if (heat < 0.5) this.scrammed = false;

    for (const e of this.entities) {
      if (e.def === 'software-decoder') {
        e.heat = heat;
        e.scrammed = this.scrammed;
      }
      if (e.def === 'demuxer') e.jammed = Math.floor(tick / 450) % 3 === 2 ? 'output full: LUMA BARS' : null;
      if (e.recipe) e.progress = (tick % 90) / 90;
    }

    const beltItems: SimSnapshot['beltItems'] = this.segments.flatMap((seg) => {
      const perTick = this.speed / TICKS_PER_SECOND;
      const span = seg.ids.length;
      return Array.from({ length: seg.count }, (_, k) => {
        const p = (tick * perTick + (k * span) / seg.count) % span;
        const idx = Math.min(span - 1, Math.floor(p));
        // Stable per-item id (contracts v2): identity survives the belt-to-belt handoff,
        // which is exactly what exact interpolation needs.
        return { item: seg.item, entity: seg.ids[idx], t: p - idx, id: seg.tag * 1000 + k };
      });
    });

    return {
      tick,
      paused: false,
      entities: this.entities,
      beltItems,
      // Oscillate the pool so buffer tanks visibly fill and drain.
      bandwidth: {
        gen: 20,
        draw: brownout ? 26 : 14,
        stored: (0.5 + 0.5 * Math.sin(tick / 60)) * 100,
        capacity: 100,
        brownout,
      },
      shippedTotal: { melt: Math.floor(tick / 90) },
      activeCommission: null,
      commissionProgress: {},
      // v4: nothing researched, so every tech-gated machine reads locked — which is what
      // the ghost's padlock is for. `unlocked` is writable from the console to check the
      // other half (see NOTES).
      research: this.research,
      // v3 hardened: the queue is active-first, then upcoming.
      commissionQueue: this.commissionQueue,
      // v5/v6: hazards and secrets. Mutable so a verification session can grow a pile,
      // aim a swarm, or excavate the relic from the console.
      wildlife: this.wildlife,
      relics: this.relics,
    };
  }

  /** Mutable so a verification session can unlock techs and watch padlocks clear. */
  readonly research = { active: null as string | null, progress: {}, unlocked: [] as string[] };
  /** v3-hardened field — present so fixtures match the contract, not just the renderer. */
  readonly commissionQueue: string[] = [];
  /** v5 hazards. `?devscene=stress` fills these to the perf bar (50 piles + 10 swarms). */
  readonly wildlife: WildlifeState[] = [];
  /** v6 secret: one buried optical fossil, unfound. Flip `found` to see the monument. */
  readonly relics: RelicState[] = [{ id: 1, kind: 'optical-fossil', pos: { x: 9, y: -7 }, found: false }];
}
