import { createHash } from 'node:crypto';
import {
  constants, copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, statSync, unlinkSync,
} from 'node:fs';
import { join, dirname, extname } from 'node:path';

/**
 * 競合台帳（計画5 / spec §10.3）。
 *
 * 台帳は files ツリーの外 — スペース単位の `.conflict/file-versions.json` — に住む。
 * Phase A 以前は `logs/.file-versions.json` で、logs/** を除外する特別扱いが必要だった。
 * 台帳が files の外に出たので、その除外ロジックは撤廃した（logs 配下も普通に version 記録される）。
 *
 * アトミック性（同一プロセス内の複数エージェント並行を前提とした堅牢化）:
 * - 台帳書込は temp ファイル → `renameSync`（rename は同一 FS でアトミック）。
 *   read-modify-write 自体は同期実行（better-sqlite3 / tools は単一スレッド同期）なので、
 *   一回の呼び出し内では中断されない。temp+rename により、別エージェントの並行書込で
 *   半端な JSON を観測したり last-writer-wins でキーを取りこぼす窓を除去する。
 * - 旧版退避先は `copyFileSync(..., COPYFILE_EXCL)` で確保してから元ファイルを消す。
 *   これにより、二者が同じ base を同時解決しても old/ 内の既存ファイルを上書きしない。
 *
 * 残存制限（spec §10.3）: per-file の書込ロックは持たない。同一成果物への真の同時上書きは
 * last-writer-wins のまま。台帳のアトミック性とは別レイヤーの課題。
 */

function hash(buf: Buffer | string): string {
  return createHash('sha256').update(buf).digest('hex');
}

function ledgerPath(conflictDir: string): string {
  return join(conflictDir, 'file-versions.json');
}

function readLedger(conflictDir: string): Record<string, string> {
  try { return JSON.parse(readFileSync(ledgerPath(conflictDir), 'utf-8')); } catch { return {}; }
}

/** 台帳を temp ファイル経由でアトミックに書く（rename は同一 FS でアトミック）。 */
function writeLedger(conflictDir: string, led: Record<string, string>): void {
  mkdirSync(conflictDir, { recursive: true });
  const target = ledgerPath(conflictDir);
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
  writeFileSync(tmp, JSON.stringify(led), 'utf-8');
  renameSync(tmp, target);
}

/**
 * 現在のディスク版を台帳に記録する（Read / Edit / 正常 Write の後に呼ぶ）。
 * ベストエフォート: 記録に失敗しても **絶対に throw しない**。これはツールの
 * 副次的な簿記であり、対象が存在しない / ディレクトリ / 権限不足 / レース等で
 * readFileSync が EISDIR/ENOENT 等を投げても、ジョブをクラッシュさせてはならない
 * （未記録なら後続 Write が競合を検知できないだけで、安全側に縮退する）。
 */
export function recordVersion(ws: string, relPath: string, conflictDir: string): void {
  try {
    const abs = join(ws, relPath);
    // 通常ファイルのみ版管理する。ディレクトリ/特殊ファイルは readFileSync が
    // 投げる（EISDIR 等）ので明示的にスキップする。statSync は欠損で throw → catch。
    if (!statSync(abs).isFile()) return;
    const led = readLedger(conflictDir);
    led[relPath] = hash(readFileSync(abs));
    writeLedger(conflictDir, led);
  } catch {
    // best-effort: 簿記の失敗でツール/ジョブを落とさない。
  }
}

export interface WriteResolution {
  /** 実際に書き込むべき相対パス */
  targetRelPath: string;
  /** 競合時に退避した旧ファイルの相対パス */
  archivedRelPath?: string;
  conflict: boolean;
}

/**
 * 既存ファイルを同じ階層の old/ へ退避する。退避名は `{stem}_oldN{ext}`。
 * COPYFILE_EXCL により、既存の退避ファイルを上書きしない。
 */
function archiveExistingFile(ws: string, relPath: string): string {
  const ext = extname(relPath);
  const baseName = relPath.slice(relPath.lastIndexOf('/') + 1);
  const stem = baseName.slice(0, baseName.length - ext.length);
  const parent = dirname(relPath);
  const oldDir = parent === '.' ? 'old' : join(parent, 'old');
  const sourceAbs = join(ws, relPath);
  // 同一プロセス内の暴走に対する上限（実用上は到達しない）。
  for (let n = 1; n < 100000; n++) {
    const candidate = join(oldDir, `${stem}_old${n}${ext}`);
    const abs = join(ws, candidate);
    try {
      mkdirSync(dirname(abs), { recursive: true });
      copyFileSync(sourceAbs, abs, constants.COPYFILE_EXCL);
      unlinkSync(sourceAbs);
      return candidate;
    } catch (e) {
      if ((e as NodeJS.ErrnoException).code === 'EEXIST') continue;
      throw e;
    }
  }
  throw new Error('archiveExistingFile: exhausted candidate names');
}

/**
 * Write の衝突解決（spec §2.4）。
 * - 対象が存在しない → そのまま書く（競合なし）。
 * - 対象が存在し、ディスク版が台帳の記録と一致 → 普通に上書き（競合なし）。
 * - 対象が存在するが台帳に記録が無い、または記録と食い違う → 競合。
 *   既存ファイルを同じ階層の `old/{name}_oldN.{ext}` へ退避し、元パスへ新しい内容を書く。
 */
export function resolveWriteTarget(
  ws: string,
  relPath: string,
  _newContent: string,
  conflictDir: string,
): WriteResolution {
  const abs = join(ws, relPath);
  if (!existsSync(abs)) return { targetRelPath: relPath, conflict: false };
  const led = readLedger(conflictDir);
  const base = led[relPath];
  const diskHash = hash(readFileSync(abs));
  if (base && base === diskHash) return { targetRelPath: relPath, conflict: false };
  const archivedRelPath = archiveExistingFile(ws, relPath);
  return { targetRelPath: relPath, archivedRelPath, conflict: true };
}
