const fs = require('fs');
const path = require('path');
const { parseMarkdown, mergeOverlay, computeBaseFileSha } = require('./overlay-merger');
const GitUtils = require('./git');
const { buildFrontmatter } = require('../commands/overlay');

/**
 * Auto-capture + verify for the seamless `baldart update` flow (v4.8.0+).
 *
 * When the consumer has divergent edits to a framework file that are NOT yet
 * preserved by an overlay, the seamless update would lose them on `--reset`.
 * Rather than dead-end the user (the old behaviour), this module CAPTURES the
 * edit into a populated overlay (section-marker model) and then VERIFIES that
 * re-merging that overlay onto the FRESH upstream base reproduces the edit
 * exactly at the section level. The verification is the safety net: if the
 * capture cannot be reproduced faithfully (a section deletion, a frontmatter /
 * preamble change, a mid-file insertion, a skill's runtime-concat model), the
 * file is reported as a BLOCKER and the seamless flow stops once instead of
 * silently mangling the customization.
 *
 * Only agents/commands are auto-capturable — they use the deterministic
 * section-merge engine in overlay-merger.js, so the merge can be replayed and
 * checked. Skills use the runtime-concat model (the CLI never merges them), so
 * they are always blockers here.
 *
 * Pure-ish: writes overlay files only for VERIFIED captures; never throws.
 */

// Consumer `.framework/<repo-content>` path → path inside the BALDART repo
// (i.e. inside FETCH_HEAD). The subtree prefix is `.framework`, so stripping it
// yields the upstream path: `framework/.claude/agents/x.md`.
function toRepoPath(p) {
  return p.replace(/^\.framework\//, '');
}

// { kind, name } from a touched `.framework/.../.claude/(agents|commands|skills)/…`
// path, or null when the path isn't an overlay-able primitive.
function kindNameFromPath(p) {
  const m = /(?:^|\/)\.framework\/framework\/\.claude\/(agents|commands|skills)\/(.+)$/.exec(p);
  if (!m) return null;
  const [, dir, rest] = m;
  if (dir === 'skills') {
    const name = rest.split('/')[0];
    return name ? { kind: 'skill', name } : null;
  }
  const name = rest.replace(/\.md$/, '');
  if (!name || name.includes('/')) return null;
  return { kind: dir === 'agents' ? 'agent' : 'command', name };
}

// Normalize for content comparison: drop trailing whitespace per line and
// collapse trailing blank lines, then trim. The merge engine normalizes the
// same way (parseMarkdown / rebuildSections), so this makes verify robust to
// cosmetic whitespace without masking real content differences.
function norm(s) {
  return (s || '').replace(/[ \t]+$/gm, '').replace(/\n{2,}$/g, '\n').trim();
}

// Build an overlay BODY (section markers) capturing the diff base→edited.
// Returns { body } on success or { blocker: <reason> } when the edit cannot be
// expressed by the overlay model.
function buildOverlayBody(baseContent, editedContent) {
  const base = parseMarkdown(baseContent);
  const edited = parseMarkdown(editedContent);

  // The merge engine keeps the BASE frontmatter and BASE preamble verbatim, so
  // a change to either cannot be captured by an overlay.
  if (norm(base.frontmatter) !== norm(edited.frontmatter)) {
    return { blocker: 'frontmatter-changed' };
  }
  if (norm(base.preamble) !== norm(edited.preamble)) {
    return { blocker: 'preamble-changed' };
  }

  const editedHeadings = new Set(edited.sections.map((s) => s.heading));
  // A deleted base section has no marker to express it.
  for (const s of base.sections) {
    if (s.heading && !editedHeadings.has(s.heading)) {
      return { blocker: `section-deleted: "${s.heading}"` };
    }
  }

  const baseByHeading = new Map(base.sections.map((s) => [s.heading, s]));
  const blocks = [];
  for (const s of edited.sections) {
    const b = baseByHeading.get(s.heading);
    if (!b) {
      // New section → plain H2 (merge appends it as a trailing custom section).
      blocks.push(`## ${s.heading}\n${s.body.replace(/\n+$/, '')}\n`);
    } else if (norm(b.body) !== norm(s.body)) {
      blocks.push(`## [OVERRIDE] ${s.heading}\n${s.body.replace(/\n+$/, '')}\n`);
    }
  }
  if (blocks.length === 0) return { blocker: 'no-diff' };
  return { body: '\n' + blocks.join('\n') };
}

// Replay the merge against the fresh base and check it reproduces the edit
// exactly (ordered sections + preamble, ignoring frontmatter and the generated
// marker comment). This is the contract behind the user's "auto-capture +
// verify" choice — capture is only trusted when it round-trips.
function verifyCapture({ kind, name, baseContent, overlayContent, editedContent, frameworkVersion }) {
  let generated;
  try {
    ({ generated } = mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion }));
  } catch (_) {
    return false;
  }
  const g = parseMarkdown(generated);
  const e = parseMarkdown(editedContent);
  if (g.sections.length !== e.sections.length) return false;
  for (let i = 0; i < e.sections.length; i++) {
    if (g.sections[i].heading !== e.sections[i].heading) return false;
    if (norm(g.sections[i].body) !== norm(e.sections[i].body)) return false;
  }
  const gPre = norm(g.preamble.replace(/<!--\s*baldart-generated:[\s\S]*?-->/g, ''));
  return gPre === norm(e.preamble);
}

/**
 * Capture + verify EVERY overlay-able file touched by the divergent commits —
 * both already-overlaid and not. The caller passes the full touched set (never
 * a pre-filtered subset): an unmappable or unverifiable path must surface as a
 * BLOCKER here, never be silently dropped (which a pre-filter would do, letting
 * the reset wipe an edit with no overlay to preserve it).
 *
 * Per file:
 *   - not an overlay primitive / no overlay mapping → BLOCKER.
 *   - skill: runtime-concat — the CLI cannot merge or verify it. An EXISTING
 *     overlay carries the customization (re-applied at runtime) → trusted; NO
 *     overlay → BLOCKER (the edit would be lost).
 *   - agent/command: fully verifiable. An EXISTING overlay is VERIFIED to still
 *     reproduce the current edit (a stale overlay that predates a newer edit is
 *     a BLOCKER, not a silent loss). NO overlay → captured from the diff and
 *     verified; un-capturable (deletion / frontmatter / preamble change) →
 *     BLOCKER.
 *
 * @param {Object}   args
 * @param {string}   args.cwd               consumer repo root
 * @param {Object}   args.git               GitUtils instance (uses .git.raw)
 * @param {string[]} args.files             consumer `.framework/…` paths
 * @param {string}   args.frameworkVersion  the version being updated TO (remote)
 * @returns {Promise<{ captured: string[], blockers: Array<{file:string,reason:string}> }>}
 *          `captured` = overlay rel-paths that preserve the edit (existing,
 *          verified, or newly written); `blockers` = files that could not be
 *          captured+verified.
 */
async function captureAndVerify({ cwd, git, files, frameworkVersion }) {
  const captured = [];
  const blockers = [];
  for (const file of [...new Set(files || [])]) {
    const kn = kindNameFromPath(file);
    if (!kn) { blockers.push({ file, reason: 'not-overlayable' }); continue; }
    const overlayRel = GitUtils.overlayRelForFrameworkFile(file);
    if (!overlayRel) { blockers.push({ file, reason: 'no-overlay-mapping' }); continue; }
    const overlayAbs = path.join(cwd, overlayRel);
    const overlayExists = fs.existsSync(overlayAbs);

    if (kn.kind === 'skill') {
      // Runtime-concat: the CLI never merges a skill overlay, so it cannot be
      // verified. Trust an existing overlay (the customization lives there and
      // is re-applied at runtime); a skill edit with no overlay would be lost.
      if (overlayExists) captured.push(overlayRel);
      else blockers.push({ file, reason: 'skill edit with no overlay — runtime-concat, cannot auto-capture; author one with /overlay' });
      continue;
    }

    // agent / command — verifiable against the fresh upstream base.
    let baseContent;
    try { baseContent = await git.git.raw(['show', `FETCH_HEAD:${toRepoPath(file)}`]); }
    catch (_) { blockers.push({ file, reason: 'no-upstream-base (new file?)' }); continue; }

    let editedContent;
    try { editedContent = fs.readFileSync(path.join(cwd, file), 'utf8'); }
    catch (_) { blockers.push({ file, reason: 'edit unreadable on disk' }); continue; }

    if (overlayExists) {
      // Verify the EXISTING overlay still reproduces the current edit. An
      // overlay authored before a newer `.framework` edit is stale — resetting
      // would silently drop the delta, so stop instead.
      let overlayContent;
      try { overlayContent = fs.readFileSync(overlayAbs, 'utf8'); }
      catch (_) { blockers.push({ file, reason: 'existing overlay unreadable' }); continue; }

      // Reset-safety shortcut: when the `.framework/` file carries NO inline
      // edit to lose, a reset+re-apply preserves everything (the customization
      // lives in the overlay and is re-applied). Two pristine signals:
      //   (a) it is byte-identical to the upstream base we'd reset to, or
      //   (b) it matches the base the overlay was authored against
      //       (`base_file_sha`) — pristine even after upstream moved on.
      // This is what makes an ADDITIVE (extend) overlay survive: the strict
      // round-trip below would otherwise demand merge(base, overlay) == file,
      // which never holds when the overlay only appends — flagging a perfectly
      // healthy overlay as "stale" on every release.
      const overlayBaseSha = (/^\s*base_file_sha\s*:\s*"?([a-f0-9]+)"?/m.exec(overlayContent) || [])[1];
      const editedPristine =
        norm(editedContent) === norm(baseContent)
        || (overlayBaseSha && computeBaseFileSha(editedContent) === overlayBaseSha);
      if (editedPristine) {
        captured.push(overlayRel);
        continue;
      }

      if (verifyCapture({ kind: kn.kind, name: kn.name, baseContent, overlayContent, editedContent, frameworkVersion })) {
        captured.push(overlayRel);
      } else {
        blockers.push({ file, reason: 'existing overlay does not reproduce your current edit (stale) — reconcile it' });
      }
      continue;
    }

    // No overlay yet — capture from the diff and verify the round-trip.
    const built = buildOverlayBody(baseContent, editedContent);
    if (built.blocker) { blockers.push({ file, reason: built.blocker }); continue; }

    const baseSha = computeBaseFileSha(baseContent);
    const overlayContent =
      buildFrontmatter({ kind: kn.kind, name: kn.name, frameworkVersion, baseSha, mode: 'extend' })
      + built.body;

    if (!verifyCapture({ kind: kn.kind, name: kn.name, baseContent, overlayContent, editedContent, frameworkVersion })) {
      blockers.push({ file, reason: 'verify-failed (merge would not reproduce the edit)' });
      continue;
    }

    try {
      fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
      fs.writeFileSync(overlayAbs, overlayContent);
      captured.push(overlayRel);
    } catch (err) {
      blockers.push({ file, reason: `could not write overlay: ${err.message}` });
    }
  }
  return { captured, blockers };
}

module.exports = {
  captureAndVerify,
  // exported for unit tests
  buildOverlayBody,
  verifyCapture,
  kindNameFromPath,
  toRepoPath,
  norm,
};
