import { createHash, randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";

type StateFile = {
  version: 1;
  entries: Record<string, number>;
};

const EMPTY_STATE: StateFile = { version: 1, entries: {} };

export class ReplayStore {
  private queue: Promise<void> = Promise.resolve();

  constructor(private readonly filePath: string) {}

  async consume(namespace: string, value: string, expiresAt: number): Promise<boolean> {
    return this.runExclusive(async () => {
      const state = await this.readState();
      this.purgeExpired(state);
      const key = this.key(namespace, value);
      if (state.entries[key]) return false;
      state.entries[key] = expiresAt;
      await this.writeState(state);
      return true;
    });
  }

  async contains(namespace: string, value: string): Promise<boolean> {
    return this.runExclusive(async () => {
      const state = await this.readState();
      const changed = this.purgeExpired(state);
      if (changed) await this.writeState(state);
      return Boolean(state.entries[this.key(namespace, value)]);
    });
  }

  private key(namespace: string, value: string): string {
    const digest = createHash("sha256").update(value).digest("base64url");
    return `${namespace}:${digest}`;
  }

  private purgeExpired(state: StateFile): boolean {
    const now = Math.floor(Date.now() / 1000);
    let changed = false;
    for (const [key, expiresAt] of Object.entries(state.entries)) {
      if (!Number.isFinite(expiresAt) || expiresAt <= now) {
        delete state.entries[key];
        changed = true;
      }
    }
    return changed;
  }

  private async readState(): Promise<StateFile> {
    try {
      const raw = await fs.readFile(this.filePath, "utf8");
      const parsed = JSON.parse(raw) as Partial<StateFile>;
      if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== "object") {
        throw new Error("Unsupported state format");
      }
      return { version: 1, entries: { ...parsed.entries } };
    } catch (error) {
      const code = (error as NodeJS.ErrnoException).code;
      if (code === "ENOENT") return { ...EMPTY_STATE, entries: {} };
      throw error;
    }
  }

  private async writeState(state: StateFile): Promise<void> {
    const directory = path.dirname(this.filePath);
    await fs.mkdir(directory, { recursive: true, mode: 0o700 });
    const temporary = `${this.filePath}.${randomUUID()}.tmp`;
    await fs.writeFile(temporary, `${JSON.stringify(state)}\n`, { mode: 0o600 });
    await fs.rename(temporary, this.filePath);
  }

  private runExclusive<T>(operation: () => Promise<T>): Promise<T> {
    const result = this.queue.then(operation, operation);
    this.queue = result.then(
      () => undefined,
      () => undefined,
    );
    return result;
  }
}
