const fs = require('fs');
const path = require('path');
const UI = require('./ui');

const FRAMEWORK_DIR = '.framework';
// The Git subtree pulls the ENTIRE BALDART repo into `.framework/`, and the
// repo itself nests its shippable content under a top-level `framework/`
// directory. So everything we symlink or copy lives at
// `.framework/framework/<path>`, never directly at `.framework/<path>`.
// Use FRAMEWORK_PAYLOAD for any source path you want to reach inside the
// installed framework — never bare FRAMEWORK_DIR.
const FRAMEWORK_PAYLOAD = path.join(FRAMEWORK_DIR, 'framework');
const CONFLICT_LOG = path.join('.baldart', 'skill-conflicts.json');

// Per-item merge kinds whose framework source is a flat directory of single
// files under `.claude/<kind>s/`. Each entry declares:
//   - dirGetter: the adapter method returning the consumer-side target dir
//     (null when the tool doesn't support the kind, e.g. Codex → workflows).
//   - ext:       the file extension of the source items.
//   - overlay:   whether the base+overlay generation path applies. Overlays
//     are a markdown construct (`## [OVERRIDE]` sections + HTML-comment
//     marker), so they only make sense for `.md` kinds. Workflows are `.js`
//     and are distributed as plain per-item symlinks — no overlay.
//   - exclude:   filenames in the source dir that are NOT installable items.
const MERGE_KINDS = {
  agent:    { dirGetter: 'agentsDir',    ext: '.md', overlay: true,  exclude: new Set(['REGISTRY.md']) },
  command:  { dirGetter: 'commandsDir',  ext: '.md', overlay: true,  exclude: new Set(['REGISTRY.md']) },
  workflow: { dirGetter: 'workflowsDir', ext: '.js', overlay: false, exclude: new Set() },
};

/**
 * Symlink management for BALDART.
 *
 * Design notes (v2.1.1+):
 *
 * - `AGENTS.md`, `agents/`, `.claude/agents/`, `.claude/commands/` are
 *   **bulk symlinks**. They've worked this way since v1.0.0 and replacing
 *   them is safe for users on canonical installations. If a user has
 *   replaced one of these with a real file/dir (customisation), we WARN
 *   and ask before overwriting — we no longer silently rename to `.backup`.
 *
 * - `.claude/skills/` is a **real directory the user owns**. Each framework
 *   skill is symlinked **per-item** inside it. User-authored skills coexist
 *   in the same dir. Name collisions are recorded in
 *   `.baldart/skill-conflicts.json` and never overwritten.
 */
class SymlinkUtils {
  constructor(cwd = process.cwd()) {
    this.cwd = cwd;
  }

  // -----------------------------------------------------------------------
  // Low-level helpers
  // -----------------------------------------------------------------------

  /**
   * Create a single symlink at linkPath pointing at target.
   * Modes:
   *   safe: refuse to overwrite a non-symlink at linkPath (returns false).
   *   prompt: ask the user before backing up a non-symlink (default).
   *   force: legacy v2.0.x behaviour — backup non-symlink to .backup and replace.
   */
  async createSymlink(target, linkPath, opts = {}) {
    const mode = opts.mode || 'prompt';
    const relative = !!opts.relative;
    const fullLinkPath = path.join(this.cwd, linkPath);
    const fullTarget = relative
      ? path.relative(path.dirname(fullLinkPath), path.join(this.cwd, target))
      : path.join(this.cwd, target);

    // Use lstat (not exists, which follows symlinks) so we detect broken
    // symlinks — they fail fs.existsSync but still occupy the path and
    // would make fs.symlinkSync below throw EEXIST.
    let lstat = null;
    try { lstat = fs.lstatSync(fullLinkPath); } catch (_) { /* path not present */ }

    if (lstat) {
      const stat = lstat;
      if (stat.isSymbolicLink()) {
        const current = fs.readlinkSync(fullLinkPath);
        if (current === fullTarget) {
          // Already correct — no-op.
          return true;
        }
        // Existing symlink pointing elsewhere — replace silently
        fs.unlinkSync(fullLinkPath);
      } else {
        // Real file or directory — user customisation
        if (mode === 'safe') {
          UI.warning(`Keeping existing (not a symlink): ${linkPath} — framework will not overwrite.`);
          return false;
        }
        if (mode === 'prompt') {
          UI.warning(`${linkPath} exists and is not a symlink (likely customised).`);
          const ok = await UI.confirm(`Move it to ${linkPath}.backup and install the framework symlink?`, false);
          if (!ok) {
            UI.info(`Skipped ${linkPath}. Your customisation is preserved.`);
            return false;
          }
        }
        const backupPath = `${fullLinkPath}.backup`;
        UI.warning(`Backing up existing: ${linkPath} → ${linkPath}.backup`);
        fs.renameSync(fullLinkPath, backupPath);
      }
    }

    fs.symlinkSync(fullTarget, fullLinkPath);
    UI.success(`Symlink: ${linkPath} → ${target}`);
    return true;
  }

  ensureDirectory(dir) {
    const fullPath = path.join(this.cwd, dir);
    if (!fs.existsSync(fullPath)) {
      fs.mkdirSync(fullPath, { recursive: true });
    }
  }

  copyFile(source, dest) {
    const fullSource = path.join(this.cwd, source);
    const fullDest = path.join(this.cwd, dest);

    const destDir = path.dirname(fullDest);
    if (!fs.existsSync(destDir)) {
      fs.mkdirSync(destDir, { recursive: true });
    }

    if (fs.existsSync(fullDest)) {
      UI.warning(`Skipped (already exists): ${dest}`);
      return false;
    }

    fs.copyFileSync(fullSource, fullDest);
    UI.success(`Copied: ${dest}`);
    return true;
  }

  // -----------------------------------------------------------------------
  // Bulk symlinks (AGENTS.md, agents/, .claude/agents/, .claude/commands/)
  // -----------------------------------------------------------------------

  async createBulkSymlinks(opts = {}) {
    const mode = opts.mode || 'prompt';

    // AGENTS.md and the cross-tool `agents/` reference dir remain BULK
    // symlinks — they're single canonical artifacts of the framework that
    // do not need per-item user-overlay behaviour.
    await this.createSymlink(
      path.join(FRAMEWORK_PAYLOAD, 'AGENTS.md'),
      'AGENTS.md',
      { mode }
    );

    await this.createSymlink(
      path.join(FRAMEWORK_PAYLOAD, 'agents'),
      'agents',
      { mode }
    );

    this.ensureDirectory('.claude');

    // NB: `.claude/agents/` and `.claude/commands/` are NO LONGER bulk
    // symlinks since v3.8.0 — they're real directories with per-item
    // symlinks (or overlay-generated real files). Populated by
    // mergeAgents() and mergeCommands() which createAllSymlinks() invokes
    // right after this method.
    this._convertLegacyBulkSymlinkToDir(path.join('.claude', 'agents'));
    this._convertLegacyBulkSymlinkToDir(path.join('.claude', 'commands'));
  }

  /**
   * If `rel` exists and is a symlink (the v3.7.x and earlier bulk-symlink
   * layout for .claude/agents and .claude/commands), unlink it so that the
   * per-item merge can populate a real directory in its place. Legacy migration.
   */
  _convertLegacyBulkSymlinkToDir(rel) {
    const full = path.join(this.cwd, rel);
    let lstat = null;
    try { lstat = fs.lstatSync(full); } catch (_) { /* absent */ }
    if (lstat && lstat.isSymbolicLink()) {
      UI.warning(`Detected legacy bulk symlink at ${rel}. Converting to per-item layout (v3.8.0+).`);
      fs.unlinkSync(full);
    }
    this.ensureDirectory(rel);
  }

  // -----------------------------------------------------------------------
  // Per-item merge for .claude/skills/
  // -----------------------------------------------------------------------

  /**
   * Merge framework skills into the user's per-tool skill directories
   * (.claude/skills/, .agents/skills/, …) without ever touching user-
   * authored skills. The SAME source file under
   * `.framework/framework/.claude/skills/<skill>/` is symlinked into each
   * enabled tool's expected directory — zero duplication, both Claude and
   * Codex read the same content.
   *
   * @param {Object} opts
   * @param {string[]} opts.tools - List of enabled tool names (default: ['claude'])
   * @returns {Object} { linked: [...], skipped: [...], conflicts: [...] }
   */
  mergeSkills(opts = {}) {
    const tools = (opts.tools && opts.tools.length) ? opts.tools : ['claude'];
    const aggregate = { linked: [], skipped: [], conflicts: [] };

    const frameworkSkillsDir = path.join(this.cwd, FRAMEWORK_PAYLOAD, '.claude', 'skills');
    if (!fs.existsSync(frameworkSkillsDir)) {
      UI.warning(`No framework skills found at ${path.relative(this.cwd, frameworkSkillsDir)}. Skipping skill merge.`);
      return aggregate;
    }

    const frameworkSkills = fs.readdirSync(frameworkSkillsDir).filter(name => {
      if (name.startsWith('.')) return false;
      const full = path.join(frameworkSkillsDir, name);
      return fs.lstatSync(full).isDirectory();
    });

    for (const tool of tools) {
      const result = this._mergeSkillsForTool(tool, frameworkSkills, frameworkSkillsDir);
      aggregate.linked.push(...result.linked);
      aggregate.skipped.push(...result.skipped);
      aggregate.conflicts.push(...result.conflicts);
    }

    if (aggregate.conflicts.length > 0) {
      this.ensureDirectory('.baldart');
      const conflictPath = path.join(this.cwd, CONFLICT_LOG);
      let existing = { conflicts: [] };
      if (fs.existsSync(conflictPath)) {
        try { existing = JSON.parse(fs.readFileSync(conflictPath, 'utf8')); }
        catch (_) { /* ignore parse errors, overwrite */ }
      }
      existing.conflicts = aggregate.conflicts;
      existing.last_merge = new Date().toISOString();
      fs.writeFileSync(conflictPath, JSON.stringify(existing, null, 2) + '\n');
      UI.warning(`Recorded ${aggregate.conflicts.length} skill conflict(s) in ${CONFLICT_LOG}`);
      UI.info('Resolve each by renaming your local skill OR confirming the framework version is the one you want, then re-run `npx baldart update`.');
    }

    return aggregate;
  }

  /**
   * Per-tool skill merge — internal. Resolves the tool's target directory
   * via the adapter, handles legacy bulk-symlink conversion, then per-item
   * symlinks every framework skill into that directory.
   */
  _mergeSkillsForTool(toolName, frameworkSkills, frameworkSkillsDir) {
    const { getAdapter } = require('./tool-adapters');
    const adapter = getAdapter(toolName, this.cwd);
    const result = { linked: [], skipped: [], conflicts: [] };

    const skillsRel = adapter.skillsDir(); // e.g. ".claude/skills" or ".agents/skills"
    const userSkillsDir = path.join(this.cwd, skillsRel);

    // Legacy: if the user's skills dir is itself a bulk symlink, convert
    // back to a real directory so per-item symlinks can coexist with user skills.
    if (fs.existsSync(userSkillsDir) && fs.lstatSync(userSkillsDir).isSymbolicLink()) {
      UI.warning(`[${adapter.label}] Detected legacy bulk skills symlink at ${skillsRel}. Converting to per-item layout…`);
      fs.unlinkSync(userSkillsDir);
    }

    this.ensureDirectory(skillsRel);

    // Compute the relative path from the user's skills dir to the framework
    // source. For ".claude/skills" or ".agents/skills" both are 2 levels deep,
    // so the prefix is "../.." — but compute it generically to support adapters
    // that pick different depths.
    const skillsDirDepth = skillsRel.split(path.sep).filter(Boolean).length;
    const upPath = path.join(...Array(skillsDirDepth).fill('..'));

    frameworkSkills.forEach(name => {
      const linkPath = path.join(userSkillsDir, name);
      const target = path.join(upPath, FRAMEWORK_PAYLOAD, '.claude', 'skills', name);
      const targetAbsolute = path.join(this.cwd, FRAMEWORK_PAYLOAD, '.claude', 'skills', name);

      // Use lstat to detect broken symlinks (fs.existsSync follows links and
      // misclassifies broken ones as "missing", causing EEXIST on symlinkSync).
      let lstat = null;
      try { lstat = fs.lstatSync(linkPath); } catch (_) { /* absent */ }

      if (!lstat) {
        fs.symlinkSync(target, linkPath);
        UI.success(`[${adapter.label}] Skill linked: ${path.join(skillsRel, name)}`);
        result.linked.push({ tool: toolName, name });
        return;
      }

      if (lstat.isSymbolicLink()) {
        const current = fs.readlinkSync(linkPath);
        const resolved = path.resolve(path.dirname(linkPath), current);
        if (current === target || resolved === targetAbsolute) {
          result.skipped.push({ tool: toolName, name, reason: 'already-linked' });
          return;
        }
        // Broken-or-misaimed symlink → replace silently with the correct one
        if (!fs.existsSync(linkPath)) {
          fs.unlinkSync(linkPath);
          fs.symlinkSync(target, linkPath);
          UI.success(`[${adapter.label}] Skill re-linked (was broken): ${path.join(skillsRel, name)}`);
          result.linked.push({ tool: toolName, name });
          return;
        }
        UI.info(`[${adapter.label}] Skill kept (user override symlink): ${path.join(skillsRel, name)} → ${current}`);
        result.skipped.push({ tool: toolName, name, reason: 'user-symlink-override', target: current });
        return;
      }

      UI.warning(`[${adapter.label}] Skill name conflict: ${path.join(skillsRel, name)} already exists locally. Framework version NOT installed.`);
      result.conflicts.push({
        tool: toolName,
        name,
        local_kind: lstat.isDirectory() ? 'directory' : 'file',
        local_path: path.join(skillsRel, name),
        framework_path: path.relative(this.cwd, path.join(frameworkSkillsDir, name)),
        detected_at: new Date().toISOString()
      });
    });

    return result;
  }

  // -----------------------------------------------------------------------
  // Public composite operations
  // -----------------------------------------------------------------------

  async createAllSymlinks(opts = {}) {
    UI.section('Creating Symlinks');
    await this.createBulkSymlinks(opts);

    UI.newline();
    UI.section('Merging Framework Skills');
    this.mergeSkills({ tools: opts.tools });

    UI.newline();
    UI.section('Merging Framework Agents');
    this.mergeAgents({ tools: opts.tools });

    UI.newline();
    UI.section('Merging Framework Commands');
    this.mergeCommands({ tools: opts.tools });

    UI.newline();
    UI.section('Merging Framework Workflows');
    this.mergeWorkflows({ tools: opts.tools });

    UI.newline();
  }

  // -----------------------------------------------------------------------
  // Per-item merge for agents and commands (v3.8.0+)
  //
  // Same model as skills (per-item, user files coexist, broken-symlink-safe)
  // PLUS overlay support: if `.baldart/overlays/<kind>s/<name>.md` exists,
  // BALDART generates a real file `.claude/<kind>s/<name>.md` = base + overlay
  // merged, with a marker header so future updates know it's regeneration-safe.
  // -----------------------------------------------------------------------

  mergeAgents(opts = {}) { return this._mergeBulkDir('agent', opts); }
  mergeCommands(opts = {}) { return this._mergeBulkDir('command', opts); }
  mergeWorkflows(opts = {}) { return this._mergeBulkDir('workflow', opts); }

  /**
   * Generic per-item merge for kinds whose framework source is a directory
   * of single files (agents/commands → .md, workflows → .js). For each
   * enabled tool whose adapter declares a target directory
   * (claude.agentsDir(), claude.workflowsDir(), …), this symlinks each base
   * file or — for overlay-capable `.md` kinds with an overlay present —
   * writes a generated real file with the overlay applied. Workflows are
   * `.js` and overlay-free, so they always take the plain-symlink path.
   */
  _mergeBulkDir(kind, opts = {}) {
    const cfg = MERGE_KINDS[kind];
    if (!cfg) throw new Error(`_mergeBulkDir: unknown kind '${kind}'`);
    const tools = (opts.tools && opts.tools.length) ? opts.tools : ['claude'];
    const aggregate = { linked: [], generated: [], skipped: [], conflicts: [], warnings: [] };
    const { getAdapter } = require('./tool-adapters');

    const fwSourceDir = path.join(this.cwd, FRAMEWORK_PAYLOAD, '.claude', `${kind}s`);
    if (!fs.existsSync(fwSourceDir)) {
      UI.warning(`No framework ${kind}s found at ${path.relative(this.cwd, fwSourceDir)}. Skipping ${kind} merge.`);
      return aggregate;
    }

    const fwItems = fs.readdirSync(fwSourceDir)
      .filter((n) => n.endsWith(cfg.ext) && !n.startsWith('.') && !cfg.exclude.has(n));

    const frameworkVersion = this._readFrameworkVersion();

    for (const tool of tools) {
      const adapter = getAdapter(tool, this.cwd);
      const dirGetter = adapter[cfg.dirGetter];
      const targetRel = (typeof dirGetter === 'function') ? dirGetter.call(adapter) : null;
      if (!targetRel) continue; // tool doesn't support this kind (e.g. Codex → workflows)

      this.ensureDirectory(targetRel);

      for (const filename of fwItems) {
        const baseName = filename.slice(0, -cfg.ext.length);
        const linkPath = path.join(this.cwd, targetRel, filename);
        const fwAbsolute = path.join(fwSourceDir, filename);

        // Compute the relative-from-link path used by the symlink so it's
        // portable if the project gets moved.
        const linkDepth = targetRel.split(path.sep).filter(Boolean).length;
        const upPath = path.join(...Array(linkDepth).fill('..'));
        const target = path.join(upPath, FRAMEWORK_PAYLOAD, '.claude', `${kind}s`, filename);

        const overlayRel = path.join('.baldart', 'overlays', `${kind}s`, filename);
        const overlayAbs = path.join(this.cwd, overlayRel);
        const hasOverlay = cfg.overlay && fs.existsSync(overlayAbs);

        // Inspect what currently lives at the link path (if anything).
        let lstat = null;
        try { lstat = fs.lstatSync(linkPath); } catch (_) { /* absent */ }

        if (hasOverlay) {
          // Generate a real file from base + overlay.
          this._generateOverlayedFile({
            kind, name: baseName, fwAbsolute, overlayAbs, linkPath, lstat,
            frameworkVersion, aggregate, adapter, targetRel
          });
        } else {
          // Plain symlink, with broken-symlink recovery (same fix-pattern as v3.5.1).
          this._installPerItemSymlink({
            linkPath, target, fwAbsolute, lstat, baseName, filename,
            aggregate, adapter, targetRel, kind
          });
        }
      }
    }

    return aggregate;
  }

  _installPerItemSymlink({ linkPath, target, fwAbsolute, lstat, baseName, filename, aggregate, adapter, targetRel, kind }) {
    if (!lstat) {
      fs.symlinkSync(target, linkPath);
      UI.success(`[${adapter.label}] ${kind} linked: ${path.join(targetRel, filename)}`);
      aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
      return;
    }
    if (lstat.isSymbolicLink()) {
      const current = fs.readlinkSync(linkPath);
      const resolved = path.resolve(path.dirname(linkPath), current);
      if (current === target || resolved === fwAbsolute) {
        aggregate.skipped.push({ tool: adapter.name, kind, name: baseName, reason: 'already-linked' });
        return;
      }
      // Symlink pointing into .baldart/generated/ → BALDART-managed from a
      // previous overlay state. Overlay has just been removed, so revert to
      // the framework-base symlink and clean up the now-stale generated file.
      if (current.split(path.sep).includes('generated') && current.includes('.baldart')) {
        fs.unlinkSync(linkPath);
        fs.symlinkSync(target, linkPath);
        try {
          const staleGenerated = path.join(this.cwd, '.baldart', 'generated', `${kind}s`, filename);
          if (fs.existsSync(staleGenerated)) fs.unlinkSync(staleGenerated);
        } catch (_) { /* best effort */ }
        UI.info(`[${adapter.label}] ${kind} overlay removed — relinking ${path.join(targetRel, filename)} → framework base`);
        aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
        return;
      }
      // Broken or pointing elsewhere — replace if broken, else respect user override
      if (!fs.existsSync(linkPath)) {
        fs.unlinkSync(linkPath);
        fs.symlinkSync(target, linkPath);
        UI.success(`[${adapter.label}] ${kind} re-linked (was broken): ${path.join(targetRel, filename)}`);
        aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
        return;
      }
      UI.info(`[${adapter.label}] ${kind} kept (user override symlink): ${path.join(targetRel, filename)} → ${current}`);
      aggregate.skipped.push({ tool: adapter.name, kind, name: baseName, reason: 'user-symlink-override' });
      return;
    }
    // Real file. Could be:
    //  (a) A baldart-generated file from a previous run where overlay USED to
    //      exist but now doesn't — safe to replace with symlink.
    //      Pre-v3.27.0 layout: marker file directly at linkPath. v3.27.0+
    //      stores the merged file under .baldart/generated/ and points a
    //      symlink at it, so the "real file at linkPath" case here is the
    //      legacy layout that no longer regenerates — just unlink and resymlink.
    //  (b) A user-authored fork — leave alone.
    const { readMarker } = require('./overlay-merger');
    const content = fs.readFileSync(linkPath, 'utf8');
    const marker = readMarker(content);
    if (marker && marker.kind === kind && marker.name === baseName) {
      // Overlay was removed → revert to symlink.
      UI.info(`[${adapter.label}] ${kind} overlay removed — replacing generated file with symlink: ${path.join(targetRel, filename)}`);
      fs.unlinkSync(linkPath);
      fs.symlinkSync(target, linkPath);
      // Also clean up any stale .baldart/generated/<kind>s/<name>.md from
      // the v3.27.0+ indirect layout (no overlay means no generated file).
      try {
        const staleGenerated = path.join(this.cwd, '.baldart', 'generated', `${kind}s`, filename);
        if (fs.existsSync(staleGenerated)) fs.unlinkSync(staleGenerated);
      } catch (_) { /* best effort */ }
      aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
      return;
    }
    const specializeHint = MERGE_KINDS[kind] && MERGE_KINDS[kind].overlay
      ? ` Move user content to .baldart/overlays/${kind}s/${filename} to specialize without losing upstream updates.`
      : ` Rename your local ${kind} to keep both.`;
    UI.warning(`[${adapter.label}] ${kind} name conflict (user file at ${path.join(targetRel, filename)}). Framework version NOT installed.${specializeHint}`);
    aggregate.conflicts.push({
      tool: adapter.name, kind, name: baseName,
      local_path: path.join(targetRel, filename),
      framework_path: path.relative(this.cwd, fwAbsolute),
      detected_at: new Date().toISOString()
    });
  }

  _generateOverlayedFile({ kind, name, fwAbsolute, overlayAbs, linkPath, lstat, frameworkVersion, aggregate, adapter, targetRel }) {
    const { mergeOverlay, isGeneratedFile, computeBaseFileSha, ensureBaseFileShaInOverlay, refreshOverlayVersionPin } = require('./overlay-merger');
    const baseContent = fs.readFileSync(fwAbsolute, 'utf8');
    const baseSha = computeBaseFileSha(baseContent);
    // Backfill base_file_sha into the overlay frontmatter so `overlay drift` is
    // deterministic for EVERY overlay, not just freshly-scaffolded ones
    // (additive-only, never overwrites). Done BEFORE reading overlayContent so
    // the marker's overlay_sha already reflects it — no one-time regen churn.
    // (v3.33.0)
    try {
      const bf = ensureBaseFileShaInOverlay(overlayAbs, baseSha);
      if (bf.changed) UI.info(`[${adapter.label}] ${kind} ${name}: backfilled base_file_sha into overlay frontmatter.`);
      // Keep the informational base_<kind>_version pin truthful — but ONLY when
      // the base content is unchanged (sha matches). When the base HAS changed
      // the sha differs and the pin is left stale on purpose, so it stays a
      // legitimate drift signal to reconcile. (v4.11.0)
      const vp = refreshOverlayVersionPin(overlayAbs, kind, frameworkVersion, baseSha);
      if (vp.changed) UI.info(`[${adapter.label}] ${kind} ${name}: refreshed base_${kind}_version pin to v${frameworkVersion} (base unchanged).`);
    } catch (_) { /* non-fatal — never block a merge on backfill */ }
    const overlayContent = fs.readFileSync(overlayAbs, 'utf8');
    const merged = mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion });

    if (merged.warnings && merged.warnings.length) {
      merged.warnings.forEach((w) => {
        UI.warning(`[${adapter.label}] ${kind} ${name}: ${w}`);
        aggregate.warnings.push({ tool: adapter.name, kind, name, warning: w });
      });
    }

    // v3.27.0+ layout: the merged content lives at
    //   .baldart/generated/<kind>s/<name>.md            (real file)
    // and the consumer-visible path is a symlink:
    //   .claude/agents/<name>.md → ../../.baldart/generated/agents/<name>.md
    // This is a workaround for Claude Code upstream bug #20931, where the
    // session-start scanner that builds the `subagent_type` registry skips
    // regular files in .claude/agents/ (only symlinks are picked up). The
    // pre-v3.27.0 layout wrote the merged file directly at linkPath as a
    // real file, which made every overlay-merged agent silently invisible
    // to the orchestrator — see CHANGELOG 3.27.0 root cause analysis.
    const generatedDir = path.join(this.cwd, '.baldart', 'generated', `${kind}s`);
    const generatedPath = path.join(generatedDir, `${name}.md`);
    fs.mkdirSync(generatedDir, { recursive: true });

    // Inspect what currently sits at linkPath. Three legal states:
    //   (1) symlink we already manage (resolves into .baldart/generated/) → stale, replace
    //   (2) symlink to the framework base (no overlay last time) → upgrade to indirect symlink
    //   (3) regular file with baldart-generated marker (legacy pre-v3.27.0 layout) → migrate
    //   (4) regular file without marker → user fork, refuse to touch
    if (lstat && !lstat.isSymbolicLink()) {
      const existing = fs.readFileSync(linkPath, 'utf8');
      if (!isGeneratedFile(existing)) {
        UI.warning(`[${adapter.label}] ${kind} ${name}: existing file at ${path.join(targetRel, name)}.md has no baldart-generated marker — NOT overwriting (user fork?). Move it aside (or merge it into the overlay) and re-run update.`);
        aggregate.conflicts.push({
          tool: adapter.name, kind, name,
          local_path: path.join(targetRel, `${name}.md`),
          reason: 'manual-edit-no-marker',
          detected_at: new Date().toISOString()
        });
        return;
      }
      // Legacy generated file at linkPath — migrate (delete in-place, rewrite at generatedPath, symlink).
    }

    // Write merged content to the indirect location.
    fs.writeFileSync(generatedPath, merged.generated);

    // Compute the relative symlink target (portable across moves of the
    // project root).
    const relativeTarget = path.relative(path.dirname(linkPath), generatedPath);

    // Replace whatever currently lives at linkPath with the new symlink.
    if (lstat) {
      try { fs.unlinkSync(linkPath); }
      catch (_) { try { fs.rmSync(linkPath, { recursive: true, force: true }); } catch (_) { /* noop */ } }
    }
    fs.symlinkSync(relativeTarget, linkPath);
    UI.success(`[${adapter.label}] ${kind} generated (overlay applied): ${path.join(targetRel, name)}.md → ${relativeTarget}`);
    aggregate.generated.push({ tool: adapter.name, kind, name, overlaySha: merged.overlaySha });
  }

  _readFrameworkVersion() {
    const vPath = path.join(this.cwd, FRAMEWORK_DIR, 'VERSION');
    try { return fs.readFileSync(vPath, 'utf8').trim(); }
    catch { return 'unknown'; }
  }

  verifySymlinks() {
    const bulkSymlinks = [
      'AGENTS.md',
      'agents'
    ];

    let allValid = true;

    bulkSymlinks.forEach(link => {
      const fullPath = path.join(this.cwd, link);

      // lstat (doesn't follow symlinks) so we can distinguish "absent" from
      // "broken symlink" — the latter fails fs.existsSync but still occupies
      // the path and must be removed before recreating the link.
      let lstat = null;
      try { lstat = fs.lstatSync(fullPath); } catch (_) { /* absent */ }

      if (!lstat) {
        UI.warning(`Missing: ${link}`);
        allValid = false;
      } else if (!lstat.isSymbolicLink()) {
        UI.warning(`Not a symlink (user customisation?): ${link}`);
        allValid = false;
      } else if (!fs.existsSync(fullPath)) {
        UI.warning(`Broken symlink: ${link} → ${fs.readlinkSync(fullPath)}`);
        allValid = false;
      } else {
        UI.success(`Valid: ${link}`);
      }
    });

    // Per-tool skill directories. Read enabled tools from baldart.config.yml
    // (fallback to ['claude'] if no config yet).
    const { getAdapter } = require('./tool-adapters');
    let enabledTools = ['claude'];
    try {
      const yaml = require('js-yaml');
      const cfgPath = path.join(this.cwd, 'baldart.config.yml');
      if (fs.existsSync(cfgPath)) {
        const cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
        if (Array.isArray(cfg?.tools?.enabled) && cfg.tools.enabled.length) {
          enabledTools = cfg.tools.enabled;
        }
      }
    } catch (_) { /* keep default */ }

    for (const toolName of enabledTools) {
      let adapter;
      try { adapter = getAdapter(toolName, this.cwd); }
      catch (err) { UI.warning(err.message); allValid = false; continue; }

      const skillsRel = adapter.skillsDir();
      const skillsDir = path.join(this.cwd, skillsRel);
      if (fs.existsSync(skillsDir)) {
        const stat = fs.lstatSync(skillsDir);
        if (stat.isSymbolicLink()) {
          UI.warning(`[${adapter.label}] Legacy bulk skills symlink at ${skillsRel}. Run \`npx baldart update\` to convert to per-item.`);
          allValid = false;
        } else {
          const sample = ['skill-creator', 'frontend-design', 'bug', 'prd', 'capture'];
          let frameworkLinks = 0;
          sample.forEach(name => {
            const p = path.join(skillsDir, name);
            if (fs.existsSync(p) && fs.lstatSync(p).isSymbolicLink()) frameworkLinks++;
          });
          if (frameworkLinks === 0) {
            UI.warning(`[${adapter.label}] ${skillsRel}/ has no framework-linked skills. Run \`npx baldart update\`.`);
            allValid = false;
          } else {
            UI.success(`[${adapter.label}] Valid: ${skillsRel}/ (per-item merge, ${frameworkLinks}/${sample.length} sampled framework skills linked)`);
          }
        }
      } else {
        UI.warning(`[${adapter.label}] Missing: ${skillsRel}/ (the framework would merge skills here)`);
        allValid = false;
      }

      // v3.8.0+: agents and commands also use per-item merge with overlay support.
      // v4.14.0+: workflows (.js) use the same per-item merge, overlay-free.
      for (const [kindGetter, kindLabel] of [['agentsDir', 'agents'], ['commandsDir', 'commands'], ['workflowsDir', 'workflows']]) {
        if (typeof adapter[kindGetter] !== 'function') continue;
        const rel = adapter[kindGetter]();
        if (!rel) continue;
        const full = path.join(this.cwd, rel);
        let s = null;
        try { s = fs.lstatSync(full); } catch (_) { /* absent */ }
        if (!s) {
          UI.warning(`[${adapter.label}] Missing: ${rel}/ (framework ${kindLabel} would be merged here)`);
          allValid = false;
        } else if (s.isSymbolicLink()) {
          UI.warning(`[${adapter.label}] Legacy bulk symlink at ${rel}. Run \`npx baldart update\` to convert to per-item layout.`);
          allValid = false;
        } else {
          const { isGeneratedFile } = require('./overlay-merger');
          const entries = fs.readdirSync(full);
          const frameworkOwned = entries.filter((n) => {
            const p = path.join(full, n);
            try {
              if (fs.lstatSync(p).isSymbolicLink()) return true;
              return isGeneratedFile(fs.readFileSync(p, 'utf8'));
            } catch { return false; }
          });
          if (frameworkOwned.length === 0) {
            UI.warning(`[${adapter.label}] ${rel}/ has no framework-managed ${kindLabel}. Run \`npx baldart update\`.`);
            allValid = false;
          } else {
            UI.success(`[${adapter.label}] Valid: ${rel}/ (${frameworkOwned.length} framework-managed ${kindLabel})`);
          }
        }
      }
    }

    // Project configuration (v3.0.0+)
    const configPath = path.join(this.cwd, 'baldart.config.yml');
    if (!fs.existsSync(configPath)) {
      UI.warning('Missing: baldart.config.yml — skills will prompt for paths on every invocation. Run `npx baldart configure`.');
      allValid = false;
    } else {
      // Check it parses + has a version field — full schema validation is the
      // configure command's job, this is a smoke test.
      try {
        const yaml = require('js-yaml');
        const cfg = yaml.load(fs.readFileSync(configPath, 'utf8'));
        if (!cfg || typeof cfg !== 'object') throw new Error('not an object');
        if (typeof cfg.version !== 'number') throw new Error('missing version');
        UI.success(`Valid: baldart.config.yml (schema v${cfg.version})`);
      } catch (err) {
        UI.warning(`baldart.config.yml is malformed (${err.message}). Re-run \`npx baldart configure\`.`);
        allValid = false;
      }
    }

    // Overlays dir (v3.0.0+)
    const overlaysDir = path.join(this.cwd, '.baldart', 'overlays');
    if (!fs.existsSync(overlaysDir)) {
      UI.warning('Missing: .baldart/overlays/ — run `npx baldart configure` to create it.');
      allValid = false;
    } else {
      UI.success('Valid: .baldart/overlays/');
    }

    return allValid;
  }

  copyCustomizableFiles() {
    UI.section('Copying Customizable Templates');

    // Hooks
    this.ensureDirectory('.claude/hooks');
    this.copyFile(
      path.join(FRAMEWORK_PAYLOAD, '.claude', 'hooks', 'lint-before-commit.sh.template'),
      path.join('.claude', 'hooks', 'lint-before-commit.sh.template')
    );

    // UI guidelines
    this.ensureDirectory('docs/references');
    this.copyFile(
      path.join(FRAMEWORK_PAYLOAD, 'docs', 'references', 'ui-guidelines.template.md'),
      path.join('docs', 'references', 'ui-guidelines.template.md')
    );

    this.copyFile(
      path.join(FRAMEWORK_PAYLOAD, 'docs', 'references', 'brand-guidelines.md'),
      path.join('docs', 'references', 'brand-guidelines.md')
    );

    // Templates — only user-edit backlog/spec templates.
    // Skip BALDART-internal templates (config schema, snippet, overlay examples)
    // which live in `.framework/templates/` and are consumed by CLI commands,
    // not by humans.
    const FRAMEWORK_INTERNAL = new Set([
      'baldart.config.template.yml',
      'skill-project-context.snippet.md',
      'overlays', // example overlays directory — consumers reference, don't copy wholesale
    ]);

    this.ensureDirectory('templates');
    const templatesDir = path.join(this.cwd, FRAMEWORK_PAYLOAD, 'templates');
    if (fs.existsSync(templatesDir)) {
      const templateFiles = fs.readdirSync(templatesDir);
      templateFiles.forEach(file => {
        if (FRAMEWORK_INTERNAL.has(file)) return;
        const src = path.join(this.cwd, FRAMEWORK_PAYLOAD, 'templates', file);
        if (fs.lstatSync(src).isDirectory()) return;
        this.copyFile(
          path.join(FRAMEWORK_PAYLOAD, 'templates', file),
          path.join('templates', file)
        );
      });
    }

    // Ensure consumer-owned overlay directory exists (real dir, never a symlink).
    // The configure command also creates this; this branch covers installs that
    // skip configure.
    const overlaysDir = path.join(this.cwd, '.baldart', 'overlays');
    if (!fs.existsSync(overlaysDir)) {
      fs.mkdirSync(overlaysDir, { recursive: true });
      fs.writeFileSync(
        path.join(overlaysDir, '.gitkeep'),
        '# Skill overlays — see framework/agents/project-context.md § 5\n',
        'utf8'
      );
      UI.success('Created .baldart/overlays/');
    }

    UI.newline();
  }
}

module.exports = SymlinkUtils;
