// src/engine/reflection/piece-writer.ts
//
// Atomically writes a new YAML version of a user's custom piece, enforcing a
// cooldown gate so the same (userId, pieceName) pair cannot be rewritten more
// than twice within piece_edit_cooldown_hours.
//
// Path convention
// ───────────────
// The caller passes dataDir as "data/users" (the root for per-user folders).
// userPiecesDir(dataDir, userId) resolves to "{dataDir}/{userId}/pieces".
// silentFork uses the same convention (dataDir = the per-user folder root,
// typically `data/users`), so we can pass ctx.dataDir straight through.

import { writeFileSync, renameSync, mkdirSync, existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import { logger } from '../../logger.js';
import { userPiecesDir } from '../../user-folder/paths.js';
import { silentFork } from './silent-fork.js';
import type { Repository } from '../../db/repository.js';
import type { PieceCatalog } from '../piece-catalog.js';

// ── Types ────────────────────────────────────────────────────────────────────

export interface WritePieceArgs {
  userId: string;
  pieceName: string;
  /** 'builtin' triggers a silent fork before overwriting; 'custom' overwrites directly. */
  pieceSource: 'builtin' | 'custom';
  /** Complete YAML text to write as the new piece version. */
  newYaml: string;
  /** Snapshot ID from snapshot.ts (ties this edit to a specific reflection run). */
  snapshotId: string;
  /** Cooldown window in hours. Default: 24. */
  cooldownHours?: number;
}

export interface WritePieceContext {
  /** Root for per-user folders, e.g. "data/users". userPiecesDir resolves relative to this. */
  dataDir: string;
  /** Path to the built-in pieces directory, e.g. "pieces". */
  builtinDir: string;
}

export type WritePieceResult =
  | { written: true }
  | { written: false; reason: 'cooldown' };

// ── Main export ──────────────────────────────────────────────────────────────

/**
 * Writes newYaml to the user's custom piece file, subject to cooldown.
 *
 * 1. Cooldown gate: if ≥ 2 edits for (userId, pieceName) within the cooldown
 *    window, returns { written: false, reason: 'cooldown' }.
 * 2. If pieceSource === 'builtin', calls silentFork() to create the user's
 *    custom copy first (idempotent — no-op if it already exists).
 * 3. Atomically writes newYaml via a .tmp.{pid} temp file + rename.
 * 4. Records the edit in reflection_piece_edits via repo.recordPieceEdit.
 * 5. Invalidates the PieceCatalog cache for userId.
 */
export async function writePiece(
  args: WritePieceArgs,
  ctx: WritePieceContext,
  repo: Repository,
  catalog: PieceCatalog,
): Promise<WritePieceResult> {
  const {
    userId,
    pieceName,
    pieceSource,
    newYaml,
    snapshotId,
    cooldownHours = 24,
  } = args;

  const cooldownMs = cooldownHours * 3600 * 1000;

  // ── Cooldown gate ────────────────────────────────────────────────────────
  const recentEdits = repo.countRecentPieceEdits(userId, pieceName, cooldownMs);
  if (recentEdits >= 2) {
    logger.info(
      `[piece-writer] cooldown userId=${userId} piece=${pieceName} recentEdits=${recentEdits} cooldownHours=${cooldownHours}`
    );
    return { written: false, reason: 'cooldown' };
  }

  // ── Silent fork (builtin → custom) ──────────────────────────────────────
  if (pieceSource === 'builtin') {
    const forkResult = silentFork(ctx.builtinDir, ctx.dataDir, userId, pieceName);
    logger.debug(
      `[piece-writer] silentFork userId=${userId} piece=${pieceName} forked=${forkResult.forked} commit=${forkResult.commit ?? 'null'}`
    );
  }

  // ── Determine destination path ───────────────────────────────────────────
  const piecesDir = userPiecesDir(ctx.dataDir, userId);
  const dstPath = join(piecesDir, `${pieceName}.yaml`);
  mkdirSync(piecesDir, { recursive: true });

  // ── Atomic write via tmp + rename ────────────────────────────────────────
  const tmpPath = `${dstPath}.tmp.${process.pid}`;
  try {
    writeFileSync(tmpPath, newYaml, 'utf-8');
    renameSync(tmpPath, dstPath);
  } catch (err) {
    // Best-effort cleanup of the temp file on rename failure.
    try {
      if (existsSync(tmpPath)) unlinkSync(tmpPath);
    } catch { /* ignore */ }
    throw err;
  }

  logger.info(
    `[piece-writer] wrote piece userId=${userId} piece=${pieceName} snapshotId=${snapshotId} dst=${dstPath}`
  );

  // ── Record the edit ──────────────────────────────────────────────────────
  repo.recordPieceEdit(userId, pieceName, snapshotId);

  // ── Invalidate catalog ───────────────────────────────────────────────────
  catalog.invalidate(userId);

  return { written: true };
}
