/**
 * Deploy sync — mirror the workspace into a configured output directory,
 * incremental by content hash.
 *
 * Two modes:
 *   'full' (DEFAULT)  — the portable project: src, tests, `.supergit`
 *                       (ledger/graph/spec/manifest/lessons), the `.git`
 *                       history with its trailer-linked commits, and the
 *                       debug I/O trace. This is what makes the folder
 *                       re-attachable by any later RHA session — an artifact
 *                       without its substrate is dead to the pipeline.
 *   'deployable'      — production push: code + frozen tests only, no
 *                       scaffolding, no git, no traces.
 *
 * `.supergit/lock` is never synced in either mode (a copied lockfile would
 * block attach at the destination).
 */
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { assertSafeRelPath } from './deployPath';

export type SyncMode = 'full' | 'deployable';

const ALWAYS_SKIP = new Set(['node_modules']);
const DEPLOYABLE_SKIP = new Set(['.git', '.supergit']);

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

function shouldSkip(rel: string, mode: SyncMode): boolean {
  const parts = rel.split(/[/\\]/);
  if (parts.some(p => ALWAYS_SKIP.has(p))) return true;
  if (rel === '.supergit/lock') return true; // never propagate a lock
  if (mode === 'deployable') {
    if (parts.some(p => DEPLOYABLE_SKIP.has(p))) return true;
    if (rel.endsWith('.rha-debug.jsonl')) return true;
  }
  return false;
}

function walk(dir: string, base = dir, out: string[] = []): string[] {
  if (!fs.existsSync(dir)) return out;
  for (const name of fs.readdirSync(dir)) {
    if (ALWAYS_SKIP.has(name)) continue;
    const abs = path.join(dir, name);
    const rel = path.relative(base, abs).replace(/\\/g, '/');
    const st = fs.statSync(abs);
    if (st.isDirectory()) walk(abs, base, out);
    else if (st.isFile()) out.push(rel);
  }
  return out;
}

export interface DeployFileEntry {
  path: string;
  sha256: string;
  size: number;
}

/** Files eligible for sync under the given mode, with content hashes. */
export function listDeployableFiles(
  workspaceDir: string,
  opts: { mode?: SyncMode; includeLedger?: boolean } = {},
): DeployFileEntry[] {
  // Back-compat: legacy includeLedger:true ≈ full; unspecified defaults to full.
  const mode: SyncMode = opts.mode ?? (opts.includeLedger === false ? 'deployable' : 'full');
  if (!fs.existsSync(workspaceDir)) return [];
  return walk(workspaceDir)
    .filter(rel => !shouldSkip(rel, mode))
    .filter(rel => {
      try {
        assertSafeRelPath(rel);
        return true;
      } catch {
        return false;
      }
    })
    .map(rel => {
      const buf = fs.readFileSync(path.join(workspaceDir, rel));
      return { path: rel, sha256: sha(buf), size: buf.length };
    });
}

export interface SyncResult {
  ok: boolean;
  mode: SyncMode;
  copied: string[];
  skipped: number;
  error?: string;
}

/** Mirror workspace → outputDir. Incremental by content hash. */
export function syncDeployable(
  workspaceDir: string,
  outputDir: string,
  opts: { mode?: SyncMode; includeLedger?: boolean } = {},
): SyncResult {
  const mode: SyncMode = opts.mode ?? (opts.includeLedger === false ? 'deployable' : 'full');
  if (!outputDir?.trim()) {
    return { ok: false, mode, copied: [], skipped: 0, error: 'outputDir not configured' };
  }
  const destRoot = path.resolve(outputDir.trim());
  if (destRoot === path.resolve(workspaceDir)) {
    return { ok: false, mode, copied: [], skipped: 0, error: 'outputDir must not equal the workspace' };
  }
  try {
    fs.mkdirSync(destRoot, { recursive: true });
  } catch (e) {
    return { ok: false, mode, copied: [], skipped: 0, error: e instanceof Error ? e.message : String(e) };
  }

  const files = listDeployableFiles(workspaceDir, { mode });
  const copied: string[] = [];
  let skipped = 0;

  for (const entry of files) {
    const src = path.join(workspaceDir, entry.path);
    const dst = path.join(destRoot, entry.path);
    const buf = fs.readFileSync(src);
    if (fs.existsSync(dst)) {
      try {
        if (sha(fs.readFileSync(dst)) === entry.sha256) {
          skipped++;
          continue;
        }
      } catch { /* rewrite */ }
    }
    fs.mkdirSync(path.dirname(dst), { recursive: true });
    fs.writeFileSync(dst, buf);
    copied.push(entry.path);
  }

  return { ok: true, mode, copied, skipped };
}
