import {
  eventKey,
  type EventUid,
  type VersionedEvent,
} from "./versioned-read-model";

export type ChainCheckpoint = {
  chainId: number;
  nextBlock: bigint;
  stale: boolean;
  lastError?: string;
};

export type EventStoreSnapshot = {
  events: VersionedEvent[];
  checkpoints: ChainCheckpoint[];
};

/**
 * Persistence-neutral canonical event store.
 *
 * Production adapters must persist the same logical key and checkpoint
 * semantics transactionally. A replacement log updates canonical block
 * metadata; a removed log deletes the canonical row.
 */
export class CanonicalEventStore {
  private readonly events = new Map<string, VersionedEvent>();
  private readonly checkpoints = new Map<number, ChainCheckpoint>();

  apply(event: VersionedEvent): "inserted" | "updated" | "removed" | "noop" {
    const key = eventKey(event.uid);
    const current = this.events.get(key);

    if (event.removed) {
      if (!current) return "noop";
      this.events.delete(key);
      return "removed";
    }

    if (
      current &&
      current.blockHash === event.blockHash &&
      current.blockNumber === event.blockNumber &&
      JSON.stringify(current.args) === JSON.stringify(event.args)
    ) {
      return "noop";
    }

    this.events.set(key, structuredClone(event));
    return current ? "updated" : "inserted";
  }

  get(uid: EventUid): VersionedEvent | undefined {
    const value = this.events.get(eventKey(uid));
    return value ? structuredClone(value) : undefined;
  }

  all(): VersionedEvent[] {
    return [...this.events.values()]
      .map((event) => structuredClone(event))
      .sort((a, b) =>
        a.uid.chainId - b.uid.chainId ||
        Number(a.blockNumber - b.blockNumber) ||
        a.uid.logIndex - b.uid.logIndex
      );
  }

  checkpoint(chainId: number): ChainCheckpoint | undefined {
    const value = this.checkpoints.get(chainId);
    return value ? { ...value } : undefined;
  }

  markRangeComplete(chainId: number, throughBlock: bigint): void {
    this.checkpoints.set(chainId, {
      chainId,
      nextBlock: throughBlock + 1n,
      stale: false,
    });
  }

  markRpcFailure(chainId: number, error: unknown): void {
    const current = this.checkpoints.get(chainId);
    this.checkpoints.set(chainId, {
      chainId,
      nextBlock: current?.nextBlock ?? 0n,
      stale: true,
      lastError: error instanceof Error ? error.message : String(error),
    });
  }

  snapshot(): EventStoreSnapshot {
    return {
      events: this.all(),
      checkpoints: [...this.checkpoints.values()]
        .map((checkpoint) => ({ ...checkpoint }))
        .sort((a, b) => a.chainId - b.chainId),
    };
  }
}

export type LogRangeReader = (
  chainId: number,
  fromBlock: bigint,
  toBlock: bigint,
) => Promise<VersionedEvent[]>;

export async function backfillRange(
  store: CanonicalEventStore,
  read: LogRangeReader,
  chainId: number,
  fromBlock: bigint,
  toBlock: bigint,
): Promise<{ applied: number; failed: false } | { applied: 0; failed: true; error: string }> {
  try {
    const logs = await read(chainId, fromBlock, toBlock);
    for (const log of logs) store.apply(log);
    store.markRangeComplete(chainId, toBlock);
    return { applied: logs.length, failed: false };
  } catch (error) {
    store.markRpcFailure(chainId, error);
    return {
      applied: 0,
      failed: true,
      error: error instanceof Error ? error.message : String(error),
    };
  }
}
