/**
 * LANE-UI — the research model (pure; see research.test.ts).
 *
 * CONTRACTS v4 gating rule, verbatim: "a machine/recipe id that appears in ANY
 * TechDef.unlocks is locked until that tech completes; ids referenced by no tech are
 * always available."
 *
 * So a lock is a property of the *data*, not of the sim: the tree says what gates what,
 * and `ResearchState.unlocked` says what has been paid for. With no research state at all
 * (SIM hasn't shipped it yet) nothing is unlocked, which is the correct reading of the
 * rule rather than a failure — every gated machine shows its padlock.
 */
import type { GameData, ResearchState, TechDef } from '../contracts';

export const ERA_ORDER = ['reel', 'broadcast', 'disc', 'stream', 'fortress'] as const;
export type Era = (typeof ERA_ORDER)[number];

export interface TechIndex {
  /** id (machine or recipe) -> the tech that gates it. */
  gatedBy: Map<string, TechDef>;
  /** techs grouped by era, in era order; eras with no techs are omitted. */
  byEra: Array<{ era: string; techs: TechDef[] }>;
}

export function indexTech(data: GameData): TechIndex {
  const gatedBy = new Map<string, TechDef>();
  for (const t of data.tech) {
    for (const id of t.unlocks) {
      // First tech to claim an id wins; a second one would be a data bug, and picking
      // deterministically beats picking randomly.
      if (!gatedBy.has(id)) gatedBy.set(id, t);
    }
  }

  const groups = new Map<string, TechDef[]>();
  for (const t of data.tech) {
    const list = groups.get(t.era);
    if (list) list.push(t);
    else groups.set(t.era, [t]);
  }

  const byEra: TechIndex['byEra'] = [];
  for (const era of ERA_ORDER) {
    const techs = groups.get(era);
    if (techs) byEra.push({ era, techs });
  }
  // An era DATA invents that we've never heard of still gets shown, just last.
  for (const [era, techs] of groups) {
    if (!(ERA_ORDER as readonly string[]).includes(era)) byEra.push({ era, techs });
  }
  return { gatedBy, byEra };
}

export function isUnlocked(index: TechIndex, research: ResearchState | undefined, id: string): boolean {
  const gate = index.gatedBy.get(id);
  if (!gate) return true; // gated by nothing — always available
  return !!research?.unlocked.includes(gate.id);
}

/** The tech a locked id is waiting on, or null if it isn't locked. */
export function gateFor(
  index: TechIndex,
  research: ResearchState | undefined,
  id: string,
): TechDef | null {
  const gate = index.gatedBy.get(id);
  if (!gate) return null;
  return research?.unlocked.includes(gate.id) ? null : gate;
}

export type TechStatus = 'done' | 'active' | 'available';

export function techStatus(research: ResearchState | undefined, tech: TechDef): TechStatus {
  if (research?.unlocked.includes(tech.id)) return 'done';
  if (research?.active === tech.id) return 'active';
  return 'available';
}

/** 0..1 across every pack the tech costs; 1 when done. */
export function techProgress(research: ResearchState | undefined, tech: TechDef): number {
  if (research?.unlocked.includes(tech.id)) return 1;
  if (!research || research.active !== tech.id) return 0;

  let need = 0;
  let have = 0;
  for (const [item, n] of Object.entries(tech.cost)) {
    need += n;
    have += Math.min(n, research.progress[item] ?? 0);
  }
  return need > 0 ? have / need : 0;
}
