/**
 * Item -> artifact mapping. This is the design core of THE SCREEN: a player who ships
 * 40 chroma slurry must SEE chroma damage specifically, not "more glitch".
 *
 * Two independent quantities come out of the ledger:
 *   - MIX   (which artifacts)  = the weighted blend of what you ship
 *   - TOTAL (how much)         = the escalation curve over lifetime shipments
 * Shipping a lot of one thing bends the mix hard toward that item's signature.
 */
import type { GameData } from '../contracts';
import { PARAMS, type Param, type ParamSet, zeroParams } from './params';

export interface ItemCorruption {
  /** How much this item escalates overall corruption. Negative = sanitizes. */
  potency: number;
  /** Which artifacts this item argues for. Values 0..1, blended by ship share. */
  weights: Partial<Record<Param, number>>;
}

/**
 * Keyed by ItemDef.id (codex canon, see docs/FKTRY_LORE.md §2/§3). Items not listed
 * here still work — they get FALLBACK, a small generic noise bump — so LANE-DATA can
 * add content without touching this lane. Unrecognised ids are reported at init.
 */
const CORRUPTION: Record<string, ItemCorruption> = {
  // ---- raw (§2) : undifferentiated damage, cheap to ship, weak signature
  'mdat-ore': { potency: 0.4, weights: { noise: 0.5, burn: 0.25 } },
  'bayer-grit': { potency: 0.4, weights: { noise: 0.55, chroma: 0.25 } },
  'field-splinters': { potency: 0.5, weights: { tear: 0.6, roll: 0.45 } }, // interlace
  'silver-frames': { potency: 0.5, weights: { burn: 0.7 } },               // celluloid

  // ---- refined (§2)
  'luma-bars': { potency: 0.6, weights: { roll: 0.6, tear: 0.5 } },
  'chroma-slurry': { potency: 1.0, weights: { chroma: 1.0, burn: 0.12 } },
  // Subsampling grades: the more you throw away, the harder the colour bleeds.
  'chroma-422': { potency: 1.05, weights: { chroma: 1.0, melt: 0.15 } },
  'chroma-420': { potency: 1.1, weights: { chroma: 1.0, melt: 0.3 } },
  'color-plate': { potency: 0.7, weights: { chroma: 0.75 } },
  // DCT coefficients overdriven: quantization, and the grid it confesses to.
  'coefficient-pack': { potency: 0.7, weights: { block: 0.7, freeze: 0.4, noise: 0.2 } },
  'hf-dust': { potency: 0.8, weights: { noise: 0.9, tear: 0.2 } },
  'mv-flux': { potency: 1.0, weights: { mosh: 0.9, melt: 0.45 } },        // motion vectors
  'v-hold-roll': { potency: 0.7, weights: { roll: 1.0 } },

  // ---- frames (§2)
  // I-frames are CLEAN REFERENCE. Shipping anchors sanitizes THE SCREEN.
  // See NOTES: deliberate tension, flagged for the orchestrator.
  'anchor-slab': { potency: -2.0, weights: {} },
  'delta-wafer': { potency: 0.9, weights: { mosh: 0.85, melt: 0.3 } },    // P: prediction, no anchor
  'janus-wafer': { potency: 1.0, weights: { mosh: 0.6, freeze: 0.6, melt: 0.25 } }, // B: bidirectional
  'gop-crate': { potency: 1.0, weights: { freeze: 0.7, mosh: 0.45, roll: 0.25 } },

  // ---- glitch products (§3) : the signatures. These must read INSTANTLY.
  melt: { potency: 1.2, weights: { melt: 0.95, mosh: 0.55 } },
  'bloom-concentrate': { potency: 1.3, weights: { mosh: 0.9, melt: 0.6 } },
  // Round 2: these three now have artifact passes of their own. A pile of bricks must
  // read as a blocky grid, not as displacement; halos as ringing, not as static.
  'macroblock-bricks': { potency: 1.1, weights: { block: 1.0, mosh: 0.2 } },
  'ringing-halos': { potency: 0.9, weights: { ring: 1.0, noise: 0.15 } },
  'static-canister': { potency: 1.0, weights: { noise: 1.0 } },
  'moire-crystal': { potency: 1.1, weights: { moire: 1.0, chroma: 0.2 } },
  'false-color-speckle': { potency: 1.2, weights: { chroma: 1.0, noise: 0.3 } },
  'judder-cams': { potency: 1.1, weights: { freeze: 1.0 } },              // 3:2 pulldown
  'explosive-hits': { potency: 1.5, weights: { mosh: 0.8, burn: 0.6, chroma: 0.5 } },
  // Gamut paints: house-gray is nearly inert, laser primaries are weapons-grade.
  'gamut-paint-rec709': { potency: 0.3, weights: { chroma: 0.2 } },
  'gamut-paint-dci-p3': { potency: 0.8, weights: { chroma: 0.7 } },
  'gamut-paint-rec2020': { potency: 1.3, weights: { chroma: 1.0, burn: 0.5 } },

  // ---- wildlife (§5)
  'mosquito-swarm': { potency: 0.9, weights: { noise: 0.95 } },           // literal mosquito noise
  'gibbs-wraith': { potency: 1.0, weights: { ring: 0.85, noise: 0.25, freeze: 0.25 } },
};

const FALLBACK: ItemCorruption = { potency: 0.5, weights: { noise: 0.6 } };

export function corruptionFor(id: string): ItemCorruption {
  return CORRUPTION[id] ?? FALLBACK;
}

/** Items whose corruption profile is unmapped — logged at init so new data is visible. */
export function unmappedItems(data: GameData): string[] {
  return data.items.filter((i) => !(i.id in CORRUPTION)).map((i) => i.id);
}

/** Shipments needed to reach full corruption. Tuned by feel; log-ish, so early ships land hard. */
export const ESCALATION_ITEMS = 100;
const KNEE = 6; // lower = steeper early curve

/** Corruption units -> 0..1 total. log-shaped: the first handful of ships feel enormous. */
export function escalation(units: number): number {
  if (units <= 0) return 0;
  const t = Math.log1p(units / KNEE) / Math.log1p(ESCALATION_ITEMS / KNEE);
  return Math.min(1, t);
}

export interface LedgerResult {
  targets: ParamSet;
  /** net corruption units after anchor-slab sanitation */
  units: number;
  /** 0..1 escalation */
  total: number;
}

/**
 * Blend the per-item tally into target param pressures.
 * Mix uses only positive-potency items (you can't corrupt *toward* clean); anchors
 * pull down `units`, which scales everything back down through `total`.
 */
export function resolveLedger(tally: ReadonlyMap<string, number>): LedgerResult {
  const targets = zeroParams();
  let units = 0;
  let mixWeight = 0;

  for (const [id, count] of tally) {
    if (count <= 0) continue;
    units += count * corruptionFor(id).potency;
    const p = corruptionFor(id).potency;
    if (p > 0) mixWeight += count * p;
  }
  units = Math.max(0, units);
  const total = escalation(units);
  if (mixWeight <= 0) return { targets, units, total };

  for (const [id, count] of tally) {
    if (count <= 0) continue;
    const { potency, weights } = corruptionFor(id);
    if (potency <= 0) continue;
    const share = (count * potency) / mixWeight;
    for (const p of PARAMS) {
      targets[p] += (weights[p] ?? 0) * share;
    }
  }
  for (const p of PARAMS) targets[p] = Math.min(1, targets[p] * total);
  return { targets, units, total };
}
