import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { dirname } from "node:path";

interface FailureEntry {
  attempts: number;
  nextAttemptAt: number;
  lastError: string;
}

export class FailureStore {
  private entries: Record<string, FailureEntry> = {};
  private saveQueue: Promise<void> = Promise.resolve();

  constructor(private readonly filePath: string) {}

  async load(): Promise<void> {
    try {
      this.entries = JSON.parse(await readFile(this.filePath, "utf8")) as Record<string, FailureEntry>;
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
    }
  }

  canRun(key: string, now = Date.now()): boolean {
    return !this.entries[key] || this.entries[key].nextAttemptAt <= now;
  }

  async recordFailure(key: string, message: string, retryable: boolean): Promise<void> {
    const attempts = (this.entries[key]?.attempts ?? 0) + 1;
    const delay = retryable
      ? Math.min(6 * 60 * 60_000, 60_000 * 2 ** Math.min(attempts - 1, 8))
      : 24 * 60 * 60_000;
    this.entries[key] = { attempts, nextAttemptAt: Date.now() + delay, lastError: message.slice(0, 4000) };
    await this.enqueueSave();
  }

  async clear(key: string): Promise<void> {
    if (!this.entries[key]) return;
    delete this.entries[key];
    await this.enqueueSave();
  }

  private enqueueSave(): Promise<void> {
    const contents = JSON.stringify(this.entries);
    const queued = this.saveQueue.then(() => this.save(contents));
    this.saveQueue = queued.catch(() => undefined);
    return queued;
  }

  private async save(contents: string): Promise<void> {
    await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
    const temporaryPath = `${this.filePath}.tmp`;
    await writeFile(temporaryPath, contents, { mode: 0o600 });
    await rename(temporaryPath, this.filePath);
  }
}
