/**
 * AtomicFile — switchboard file-native durability primitive (WP-02).
 *
 * Protocol: write unique `.tmp` → fsync → rename → (best-effort parent fsync).
 * No DB / broker. Crash mid-write leaves original intact + orphan tmp for recovery.
 */

import { randomBytes } from "node:crypto";
import { promises as fs } from "node:fs";
import { dirname, join } from "node:path";

import {
  atomicWriteJson,
  cleanupTmp,
  renameWithRetry,
} from "../../_internal/atomic-write.ts";

export class AtomicFileError extends Error {
  constructor(message: string, options?: { cause?: unknown }) {
    super(message, options);
    this.name = "AtomicFileError";
  }
}

function uniqueTmpPath(targetPath: string): string {
  return `${targetPath}.tmp.${randomBytes(8).toString("hex")}`;
}

export class AtomicFile {
  /** Atomically write UTF-8 text (JSON/Markdown/plain). */
  static async writeText(targetPath: string, body: string): Promise<void> {
    await fs.mkdir(dirname(targetPath), { recursive: true });
    const tmpPath = uniqueTmpPath(targetPath);
    let handle: import("node:fs/promises").FileHandle | null = null;
    try {
      handle = await fs.open(tmpPath, "w");
      await handle.writeFile(body, "utf-8");
      await handle.sync();
    } catch (err) {
      await cleanupTmp(tmpPath);
      throw new AtomicFileError(`atomic text write failed: ${targetPath}`, {
        cause: err,
      });
    } finally {
      if (handle) await handle.close().catch(() => undefined);
    }

    try {
      await renameWithRetry(tmpPath, targetPath);
    } catch (err) {
      await cleanupTmp(tmpPath);
      throw new AtomicFileError(`atomic rename failed: ${targetPath}`, {
        cause: err,
      });
    }

    if (process.platform !== "win32") {
      let dirHandle: import("node:fs/promises").FileHandle | null = null;
      try {
        dirHandle = await fs.open(dirname(targetPath), "r");
        await dirHandle.sync();
      } catch {
        /* best-effort */
      } finally {
        if (dirHandle) await dirHandle.close().catch(() => undefined);
      }
    }
  }

  /** Atomically write a JSON value (pretty-printed, UTF-8). */
  static async writeJson(targetPath: string, value: unknown): Promise<void> {
    const body = `${JSON.stringify(value, null, 2)}\n`;
    try {
      await atomicWriteJson(targetPath, body);
    } catch (err) {
      throw new AtomicFileError(`atomic json write failed: ${targetPath}`, {
        cause: err,
      });
    }
  }

  static async readText(targetPath: string): Promise<string> {
    try {
      return await fs.readFile(targetPath, "utf-8");
    } catch (err) {
      throw new AtomicFileError(`read failed: ${targetPath}`, { cause: err });
    }
  }

  static async readJson<T = unknown>(targetPath: string): Promise<T> {
    const raw = await AtomicFile.readText(targetPath);
    try {
      return JSON.parse(raw) as T;
    } catch (err) {
      throw new AtomicFileError(`json parse failed: ${targetPath}`, {
        cause: err,
      });
    }
  }

  static async exists(targetPath: string): Promise<boolean> {
    try {
      await fs.access(targetPath);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Crash recovery: remove orphan staging files (`*.tmp` / `*.tmp.<hex>`)
   * under `dir` without touching committed targets.
   */
  static async recoverOrphanTmp(
    dir: string,
  ): Promise<{ removed: string[]; keptTargets: number }> {
    let names: string[] = [];
    try {
      names = await fs.readdir(dir);
    } catch {
      return { removed: [], keptTargets: 0 };
    }
    const removed: string[] = [];
    let keptTargets = 0;
    for (const name of names) {
      const full = join(dir, name);
      if (/\.tmp(?:\.|$)/i.test(name) || name.endsWith(".tmp")) {
        await cleanupTmp(full);
        removed.push(name);
        continue;
      }
      keptTargets += 1;
    }
    return { removed, keptTargets };
  }
}
