import { arenaChecksum } from "./checksum.ts";
import { ArenaRandom, deriveArenaSeed, normalizeArenaSeed } from "./random.ts";
import type { ArenaScenario, ArenaScenarioRequest, ArenaSplit } from "./types.ts";

export interface ArenaScenarioDefinition<P extends object> {
  id: string;
  split: ArenaSplit;
  parameters: P;
}

export type ScenarioSampler<P extends object> = (parameters: Readonly<P>, random: ArenaRandom) => P;

/** Immutable public scenario catalogue with seeded, reproducible materialization. */
export class ArenaScenarioRegistry<P extends object> {
  readonly envId: string;
  readonly definitions: readonly ArenaScenarioDefinition<P>[];
  private readonly byId = new Map<string, ArenaScenarioDefinition<P>>();
  private readonly sampler: ScenarioSampler<P>;

  constructor(
    envId: string,
    definitions: readonly ArenaScenarioDefinition<P>[],
    sampler: ScenarioSampler<P>,
  ) {
    if (definitions.length === 0) throw new RangeError("arena scenario registry cannot be empty");
    this.envId = envId;
    this.definitions = definitions.map((definition) => ({
      ...definition,
      parameters: structuredClone(definition.parameters),
    }));
    this.sampler = sampler;
    for (const definition of this.definitions) {
      if (!definition.id || this.byId.has(definition.id)) {
        throw new RangeError(`duplicate or empty scenario id: ${definition.id}`);
      }
      this.byId.set(definition.id, definition);
    }
    for (const split of ["train", "dev"] as const) {
      if (!this.definitions.some((definition) => definition.split === split)) {
        throw new RangeError(`${envId} must expose at least one ${split} scenario`);
      }
    }
  }

  ids(split: ArenaSplit): readonly string[] {
    return this.definitions
      .filter((definition) => definition.split === split)
      .map((definition) => definition.id);
  }

  resolve(seedValue: number, request: string | ArenaScenarioRequest): ArenaScenario<P> {
    const seed = normalizeArenaSeed(seedValue);
    let definition: ArenaScenarioDefinition<P> | undefined;
    if (typeof request === "string") {
      definition = this.byId.get(request);
    } else if (request.id !== undefined) {
      definition = this.byId.get(request.id);
      if (definition?.split !== request.split) definition = undefined;
    } else {
      const candidates = this.definitions.filter((entry) => entry.split === request.split);
      definition = candidates[seed % candidates.length];
    }
    if (!definition) throw new RangeError(`unknown ${this.envId} scenario`);
    const random = new ArenaRandom(deriveArenaSeed(seed, `${this.envId}:${definition.id}`));
    const parameters = this.sampler(definition.parameters, random);
    const hash = arenaChecksum({
      envId: this.envId,
      id: definition.id,
      split: definition.split,
      seed,
      parameters,
    });
    return { id: definition.id, split: definition.split, seed, parameters, hash };
  }
}
