const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const UI = require('../utils/ui');
const { mergeOverlay, computeBaseFileSha, parseMarkdown } = require('../utils/overlay-merger');

const FRAMEWORK_DIR = '.framework';
const FRAMEWORK_PAYLOAD = path.join(FRAMEWORK_DIR, 'framework');
const OVERLAYS_DIR = path.join('.baldart', 'overlays');

const KINDS = ['skill', 'agent', 'command'];

function frameworkRoot(cwd) {
  return path.join(cwd, FRAMEWORK_DIR);
}

function ensureConsumer(cwd) {
  if (!fs.existsSync(frameworkRoot(cwd))) {
    UI.error('Not a BALDART consumer repo — `.framework/` not found.');
    UI.info('Run `npx baldart` to install the framework first.');
    process.exit(1);
  }
}

function readFrameworkVersion(cwd) {
  try {
    return fs.readFileSync(path.join(cwd, FRAMEWORK_DIR, 'VERSION'), 'utf8').trim();
  } catch (_) {
    return null;
  }
}

function baseFilePathFor(cwd, kind, name) {
  if (kind === 'skill') {
    return path.join(cwd, FRAMEWORK_PAYLOAD, '.claude', 'skills', name, 'SKILL.md');
  }
  return path.join(cwd, FRAMEWORK_PAYLOAD, '.claude', `${kind}s`, `${name}.md`);
}

function overlayPathFor(cwd, kind, name) {
  if (kind === 'skill') return path.join(cwd, OVERLAYS_DIR, `${name}.md`);
  return path.join(cwd, OVERLAYS_DIR, `${kind}s`, `${name}.md`);
}

function parseTarget(target) {
  // Accept "agents/coder", "commands/codexreview", "skill/bug", or bare name
  // (skill assumed). Always normalise to { kind: 'agent'|'command'|'skill', name }.
  if (!target || typeof target !== 'string') return null;
  if (target.includes('/')) {
    const [head, ...rest] = target.split('/');
    const name = rest.join('/');
    if (!name) return null;
    if (head === 'skill' || head === 'skills') return { kind: 'skill', name };
    if (head === 'agent' || head === 'agents') return { kind: 'agent', name };
    if (head === 'command' || head === 'commands') return { kind: 'command', name };
    return null;
  }
  return { kind: 'skill', name: target };
}

function parseFrontmatter(content) {
  const m = content.match(/^---\n([\s\S]*?)\n---/);
  if (!m) return { fm: {}, found: false };
  try {
    const fm = yaml.load(m[1]) || {};
    // Coerce `base_file_sha` to string — a 12-char hex SHA may be all digits,
    // which YAML would otherwise parse as a number (0 → falsy → false drift).
    if (fm.base_file_sha != null) fm.base_file_sha = String(fm.base_file_sha);
    return { fm, found: true };
  } catch (_) {
    return { fm: {}, found: true, malformed: true };
  }
}

// Skill overlays use the runtime-concat model — the executing agent reads the
// base SKILL.md and the overlay and applies the overlay's guidance itself.
// Unlike agent/command overlays, NOTHING parses the [OVERRIDE]/[APPEND]/[PREPEND]
// markers here (see framework/agents/project-context.md § 5). This helper gives
// the skill-overlay author the same "what does the merge look like?" feedback the
// marker model already gives for agents/commands: it lists the overlay's sections,
// flags markers whose target heading no longer exists in the base (the agent has
// nothing to anchor the override to), and reports the concatenated size. Pure — no IO.
function analyzeSkillConcat(baseContent, overlayContent) {
  const base = parseMarkdown(baseContent);
  const overlay = parseMarkdown(overlayContent);
  const baseHeadings = new Set(base.sections.map((s) => s.heading).filter(Boolean));

  const markerSections = [];
  let plainSections = 0;
  for (const s of overlay.sections) {
    if (s.marker) {
      markerSections.push({ marker: s.marker, heading: s.heading, orphan: !baseHeadings.has(s.heading) });
    } else if (s.heading) {
      plainSections += 1;
    }
  }
  return {
    baseLines: baseContent.split('\n').length,
    overlayLines: overlayContent.split('\n').length,
    baseSectionCount: base.sections.length,
    markerSections,
    plainSections,
    orphans: markerSections.filter((m) => m.orphan),
  };
}

function scaffoldBody(kind, name) {
  if (kind === 'skill') {
    return [
      '',
      `> HOW SKILL OVERLAYS WORK`,
      `> When the "${name}" skill runs, the agent reads this file and applies it on top`,
      `> of the base SKILL.md (runtime concatenation). NOTHING in the CLI parses the`,
      `> [OVERRIDE]/[APPEND]/[PREPEND] markers below — for skills they are textual`,
      `> conventions the agent honours (best-effort), not a compile-time merge (that`,
      `> model is agent/command-only). Keep this file the single home for everything`,
      `> project-specific the base skill deliberately leaves generic.`,
      `>`,
      `> Frontmatter above: bump base_skill_version when you re-review against a newer`,
      `> framework; \`mode: override\` tells the agent to prefer this file wholesale.`,
      `> Fill in the sections that apply and delete the rest, then run`,
      `> \`npx baldart overlay validate .baldart/overlays/${name}.md\` to preview the merge.`,
      '',
      `## [APPEND] Project-specific guidance`,
      '',
      `<!-- The default home for project facts the base "${name}" skill cannot know: -->`,
      `<!--   - canonical paths / module entry points (cite REAL files in this repo) -->`,
      `<!--   - domain vocabulary, naming conventions, terminology -->`,
      `<!--   - project-specific BLOCKING rules ("never X", "always read Y first") -->`,
      `<!--   - mandated stack / forbidden libraries beyond baldart.config.yml stack.* -->`,
      '',
      `## [OVERRIDE] <exact base H2 to replace>`,
      '',
      `<!-- OPTIONAL. Copy an H2 heading VERBATIM from the base SKILL.md to tell the -->`,
      `<!-- agent to prefer this version over the base. \`overlay validate\` warns if -->`,
      `<!-- the heading matches no base section. Delete this block if you only append. -->`,
      '',
    ].join('\n');
  }
  return [
    '',
    `> Section markers (parsed by overlay-merger.js):`,
    `>   ## [OVERRIDE] <H2> — replace base section verbatim`,
    `>   ## [APPEND]   <H2> — add overlay content after base section`,
    `>   ## [PREPEND]  <H2> — add overlay content before base section`,
    `>   ## <H2>            — appended as a new section at the end`,
    '',
    `## [APPEND] <heading from base ${kind} "${name}">`,
    '',
    `<!-- Replace the heading above with one that exists in the base file. -->`,
    '',
  ].join('\n');
}

function buildFrontmatter({ kind, name, frameworkVersion, baseSha, mode }) {
  const lines = ['---'];
  lines.push(`base_${kind}: ${name}`);
  lines.push(`base_${kind}_version: ${frameworkVersion || 'unknown'}`);
  // Quote the SHA — a 12-char hex string can be all digits, which YAML would
  // otherwise parse as a number (0 → falsy → false-negative in drift check).
  if (baseSha) lines.push(`base_file_sha: "${baseSha}"`);
  lines.push(`mode: ${mode || 'extend'}`);
  lines.push('---');
  return lines.join('\n') + '\n';
}

// ─── scaffold ──────────────────────────────────────────────────────────────

// Pure scaffolder — performs the filesystem work and returns a structured
// result WITHOUT printing or calling process.exit. The interactive `scaffold`
// wrapper renders the result; `update --on-divergence scaffold-overlays` calls
// this in a loop (so one already-existing or missing overlay can't terminate
// the whole update). Keeping the path/frontmatter logic here honours the
// "CLI is the source of truth, never re-implement" rule.
function scaffoldFile(cwd, target, options = {}) {
  const parsed = parseTarget(target);
  if (!parsed) return { ok: false, reason: 'invalid-target', target };
  const { kind, name } = parsed;

  const baseAbs = baseFilePathFor(cwd, kind, name);
  if (!fs.existsSync(baseAbs)) {
    return { ok: false, reason: 'base-missing', kind, name, baseRel: path.relative(cwd, baseAbs) };
  }

  const overlayAbs = overlayPathFor(cwd, kind, name);
  const overlayRel = path.relative(cwd, overlayAbs);
  if (fs.existsSync(overlayAbs)) {
    return { ok: false, reason: 'exists', kind, name, overlayRel };
  }

  const mode = (options.mode || 'extend').trim();
  if (!['extend', 'override'].includes(mode)) {
    return { ok: false, reason: 'bad-mode', mode };
  }

  fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
  const baseContent = fs.readFileSync(baseAbs, 'utf8');
  const baseSha = computeBaseFileSha(baseContent);
  const fwVersion = readFrameworkVersion(cwd);
  const content =
    buildFrontmatter({ kind, name, frameworkVersion: fwVersion, baseSha, mode }) +
    scaffoldBody(kind, name);
  fs.writeFileSync(overlayAbs, content);

  return {
    ok: true, reason: 'created', kind, name, overlayRel,
    baseRel: path.relative(cwd, baseAbs), fwVersion, baseSha,
  };
}

async function scaffold(target, options = {}) {
  const cwd = process.cwd();
  ensureConsumer(cwd);

  const res = scaffoldFile(cwd, target, options);
  if (!res.ok) {
    if (res.reason === 'invalid-target') {
      UI.error('Invalid target. Use: skill/<name>, agents/<name>, or commands/<name>.');
      UI.info('Example: npx baldart overlay scaffold agents/coder');
      process.exit(1);
    }
    if (res.reason === 'base-missing') {
      UI.error(`Base ${res.kind} "${res.name}" not found at ${res.baseRel}.`);
      UI.info(`Run \`ls ${path.dirname(res.baseRel)}\` to list available ${res.kind}s.`);
      process.exit(1);
    }
    if (res.reason === 'exists') {
      UI.warning(`Overlay already exists: ${res.overlayRel}`);
      UI.info('Run `npx baldart overlay drift ' + res.overlayRel + '` to check it, or edit it directly to extend.');
      process.exit(0);
    }
    if (res.reason === 'bad-mode') {
      UI.error(`Invalid --mode "${res.mode}". Use "extend" or "override".`);
      process.exit(1);
    }
    process.exit(1);
  }

  UI.success(`Created ${res.overlayRel}`);
  UI.info(`Base file: ${res.baseRel}`);
  UI.info(`Targets framework v${res.fwVersion || 'unknown'} (base_file_sha=${res.baseSha}).`);
  UI.newline();
  UI.list([
    `Edit ${res.overlayRel} to add your customisations.`,
    `Validate the merge with: npx baldart overlay validate ${res.overlayRel}`,
    res.kind === 'skill'
      ? `Run any /-command that uses this skill to see the overlay applied at runtime.`
      : `Apply with: npx baldart update (regenerates .claude/${res.kind}s/${res.name}.md from base + overlay).`,
  ]);
}

// ─── validate ──────────────────────────────────────────────────────────────

function deriveTargetFromOverlayPath(cwd, overlayPath) {
  const abs = path.isAbsolute(overlayPath) ? overlayPath : path.join(cwd, overlayPath);
  const root = path.join(cwd, OVERLAYS_DIR);
  const rel = path.relative(root, abs);
  if (rel.startsWith('..')) return null;
  const parts = rel.split(path.sep);
  if (parts.length === 1) {
    return { kind: 'skill', name: parts[0].replace(/\.md$/, ''), abs };
  }
  if (parts.length === 2) {
    const sub = parts[0];
    if (sub === 'agents') return { kind: 'agent', name: parts[1].replace(/\.md$/, ''), abs };
    if (sub === 'commands') return { kind: 'command', name: parts[1].replace(/\.md$/, ''), abs };
  }
  return null;
}

async function validate(overlayPath) {
  const cwd = process.cwd();
  ensureConsumer(cwd);

  if (!overlayPath) {
    UI.error('Usage: npx baldart overlay validate <path>');
    UI.info('Example: npx baldart overlay validate .baldart/overlays/agents/coder.md');
    process.exit(1);
  }

  const t = deriveTargetFromOverlayPath(cwd, overlayPath);
  if (!t) {
    UI.error(`Path is not under ${OVERLAYS_DIR}/ or has unexpected shape: ${overlayPath}`);
    process.exit(1);
  }
  if (!fs.existsSync(t.abs)) {
    UI.error(`Overlay file not found: ${path.relative(cwd, t.abs)}`);
    process.exit(1);
  }

  const overlayContent = fs.readFileSync(t.abs, 'utf8');
  const { fm, found, malformed } = parseFrontmatter(overlayContent);
  if (!found) {
    UI.error('Overlay has no YAML frontmatter.');
    process.exit(1);
  }
  if (malformed) {
    UI.error('Overlay frontmatter is not valid YAML.');
    process.exit(1);
  }
  const expectedKindField = `base_${t.kind}`;
  if (fm[expectedKindField] !== t.name) {
    UI.error(`Frontmatter mismatch: expected ${expectedKindField}: ${t.name} (got ${fm[expectedKindField] || 'missing'}).`);
    process.exit(1);
  }

  const baseAbs = baseFilePathFor(cwd, t.kind, t.name);
  if (!fs.existsSync(baseAbs)) {
    UI.error(`Base ${t.kind} "${t.name}" not found at ${path.relative(cwd, baseAbs)}.`);
    process.exit(1);
  }

  if (t.kind === 'skill') {
    const baseContent = fs.readFileSync(baseAbs, 'utf8');
    const a = analyzeSkillConcat(baseContent, overlayContent);
    UI.success(`Overlay valid (skill, runtime-concat model).`);
    UI.info(`Targets v${fm.base_skill_version || 'unknown'}, mode=${fm.mode || 'extend'}.`);
    UI.newline();
    // Dry-run preview. For agent/command we render the actual merged output;
    // skills can't be merged at the CLI (the executing agent reads base + overlay
    // and applies it), so we show what the agent will see instead of a fake merge.
    UI.info('Merge preview (runtime concat: base SKILL.md + overlay):');
    UI.list([
      `base sections: ${a.baseSectionCount} · overlay sections: ${a.markerSections.length + a.plainSections} (${a.markerSections.length} marked, ${a.plainSections} new)`,
      `concatenated size: ~${a.baseLines + a.overlayLines} lines (${a.baseLines} base + ${a.overlayLines} overlay)`,
    ]);
    if (a.markerSections.length) {
      UI.newline();
      UI.info('Section markers (textual conventions — honoured by the agent at runtime, NOT parsed by the CLI):');
      a.markerSections.forEach((m) => {
        if (m.orphan) {
          UI.warning(`  [${m.marker}] ${m.heading} — no matching H2 in base SKILL.md; the agent has no base section to anchor this to. Fix the heading or drop the marker.`);
        } else {
          UI.info(`  [${m.marker}] ${m.heading} — matches a base section`);
        }
      });
    }
    UI.newline();
    if (fm.base_file_sha) {
      const currentSha = computeBaseFileSha(baseContent);
      if (currentSha !== fm.base_file_sha) {
        UI.warning(`Base file changed since overlay was authored (sha ${fm.base_file_sha} → ${currentSha}). Review for drift.`);
      } else {
        UI.success('Base file unchanged since overlay was authored.');
      }
    } else {
      UI.info('No base_file_sha in frontmatter — drift check by version only.');
    }
    return;
  }

  // Agent/command — dry-run the merger.
  const baseContent = fs.readFileSync(baseAbs, 'utf8');
  const fwVersion = readFrameworkVersion(cwd);
  let result;
  try {
    result = mergeOverlay({
      kind: t.kind,
      name: t.name,
      baseContent,
      overlayContent,
      frameworkVersion: fwVersion
    });
  } catch (err) {
    UI.error(`Merger threw: ${err.message}`);
    process.exit(1);
  }

  if (result.warnings && result.warnings.length) {
    result.warnings.forEach((w) => UI.warning(w));
  }
  UI.success(`Overlay valid (${t.kind}, marker model).`);
  UI.info(`Targets v${result.baseVersionTargeted}, mode=${result.overlayMode}.`);
  UI.info(`Generated output: ${result.generated.split('\n').length} lines.`);
  if (fm.base_file_sha) {
    const currentSha = computeBaseFileSha(baseContent);
    if (currentSha !== fm.base_file_sha) {
      UI.warning(`Base file changed since overlay was authored (sha ${fm.base_file_sha} → ${currentSha}). Review for drift.`);
    } else {
      UI.success('Base file unchanged since overlay was authored.');
    }
  }
}

// ─── drift ─────────────────────────────────────────────────────────────────

function collectOverlays(cwd) {
  const root = path.join(cwd, OVERLAYS_DIR);
  if (!fs.existsSync(root)) return [];
  const out = [];
  const pushFrom = (sub, kind) => {
    const dir = sub ? path.join(root, sub) : root;
    if (!fs.existsSync(dir)) return;
    for (const f of fs.readdirSync(dir)) {
      if (!f.endsWith('.md')) continue;
      if (f.endsWith('.example.md')) continue;
      if (sub === '' && (f === 'README.md')) continue;
      out.push({ kind, name: f.replace(/\.md$/, ''), abs: path.join(dir, f), rel: path.join(OVERLAYS_DIR, sub || '', f) });
    }
  };
  pushFrom('', 'skill');
  pushFrom('agents', 'agent');
  pushFrom('commands', 'command');
  return out;
}

async function drift(target, options = {}) {
  const cwd = process.cwd();
  ensureConsumer(cwd);

  const fwVersion = readFrameworkVersion(cwd);
  const all = !!options.all || !target;
  const targets = [];
  if (all) {
    targets.push(...collectOverlays(cwd));
  } else {
    const t = deriveTargetFromOverlayPath(cwd, target);
    if (!t) {
      UI.error(`Path is not under ${OVERLAYS_DIR}/ or has unexpected shape: ${target}`);
      process.exit(1);
    }
    if (!fs.existsSync(t.abs)) {
      UI.error(`Overlay not found: ${path.relative(cwd, t.abs)}`);
      process.exit(1);
    }
    targets.push({ kind: t.kind, name: t.name, abs: t.abs, rel: path.relative(cwd, t.abs) });
  }

  if (targets.length === 0) {
    UI.info('No overlays authored.');
    return;
  }

  let drifted = 0;
  let unknown = 0;
  let clean = 0;
  for (const t of targets) {
    const content = fs.readFileSync(t.abs, 'utf8');
    const { fm } = parseFrontmatter(content);
    const versionField = `base_${t.kind}_version`;
    const overlayVersion = fm[versionField];
    const baseAbs = baseFilePathFor(cwd, t.kind, t.name);
    const baseExists = fs.existsSync(baseAbs);

    if (!baseExists) {
      UI.error(`${t.rel}: base ${t.kind} "${t.name}" no longer exists in framework.`);
      drifted++;
      continue;
    }

    const baseSha = computeBaseFileSha(fs.readFileSync(baseAbs, 'utf8'));
    let shaDrift = null;
    if (fm.base_file_sha) {
      shaDrift = fm.base_file_sha !== baseSha;
    }
    const versionDrift = overlayVersion && fwVersion && overlayVersion !== fwVersion;

    if (shaDrift === true) {
      UI.warning(`${t.rel}: base file changed since overlay was authored (sha ${fm.base_file_sha} → ${baseSha}).`);
      // For skills, surface WHICH overlay markers the base change orphaned — the
      // same actionable feedback agent/command overlays get from the merger. A
      // marker whose heading vanished upstream needs re-anchoring by hand.
      if (t.kind === 'skill') {
        const a = analyzeSkillConcat(fs.readFileSync(baseAbs, 'utf8'), content);
        a.orphans.forEach((m) =>
          UI.warning(`  └─ overlay marker [${m.marker}] "${m.heading}" no longer matches any base H2 — likely renamed/removed upstream. Re-anchor it.`)
        );
      }
      drifted++;
    } else if (shaDrift === false) {
      UI.success(`${t.rel}: clean (sha matches base v${fwVersion}).`);
      clean++;
    } else if (versionDrift) {
      UI.warning(`${t.rel}: targets v${overlayVersion}, installed v${fwVersion} — review (no base_file_sha to confirm).`);
      unknown++;
    } else if (overlayVersion) {
      UI.info(`${t.rel}: targets v${overlayVersion} (no base_file_sha — drift unknown).`);
      unknown++;
    } else {
      UI.warning(`${t.rel}: no base_${t.kind}_version in frontmatter.`);
      unknown++;
    }
  }

  UI.newline();
  UI.info(`Summary: ${clean} clean, ${drifted} drifted, ${unknown} unknown — of ${targets.length} overlay(s).`);
}

module.exports = { scaffold, scaffoldFile, validate, drift, parseTarget, analyzeSkillConcat, buildFrontmatter };
