/** Deterministic mulberry32. Same seed → same trace, always. */
export class Rng {
  private state: number;

  constructor(seed: number) {
    this.state = seed >>> 0;
  }

  next(): number {
    this.state = (this.state + 0x6d2b79f5) >>> 0;
    let t = this.state;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  }

  int(min: number, max: number): number {
    return min + Math.floor(this.next() * (max - min + 1));
  }

  pick<T>(items: readonly T[]): T {
    return items[this.int(0, items.length - 1)]!;
  }

  chance(p: number): boolean {
    return this.next() < p;
  }

  shuffle<T>(items: T[]): T[] {
    const copy = [...items];
    for (let i = copy.length - 1; i > 0; i--) {
      const j = this.int(0, i);
      [copy[i], copy[j]] = [copy[j]!, copy[i]!];
    }
    return copy;
  }
}

/** Fake clock — the only time the harness is allowed to consult. */
export class Clock {
  nowMs = 0;

  advance(ms: number): void {
    this.nowMs += ms;
  }

  iso(): string {
    return new Date(this.nowMs).toISOString();
  }
}

export interface FaultConfig {
  dropRate: number;
  duplicateRate: number;
  reorder: boolean;
  maxDelayMs: number;
  crashBeforeCommitRate: number;
  crashAfterCommitRate: number;
  httpFailRate: number;
}

export const NO_FAULTS: FaultConfig = {
  dropRate: 0,
  duplicateRate: 0,
  reorder: false,
  maxDelayMs: 0,
  crashBeforeCommitRate: 0,
  crashAfterCommitRate: 0,
  httpFailRate: 0,
};

export function faultsFromRng(rng: Rng): FaultConfig {
  return {
    dropRate: rng.next() * 0.25,
    duplicateRate: rng.next() * 0.15,
    reorder: rng.chance(0.5),
    maxDelayMs: rng.int(0, 40),
    crashBeforeCommitRate: rng.next() * 0.08,
    crashAfterCommitRate: rng.next() * 0.1,
    httpFailRate: rng.next() * 0.08,
  };
}

export type LiveEvent =
  | { type: "message:created"; messageId: number }
  | { type: "message:edited"; messageId: number }
  | { type: "message:deleted"; messageId: number; seq: number }
  | { type: "reaction:changed"; messageId: number }
  | { type: "presence:changed"; userId: number; status: "active" | "offline" };

interface Envelope {
  toUserId: number;
  event: LiveEvent;
  deliverAt: number;
}

/**
 * Scriptable transport (§6). Not Socket.IO — a queue the world drains
 * against the fake clock, so every run is replayable from a seed.
 */
export class Transport {
  private pending: Envelope[] = [];
  /** Users currently partitioned (cannot receive). */
  readonly partitioned = new Set<number>();

  constructor(
    private readonly clock: Clock,
    private readonly rng: Rng,
    private readonly faults: FaultConfig,
  ) {}

  broadcast(memberIds: readonly number[], event: LiveEvent): void {
    for (const userId of memberIds) this.sendTo(userId, event);
  }

  sendTo(userId: number, event: LiveEvent): void {
    if (this.rng.chance(this.faults.dropRate)) return;
    const copies = this.rng.chance(this.faults.duplicateRate) ? 2 : 1;
    for (let i = 0; i < copies; i++) {
      this.pending.push({
        toUserId: userId,
        event,
        deliverAt: this.clock.nowMs + this.rng.int(0, this.faults.maxDelayMs),
      });
    }
  }

  deliverDue(): Envelope[] {
    const due = this.pending.filter(
      (e) => e.deliverAt <= this.clock.nowMs && !this.partitioned.has(e.toUserId),
    );
    const keep = this.pending.filter((e) => !due.includes(e));
    this.pending = keep;
    return this.faults.reorder ? this.rng.shuffle(due) : due;
  }

  remaining(): number {
    return this.pending.length;
  }

  /** Deliver everything not blocked by a partition, jumping the clock if needed. */
  flushUnpartitioned(): Envelope[] {
    const maxAt = this.pending.reduce((m, e) => Math.max(m, e.deliverAt), this.clock.nowMs);
    this.clock.nowMs = maxAt;
    return this.deliverDue();
  }
}

export class CommitCrash extends Error {
  constructor() {
    super("injected crash before commit");
    this.name = "CommitCrash";
  }
}

export class EmitCrash extends Error {
  constructor() {
    super("injected crash after commit, before emit");
    this.name = "EmitCrash";
  }
}

export class HttpFault extends Error {
  constructor() {
    super("injected HTTP failure");
    this.name = "HttpFault";
  }
}
