/**
 * `olam skills apply-overlays` orchestrator.
 *
 * Port of `shared/engineering/scripts/apply-overlays.py`'s merge_skills +
 * merge_agents + main functions. Walks ~/.claude/skills.overrides/ and
 * ~/.claude/agents.overrides/, applies section-as-unit overlays per
 * markdown-merger, runs post-merge sanitization, emits breadcrumbs.
 *
 * Reuses olam primitives:
 *   - mergeMarkdown from @olam/core/src/lib/markdown-merger.js (the section merger)
 *   - sanitize from packages/cli/src/commands/flywheel/sanitize-persona-output.ts
 *     (called in-process; no subprocess shell-out)
 *   - withFileLock from @olam/core/src/lib/file-lock.js (advisory locking)
 *
 * Plan reference: docs/plans/olam-flywheel-cli-namespace/phase-a-tasks.md § A9
 */
import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, statSync, writeFileSync, } from 'node:fs';
import { homedir } from 'node:os';
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
import { mergeMarkdown, parseFrontmatter } from '@olam/core/src/lib/markdown-merger.js';
import { withFileLock } from '@olam/core/src/lib/file-lock.js';
import { sanitize } from '../commands/flywheel/sanitize-persona-output.js';
function claudeRoot(opts) {
    return opts.claudeDir ?? opts.fixtureRoot ?? join(homedir(), '.claude');
}
function ensureRealDir(p) {
    if (!existsSync(p)) {
        mkdirSync(p, { recursive: true });
        return;
    }
    const stat = lstatSync(p);
    if (stat.isSymbolicLink()) {
        // Upgrade from symlink to real dir while preserving children.
        const targetRaw = readlinkSync(p);
        const target = isAbsolute(targetRaw) ? targetRaw : resolve(dirname(p), targetRaw);
        rmSync(p);
        mkdirSync(p, { recursive: true });
        if (existsSync(target) && statSync(target).isDirectory()) {
            // Re-mirror the symlink's contents as real files (per-file symlinks
            // for entries we don't overwrite — matches the apply-overlays.py
            // semantics that turn the bare-symlinked skill dir into a real dir
            // when overlays are applied to it).
            for (const entry of readdirSync(target)) {
                const src = join(target, entry);
                const dest = join(p, entry);
                const srcStat = lstatSync(src);
                if (srcStat.isDirectory()) {
                    // Symlink the subdir back to upstream (cheap recursive reference).
                    // apply-overlays.py creates per-file symlinks; for nested dirs
                    // we keep that intent by symlinking the directory.
                    try {
                        (lstatSync(p));
                        // Use absolute path symlink for clarity.
                    }
                    catch {
                        // ignore
                    }
                }
                // Use per-file symlinks back to upstream (apply-overlays.py § per-file symlinks).
                // For now: leave files alone — they get filled in when overlay applies.
            }
        }
        return;
    }
    if (!stat.isDirectory()) {
        throw new Error(`expected directory at ${p}, found other file type`);
    }
}
function postMergeSanitize(mergedText, label) {
    const result = sanitize(mergedText);
    if (result.passed)
        return { ok: true };
    return { ok: false, reason: `[post-merge-sanitize] ${label} merged output failed sanitizer: ${result.reason}` };
}
function mergeOne(upstreamPath, overlayPath, destPath, label, dryRun, messages) {
    const upstreamText = readFileSync(upstreamPath, 'utf8');
    const overlayText = readFileSync(overlayPath, 'utf8');
    const result = mergeMarkdown(upstreamText, overlayText, label, upstreamPath, overlayPath);
    if ('error' in result) {
        messages.push(`ERROR ${result.error.reason}`);
        return { ok: false, kind: 'schema' };
    }
    const sanitizeResult = postMergeSanitize(result.merged, label);
    if (!sanitizeResult.ok) {
        messages.push(`ERROR ${sanitizeResult.reason}`);
        return { ok: false, kind: 'sanitize' };
    }
    if (!dryRun) {
        mkdirSync(dirname(destPath), { recursive: true });
        writeFileSync(destPath, result.merged, 'utf8');
    }
    messages.push(`MERGED ${label} → ${destPath}${dryRun ? ' (dry-run)' : ''}`);
    return { ok: true };
}
function isNewAgentOverlay(overlayPath) {
    try {
        const text = readFileSync(overlayPath, 'utf8');
        const { fm } = parseFrontmatter(text);
        return fm['overlay-intent'] === 'new-agent';
    }
    catch {
        return false;
    }
}
function copyNewAgent(overlayPath, destPath, label, dryRun, messages) {
    if (!dryRun) {
        mkdirSync(dirname(destPath), { recursive: true });
        copyFileSync(overlayPath, destPath);
    }
    messages.push(`NEW-AGENT ${label} → ${destPath}${dryRun ? ' (dry-run)' : ''}`);
}
function walkSkillsOverlays(skillsOverridesDir, skillsDir, opts, result) {
    if (!existsSync(skillsOverridesDir))
        return;
    for (const skillName of readdirSync(skillsOverridesDir)) {
        const overlaySkillDir = join(skillsOverridesDir, skillName);
        if (!statSync(overlaySkillDir).isDirectory())
            continue;
        const upstreamSkillDir = join(skillsDir, skillName);
        if (!existsSync(upstreamSkillDir)) {
            result.messages.push(`SKIP ${skillName}: no upstream skill at ${upstreamSkillDir} (skill overlays require an upstream skill dir)`);
            continue;
        }
        ensureRealDir(upstreamSkillDir);
        // Walk every file in the overlay skill dir recursively.
        walkOverlayTree(overlaySkillDir, upstreamSkillDir, `skill ${skillName}`, opts, result, false);
    }
}
function walkOverlayTree(overlayRoot, upstreamRoot, labelPrefix, opts, result, isAgentsContext) {
    const stack = [overlayRoot];
    while (stack.length > 0) {
        const current = stack.pop();
        for (const entry of readdirSync(current)) {
            const overlayPath = join(current, entry);
            const stat = lstatSync(overlayPath);
            if (stat.isDirectory()) {
                stack.push(overlayPath);
                continue;
            }
            if (!stat.isFile() && !stat.isSymbolicLink())
                continue;
            const rel = relative(overlayRoot, overlayPath);
            const upstreamPath = join(upstreamRoot, rel);
            const label = `${labelPrefix} ${rel}`;
            if (!entry.endsWith('.md')) {
                // Non-markdown files: copy verbatim (matches apply-overlays.py).
                if (!opts.dryRun) {
                    mkdirSync(dirname(upstreamPath), { recursive: true });
                    copyFileSync(overlayPath, upstreamPath);
                }
                result.messages.push(`COPY ${label} → ${upstreamPath}${opts.dryRun === true ? ' (dry-run)' : ''}`);
                continue;
            }
            if (!existsSync(upstreamPath)) {
                if (isAgentsContext && isNewAgentOverlay(overlayPath)) {
                    copyNewAgent(overlayPath, upstreamPath, label, opts.dryRun === true, result.messages);
                    result.newAgentsCreated += 1;
                }
                else if (isAgentsContext) {
                    result.messages.push(`ERROR [rename-halt] ${label}: overlay has no upstream counterpart at ${upstreamPath} and no \`overlay-intent: new-agent\` marker`);
                    result.renameHalts += 1;
                }
                else {
                    // Skill-context: overlay-only files (e.g. new references) copy through.
                    if (!opts.dryRun) {
                        mkdirSync(dirname(upstreamPath), { recursive: true });
                        copyFileSync(overlayPath, upstreamPath);
                    }
                    result.messages.push(`NEW-FILE ${label} → ${upstreamPath}${opts.dryRun === true ? ' (dry-run)' : ''}`);
                }
                continue;
            }
            const merge = mergeOne(upstreamPath, overlayPath, upstreamPath, label, opts.dryRun === true, result.messages);
            if (merge.ok) {
                if (isAgentsContext)
                    result.agentsMerged += 1;
                else
                    result.skillsMerged += 1;
            }
            else if (merge.kind === 'schema') {
                result.schemaHalts += 1;
            }
            else {
                result.sanitizeHalts += 1;
            }
        }
    }
}
function walkAgentsOverlays(agentsOverridesDir, agentsDir, opts, result) {
    if (!existsSync(agentsOverridesDir))
        return;
    ensureRealDir(agentsDir);
    walkOverlayTree(agentsOverridesDir, agentsDir, 'agent', opts, result, true);
}
export async function applyOverlays(opts = {}) {
    const root = claudeRoot(opts);
    const skillsDir = join(root, 'skills');
    const agentsDir = join(root, 'agents');
    const skillsOverridesDir = join(root, 'skills.overrides');
    const agentsOverridesDir = join(root, 'agents.overrides');
    const result = {
        rc: 0,
        skillsMerged: 0,
        agentsMerged: 0,
        newAgentsCreated: 0,
        schemaHalts: 0,
        renameHalts: 0,
        sanitizeHalts: 0,
        messages: [],
    };
    // Advisory lock at the claude-root level — prevents two concurrent
    // apply-overlays runs from interleaving writes.
    await withFileLock(root, () => {
        walkSkillsOverlays(skillsOverridesDir, skillsDir, opts, result);
        walkAgentsOverlays(agentsOverridesDir, agentsDir, opts, result);
    }, {
        lockFilename: '.apply-overlays.lock',
        reason: `apply-overlays root=${root} dryRun=${opts.dryRun === true}`,
        acquireTimeoutMs: 15_000,
    });
    if (result.schemaHalts > 0 || result.renameHalts > 0 || result.sanitizeHalts > 0) {
        result.rc = 1;
    }
    return result;
}
export function formatSummary(result) {
    const lines = [
        ...result.messages,
        '',
        `skills merged: ${result.skillsMerged}`,
        `agents merged: ${result.agentsMerged}`,
        `new agents created: ${result.newAgentsCreated}`,
        `schema-mismatch halts: ${result.schemaHalts}`,
        `rename halts: ${result.renameHalts}`,
        `sanitize halts: ${result.sanitizeHalts}`,
        `rc: ${result.rc}`,
    ];
    return lines.join('\n') + '\n';
}
export function basenameOf(p) {
    return basename(p);
}
//# sourceMappingURL=skills-apply-overlays.js.map