const crypto = require('crypto');
const fs = require('fs');
const yaml = require('js-yaml');

/**
 * Overlay merge engine for agents and commands.
 *
 * Reads a base markdown file (from the framework) plus an overlay file
 * (from `.baldart/overlays/<kind>/<name>.md`) and produces the merged
 * content that BALDART writes into `.claude/<kind>s/<name>.md` at
 * `add`/`update` time.
 *
 * Section model
 * -------------
 * - The base is split into H2 sections (`^## <heading>` … up to next H2/EOF).
 * - The overlay body uses three section markers:
 *     `## [OVERRIDE] <heading>`  → replace base section <heading> verbatim
 *     `## [APPEND]   <heading>`  → append overlay block after base section
 *     `## [PREPEND]  <heading>`  → prepend overlay block before base section
 *   Sections in the overlay without a marker are appended at the end of the
 *   merged file as new H2 sections (custom additions).
 *
 * - The base file's frontmatter (YAML between leading `---` fences) is
 *   preserved untouched in the output — Claude Code uses `name`, `description`,
 *   `tools`, `model` from it. Overlay frontmatter is metadata FOR BALDART only
 *   (`base_agent`, `base_agent_version`, `mode`) and is NOT propagated.
 *
 * - The output gets a BALDART marker comment as the first line (above the
 *   frontmatter) so `update` can detect that the file is regeneration-safe.
 */

const MARKER_PREFIX = '<!-- baldart-generated:';

function shortSha(content) {
  return crypto.createHash('sha256').update(content).digest('hex').slice(0, 12);
}

// Public alias — the CLI scaffolder writes this value into the overlay
// frontmatter (as `base_file_sha`) so `baldart overlay drift` can detect
// when the base file changed upstream without re-parsing the merger logic.
function computeBaseFileSha(content) {
  return shortSha(content);
}

/**
 * Idempotently backfill `base_file_sha` into an overlay file's frontmatter so
 * `baldart overlay drift` is deterministic for EVERY overlay — not only those
 * freshly scaffolded by `overlay scaffold` (which already writes it). Overlays
 * authored by hand or by pre-v3.19.0 versions are otherwise "unknown" to drift
 * (overlay.js drift § no base_file_sha). The merger knows the base file, so it
 * stamps the sha here at `add`/`update` time.
 *
 * Contract: ADDITIVE-ONLY. Never overwrites an existing `base_file_sha`. Only
 * touches a file that already has a leading YAML frontmatter block — never
 * fabricates frontmatter on a file that lacks it. Fail-safe: any I/O error is
 * swallowed (returns `changed:false`) so a backfill can never block a merge.
 *
 * @param {string} overlayPath absolute path to `.baldart/overlays/<…>.md`
 * @param {string} baseSha     12-char hex from computeBaseFileSha(baseContent)
 * @returns {{ changed: boolean, reason: string }}
 */
function ensureBaseFileShaInOverlay(overlayPath, baseSha) {
  if (!baseSha) return { changed: false, reason: 'no-base-sha' };
  let content;
  try { content = fs.readFileSync(overlayPath, 'utf8'); }
  catch (_) { return { changed: false, reason: 'unreadable' }; }
  // Leading frontmatter block only (optionally after a BOM / blank lines).
  const m = content.match(/^(\uFEFF?\s*)---\n([\s\S]*?)\n---(\r?\n|$)/);
  if (!m) return { changed: false, reason: 'no-frontmatter' };
  const fmBody = m[2];
  if (/^\s*base_file_sha\s*:/m.test(fmBody)) {
    return { changed: false, reason: 'already-present' };
  }
  // Quote the value — a 12-char hex can be all digits, which YAML would
  // otherwise parse as a number (matches overlay.js buildFrontmatter).
  const newFmBody = `${fmBody.replace(/\n+$/, '')}\nbase_file_sha: "${baseSha}"`;
  const replaced = `${m[1]}---\n${newFmBody}\n---${m[3]}`;
  const updated = content.slice(0, m.index) + replaced + content.slice(m.index + m[0].length);
  try { fs.writeFileSync(overlayPath, updated, 'utf8'); }
  catch (_) { return { changed: false, reason: 'unwritable' }; }
  return { changed: true, reason: 'backfilled' };
}

// The marker identity is CONTENT-ADDRESSED only: `base_sha` + `overlay_sha`,
// both of which change exclusively when the base file or the overlay actually
// change. `base_version` is deliberately NOT stamped here — VERSION bumps on
// every framework release regardless of whether THIS base file changed, so
// embedding it made every `baldart update` rewrite this line and produce a
// spurious git diff on byte-identical generated files (→ a "post-update
// reconcile" commit every single update). The field was informational only
// (readMarker parsed it but nothing gated regeneration on it), so dropping it
// buys byte-stability across version-only bumps at zero cost. (v4.11.0)
/**
 * Refresh the overlay's informational `base_<kind>_version` pin to the installed
 * VERSION — but ONLY when the base content is unchanged (the overlay's
 * `base_file_sha` still matches the current base sha). This keeps the pin
 * truthful (it tracks the version the overlay was last reconciled against)
 * without ever masking real drift: when the base content HAS changed the sha
 * differs, so we leave the stale pin alone and it remains a legitimate
 * "review me" signal for `overlay drift` / `status` / `doctor`.
 *
 * Contract: NO-OP unless `base_file_sha` is present AND equals `currentBaseSha`
 * AND the version field actually differs. Fail-safe: any I/O error returns
 * `changed:false` so it can never block a merge. (v4.11.0)
 *
 * @param {string} overlayPath    absolute path to `.baldart/overlays/<…>.md`
 * @param {string} kind           'agent' | 'command' | 'skill'
 * @param {string} frameworkVersion installed framework VERSION
 * @param {string} currentBaseSha  computeBaseFileSha(currentBaseContent)
 * @returns {{ changed: boolean, reason: string }}
 */
function refreshOverlayVersionPin(overlayPath, kind, frameworkVersion, currentBaseSha) {
  if (!frameworkVersion || !currentBaseSha) return { changed: false, reason: 'missing-input' };
  let content;
  try { content = fs.readFileSync(overlayPath, 'utf8'); }
  catch (_) { return { changed: false, reason: 'unreadable' }; }
  const m = content.match(/^(﻿?\s*)---\n([\s\S]*?)\n---(\r?\n|$)/);
  if (!m) return { changed: false, reason: 'no-frontmatter' };
  const fmBody = m[2];
  // Only refresh when the base is provably unchanged — never mask drift.
  const shaMatch = fmBody.match(/^\s*base_file_sha\s*:\s*["']?([0-9a-f]+)["']?\s*$/m);
  if (!shaMatch || shaMatch[1] !== currentBaseSha) return { changed: false, reason: 'sha-mismatch-or-absent' };
  const versionKey = `base_${kind}_version`;
  const verRe = new RegExp(`^(\\s*${versionKey}\\s*:\\s*).*$`, 'm');
  if (!verRe.test(fmBody)) return { changed: false, reason: 'no-version-field' };
  const current = fmBody.match(verRe)[0].split(':').slice(1).join(':').trim();
  if (current === String(frameworkVersion)) return { changed: false, reason: 'already-current' };
  const newFmBody = fmBody.replace(verRe, `$1${frameworkVersion}`);
  const replaced = `${m[1]}---\n${newFmBody}\n---${m[3]}`;
  const updated = content.slice(0, m.index) + replaced + content.slice(m.index + m[0].length);
  try { fs.writeFileSync(overlayPath, updated, 'utf8'); }
  catch (_) { return { changed: false, reason: 'unwritable' }; }
  return { changed: true, reason: 'refreshed' };
}

function buildMarker({ kind, name, baseSha, overlaySha }) {
  const baseShaField = baseSha ? ` base_sha=${baseSha}` : '';
  return `${MARKER_PREFIX} kind=${kind} name=${name}${baseShaField} overlay_sha=${overlaySha}
     DO NOT EDIT MANUALLY. Edit .baldart/overlays/${kind}s/${name}.md instead;
     this file is regenerated by \`npx baldart update\`. -->`;
}

/**
 * Returns the marker fields encoded in the generated-marker block if present,
 * or null. Used by `update` to know "is this file ours (safe to regenerate) or
 * did the user edit it manually?"
 *
 * The marker block can appear in two positions:
 *   - line 1 (pre-v3.26.1 generated files — kept for back-compat reading)
 *   - immediately after the closing `---` of the YAML frontmatter (v3.26.1+)
 *
 * `base_sha` is optional — older generated files (pre-v3.19.0) won't have it.
 * `base_version` is optional too: v4.11.0+ stops stamping it (see buildMarker),
 * but files generated by older versions still carry it, so the regex tolerates
 * either shape and surfaces `baseVersion` as null when absent.
 */
function readMarker(content) {
  const idx = content.indexOf(MARKER_PREFIX);
  if (idx === -1) return null;
  // The marker block ends at the next `-->`. Cap the search to the first 4KB
  // to avoid scanning huge files when the marker isn't actually there.
  const block = content.slice(idx, idx + 4000);
  const end = block.indexOf('-->');
  if (end === -1) return null;
  const m = block.slice(0, end).match(/kind=(\S+)\s+name=(\S+)(?:\s+base_version=(\S+))?(?:\s+base_sha=(\S+))?\s+overlay_sha=(\S+)/);
  if (!m) return null;
  return { kind: m[1], name: m[2], baseVersion: m[3] || null, baseSha: m[4] || null, overlaySha: m[5] };
}

/**
 * Tells whether `content` is a BALDART-generated file (agent or command).
 * Position-agnostic — accepts both pre-v3.26.1 (marker on line 1) and v3.26.1+
 * (marker after the frontmatter) layouts.
 */
function isGeneratedFile(content) {
  return readMarker(content) !== null;
}

/**
 * Split markdown text into:
 *   - frontmatter: the raw text between leading `---` fences (or '')
 *   - preamble: any text after frontmatter before the first H2
 *   - sections: ordered list of { heading, marker, body }
 *               marker is null | 'OVERRIDE' | 'APPEND' | 'PREPEND'
 */
function parseMarkdown(text) {
  let frontmatter = '';
  let rest = text;

  // Strip leading BOM / whitespace
  rest = rest.replace(/^﻿/, '');

  const fmMatch = rest.match(/^---\n([\s\S]*?)\n---\n?/);
  if (fmMatch) {
    frontmatter = fmMatch[1];
    rest = rest.slice(fmMatch[0].length);
  }

  // Split on H2 boundaries. Keep delimiters by using a lookahead.
  const parts = rest.split(/(?=^## )/m);
  const preamble = (parts.length && !parts[0].startsWith('## ')) ? parts.shift() : '';

  const sections = parts.map((block) => {
    // Heading line
    const eol = block.indexOf('\n');
    const headingLine = eol >= 0 ? block.slice(0, eol) : block;
    const body = eol >= 0 ? block.slice(eol + 1) : '';

    // Detect marker — `## [OVERRIDE] Foo` etc. Marker keyword is upper-case
    // between square brackets, then a single space, then the heading text.
    const m = headingLine.match(/^##\s+\[(OVERRIDE|APPEND|PREPEND)\]\s+(.+?)\s*$/);
    if (m) {
      return { marker: m[1], heading: m[2].trim(), body: body.replace(/\n+$/, '') + '\n' };
    }
    const plain = headingLine.match(/^##\s+(.+?)\s*$/);
    return { marker: null, heading: plain ? plain[1].trim() : '', body: body.replace(/\n+$/, '') + '\n' };
  });

  return { frontmatter, preamble, sections };
}

function rebuildSections(sections) {
  return sections.map((s) => `## ${s.heading}\n${s.body}`).join('\n').replace(/\n+$/, '\n');
}

/**
 * Merge base + overlay → generated content + warnings.
 *
 * @param {Object} args
 * @param {string} args.kind          - 'agent' | 'command'
 * @param {string} args.name          - identifier (filename without extension)
 * @param {string} args.baseContent   - raw markdown of the framework base
 * @param {string} args.overlayContent - raw markdown of the overlay
 * @param {string} args.frameworkVersion - the installed framework VERSION
 * @returns {{ generated: string, warnings: string[], baseVersionTargeted: string, overlayMode: string }}
 */
function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion }) {
  const warnings = [];

  // Parse overlay frontmatter for metadata.
  const overlayParsed = parseMarkdown(overlayContent);
  let overlayMeta = {};
  if (overlayParsed.frontmatter) {
    try { overlayMeta = yaml.load(overlayParsed.frontmatter) || {}; }
    catch (err) {
      warnings.push(`overlay frontmatter parse error (${err.message}) — treating overlay as plain markdown without metadata`);
    }
  }
  const baseVersionTargeted = String(overlayMeta[`base_${kind}_version`] || '').trim()
    || (frameworkVersion ? String(frameworkVersion) : 'unknown');
  const overlayMode = (overlayMeta.mode || 'extend').trim();

  // Parse the base. Keep its frontmatter verbatim — that's what Claude reads.
  const baseParsed = parseMarkdown(baseContent);

  // Build a heading → section map for the base. If headings repeat we keep
  // the first occurrence (Claude Code does the same for repeated `name:`).
  const baseSectionByHeading = new Map();
  baseParsed.sections.forEach((s, idx) => {
    if (s.heading && !baseSectionByHeading.has(s.heading)) {
      baseSectionByHeading.set(s.heading, { ...s, _idx: idx });
    }
  });

  // Apply overlay sections.
  const newSections = baseParsed.sections.map((s) => ({ ...s }));
  const trailing = []; // overlay sections without marker → custom adds at the end

  for (const ov of overlayParsed.sections) {
    if (!ov.marker) {
      trailing.push(ov);
      continue;
    }
    const base = baseSectionByHeading.get(ov.heading);
    if (!base) {
      warnings.push(`overlay section "${ov.marker} ${ov.heading}" has no matching H2 in base ${kind} "${name}" — appending as new section instead`);
      trailing.push({ marker: null, heading: ov.heading, body: ov.body });
      continue;
    }
    const target = newSections[base._idx];
    if (ov.marker === 'OVERRIDE') {
      target.body = ov.body;
    } else if (ov.marker === 'APPEND') {
      target.body = `${target.body.replace(/\n+$/, '')}\n\n${ov.body}`;
    } else if (ov.marker === 'PREPEND') {
      target.body = `${ov.body.replace(/\n+$/, '')}\n\n${target.body}`;
    }
  }

  // Compose the merged body.
  const sectionsOut = rebuildSections(newSections);
  const trailingOut = trailing.length
    ? '\n' + trailing.map((s) => `## ${s.heading}\n${s.body}`).join('\n').replace(/\n+$/, '\n')
    : '';

  const overlaySha = shortSha(overlayContent);
  const baseSha = shortSha(baseContent);
  const marker = buildMarker({ kind, name, baseSha, overlaySha });

  const fmBlock = baseParsed.frontmatter
    ? `---\n${baseParsed.frontmatter}\n---\n`
    : '';

  // v3.26.1+ layout: frontmatter MUST be the first thing in the file —
  // Claude Code's agent discovery uses a strict YAML-frontmatter parser that
  // skips files where `---` is not on line 1. Pre-v3.26.1 placed the marker
  // first, which silently de-registered every overlay-merged agent (no
  // `subagent_type` exposed → spawns failed with "agent not found"). The
  // marker now sits as a CommonMark HTML comment immediately after the
  // closing `---`, which `readMarker` still finds via position-agnostic
  // scan. If the base has no frontmatter (unusual), marker still goes first.
  const generated = fmBlock
    ? fmBlock + `${marker}\n` + (baseParsed.preamble || '') + sectionsOut + trailingOut
    : `${marker}\n` + (baseParsed.preamble || '') + sectionsOut + trailingOut;

  return {
    generated,
    warnings,
    baseVersionTargeted,
    overlayMode,
    overlaySha
  };
}

module.exports = {
  mergeOverlay,
  readMarker,
  isGeneratedFile,
  parseMarkdown,
  shortSha,
  computeBaseFileSha,
  ensureBaseFileShaInOverlay,
  refreshOverlayVersionPin,
  MARKER_PREFIX
};
