/**
 * Regeneration safety for ReactAppStarter
 *
 * When realize runs Factory B, generated files can end up in two states:
 *
 *  1. Pristine — the file on disk is byte-identical to what we last
 *     wrote. Safe to overwrite with a new version.
 *
 *  2. User-edited — the user modified the file. We must NOT clobber
 *     their work.
 *
 * The mechanism is an SHA-256 hash recorded per file in
 * `.specverse-gen/hashes.json` at write time. At regeneration, we
 * compare the on-disk hash against the recorded one. Mismatch → skip
 * with a warning. Match → overwrite and update the hash.
 *
 * The factory orchestrator wraps realize's write calls through
 * `reconcileWrites` below, which handles all three outcomes:
 *  - brand-new file → write, record hash
 *  - pristine existing → overwrite, update hash
 *  - user-edited existing → skip, leave hash unchanged
 */

import { createHash } from 'crypto';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, relative } from 'path';

/** Relative path (from project root) → SHA-256 hex digest. */
export type HashManifest = Record<string, string>;

export const HASHES_DIR = '.specverse-gen';
export const HASHES_FILE = 'hashes.json';

export function sha256(s: string): string {
  return createHash('sha256').update(s, 'utf8').digest('hex');
}

/**
 * Load a `.specverse-gen/hashes.json` from the project root. Returns
 * an empty manifest if the file is missing or malformed — the
 * calling convention is that a missing manifest means "no record of
 * prior generation, treat everything as new."
 */
export function loadHashManifest(projectRoot: string): HashManifest {
  const path = join(projectRoot, HASHES_DIR, HASHES_FILE);
  if (!existsSync(path)) return {};
  try {
    const raw = readFileSync(path, 'utf8');
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
      const out: HashManifest = {};
      for (const [k, v] of Object.entries(parsed)) {
        if (typeof v === 'string') out[k] = v;
      }
      return out;
    }
  } catch {
    // Malformed — treat as missing. Next generate pass will overwrite.
  }
  return {};
}

/** Write the manifest back to `.specverse-gen/hashes.json`. */
export function saveHashManifest(projectRoot: string, manifest: HashManifest): void {
  const dir = join(projectRoot, HASHES_DIR);
  mkdirSync(dir, { recursive: true });
  const path = join(dir, HASHES_FILE);
  writeFileSync(path, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
}

export interface ReconcileResult {
  /** Relative path → content for every file approved for writing. */
  approvedWrites: Record<string, string>;
  /** Files we skipped because the user edited them (or couldn't confirm origin). */
  skipped: { path: string; reason: string }[];
  /** The updated hash manifest reflecting all approved writes. */
  manifest: HashManifest;
}

/**
 * Pure-planning triage. For each proposed file, decides whether the
 * write is safe:
 *  - Path doesn't exist → APPROVE, record hash.
 *  - Path exists + on-disk hash matches recorded hash → APPROVE
 *    (pristine overwrite), update hash.
 *  - Path exists + hash mismatch → SKIP (user-edited), preserve old
 *    hash record.
 *  - Path exists + no recorded hash → SKIP (cautious default).
 *
 * Reads the filesystem but doesn't write. Returns the approved writes
 * as a map the caller can pass to the realize write pipeline.
 */
export function reconcileWrites(
  projectRoot: string,
  proposed: Record<string, string>,
  prevManifest: HashManifest
): ReconcileResult {
  const manifest: HashManifest = { ...prevManifest };
  const approvedWrites: Record<string, string> = {};
  const skipped: { path: string; reason: string }[] = [];

  for (const [relPath, content] of Object.entries(proposed)) {
    const abs = join(projectRoot, relPath);
    const newHash = sha256(content);

    if (!existsSync(abs)) {
      approvedWrites[relPath] = content;
      manifest[relPath] = newHash;
      continue;
    }

    const currentContent = readFileSync(abs, 'utf8');
    const currentHash = sha256(currentContent);
    const recordedHash = prevManifest[relPath];

    if (recordedHash == null) {
      skipped.push({
        path: relPath,
        reason: 'no prior hash recorded — cannot confirm this file was generated by us',
      });
      continue;
    }

    if (currentHash === recordedHash) {
      approvedWrites[relPath] = content;
      manifest[relPath] = newHash;
      continue;
    }

    skipped.push({
      path: relPath,
      reason: 'file has been edited since last generation',
    });
  }

  return { approvedWrites, skipped, manifest };
}

/**
 * Human-readable summary of a ReconcileResult. Used by the factory
 * orchestrator to log after a run.
 */
export function summarize(result: ReconcileResult, projectRoot: string): string {
  const lines: string[] = [];
  const writeCount = Object.keys(result.approvedWrites).length;
  lines.push(`[ReactAppStarter] Approved ${writeCount} file(s) for writing.`);
  if (result.skipped.length > 0) {
    lines.push(`[ReactAppStarter] Skipped ${result.skipped.length} user-edited file(s):`);
    for (const { path, reason } of result.skipped) {
      lines.push(`  - ${path} (${reason})`);
    }
    lines.push(
      `To accept upstream regeneration for a skipped file, delete it (\`rm ${relative(process.cwd(), projectRoot)}/PATH\`) and re-run \`spv realize\`.`
    );
  }
  return lines.join('\n');
}
