import { closeSync, openSync, fsyncSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, existsSync } from 'node:fs';
import { dirname } from 'node:path';
import { logger } from '../../logger.js';

/**
 * Atomic JSON read/write for Phase 5 cross-workspace artifacts (handoff,
 * delta, absorbed-deltas log).
 *
 * Codex review reflection:
 *   - write: temp file → fsync → rename. Concurrent reads see either the
 *     old or the new full content, never a half-written byte stream.
 *   - read: parse failure / wrong version / missing required fields all
 *     produce a `SafeReadResult` with `kind: 'corrupt'` rather than
 *     throwing. Callers (memory absorb / handoff load) treat corrupt
 *     records as "skip this artifact" and let the movement continue.
 *   - quarantine: a corrupted absorbed-deltas log is moved to
 *     `<path>.corrupt.<ts>.json` so we don't lose forensic data, then
 *     replaced with a fresh log. Item-level dedupe (sourceDeltaId in
 *     individual entries) backstops re-merge protection if quarantine
 *     happens.
 */

export interface AtomicJsonSchema<T> {
  /** Numeric schema version. Mismatches → 'corrupt'. */
  expectedVersion: number;
  /** Required structural check after parse. Returns null on success, error string on failure. */
  validate: (parsed: unknown) => string | null;
  /** Final cast — only called when validate returned null. */
  cast: (parsed: unknown) => T;
}

export type SafeReadResult<T> =
  | { kind: 'ok'; value: T }
  | { kind: 'missing' }
  | { kind: 'corrupt'; reason: string };

export function writeAtomicJson(absolutePath: string, payload: unknown): void {
  const dir = dirname(absolutePath);
  mkdirSync(dir, { recursive: true });
  const tmpPath = `${absolutePath}.tmp.${process.pid}.${Date.now()}`;
  const json = JSON.stringify(payload, null, 2);
  writeFileSync(tmpPath, json, 'utf-8');
  // fsync the tmp file so the bytes are durable before rename.
  try {
    const fd = openSync(tmpPath, 'r');
    try {
      fsyncSync(fd);
    } finally {
      closeSync(fd);
    }
  } catch (err) {
    // fsync failure is non-fatal — proceed with rename and rely on rename
    // atomicity. Log so it's visible.
    logger.warn(`[atomic-json] fsync failed for ${tmpPath}: ${(err as Error).message}`);
  }
  renameSync(tmpPath, absolutePath);
}

export function readSafeJson<T>(absolutePath: string, schema: AtomicJsonSchema<T>): SafeReadResult<T> {
  if (!existsSync(absolutePath)) return { kind: 'missing' };

  let raw: string;
  try {
    raw = readFileSync(absolutePath, 'utf-8');
  } catch (err) {
    return { kind: 'corrupt', reason: `read failed: ${(err as Error).message}` };
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch (err) {
    return { kind: 'corrupt', reason: `JSON parse failed: ${(err as Error).message}` };
  }

  if (!parsed || typeof parsed !== 'object') {
    return { kind: 'corrupt', reason: 'top-level value is not an object' };
  }
  const versionField = (parsed as { version?: unknown }).version;
  if (versionField !== schema.expectedVersion) {
    return { kind: 'corrupt', reason: `version mismatch: expected ${schema.expectedVersion}, got ${String(versionField)}` };
  }

  const validationError = schema.validate(parsed);
  if (validationError !== null) {
    return { kind: 'corrupt', reason: validationError };
  }

  return { kind: 'ok', value: schema.cast(parsed) };
}

/**
 * Move a corrupt artifact aside so it isn't read again on the next run,
 * then return the quarantine path for logging. Used by absorb/handoff
 * loaders when `readSafeJson` returns 'corrupt'. Best-effort: failure
 * here is logged but not thrown — the caller already has a soft skip.
 */
export function quarantineCorruptFile(absolutePath: string): string | null {
  try {
    if (!existsSync(absolutePath)) return null;
    const ts = new Date().toISOString().replace(/[:.]/g, '-');
    const quarantinePath = `${absolutePath}.corrupt.${ts}.json`;
    renameSync(absolutePath, quarantinePath);
    return quarantinePath;
  } catch (err) {
    logger.warn(`[atomic-json] quarantine failed for ${absolutePath}: ${(err as Error).message}`);
    // Last-resort: try to delete so re-runs don't keep re-failing.
    try {
      unlinkSync(absolutePath);
    } catch { /* ignore */ }
    return null;
  }
}
