// engine/hook-generator/hook-merger.ts — Idempotent hook file merge engine (SPEC-416)
import { readFile, writeFile, access, mkdir } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { markerStart, markerEnd } from './stack-hook-templates.js';
// ---------------------------------------------------------------------------
// File path resolution
// ---------------------------------------------------------------------------
/**
 * Resolves the path to the hook file for a given phase.
 * Prefers .husky/ when the directory exists, falls back to .git/hooks/.
 */
async function hookFilePath(projectPath, phase) {
    const huskyDir = join(projectPath, '.husky');
    try {
        await access(huskyDir);
        return join(huskyDir, phase);
    }
    catch {
        return join(projectPath, '.git', 'hooks', phase);
    }
}
// ---------------------------------------------------------------------------
// File I/O helpers
// ---------------------------------------------------------------------------
/** Reads an existing hook file. Returns null if it does not exist. */
async function readHookFile(filePath) {
    try {
        return await readFile(filePath, 'utf8');
    }
    catch {
        return null;
    }
}
/** Returns a minimal shell script header for new hook files. */
function shellHeader() {
    return '#!/usr/bin/env sh\n';
}
/**
 * Injects a HookSection into existing hook file content using planu markers.
 *
 * - If markers already present with IDENTICAL body → 'skipped'
 * - If markers present with different body → 'updated' (replaces between markers)
 * - If no markers → 'added' (appends block at end)
 */
function injectSection(content, section) {
    const start = markerStart(section.id);
    const end = markerEnd(section.id);
    const block = [`\n${start}`, `# ${section.description}`, section.content, end].join('\n');
    const startIdx = content.indexOf(start);
    const endIdx = content.indexOf(end);
    if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
        // Markers exist — extract current body between them
        const currentBlock = content.slice(startIdx, endIdx + end.length);
        if (currentBlock === block.trimStart()) {
            return { content, action: 'skipped' };
        }
        // Replace the existing block
        const updated = content.slice(0, startIdx) + block.trimStart() + content.slice(endIdx + end.length);
        return { content: updated, action: 'updated' };
    }
    // No markers — append
    const sep = content.endsWith('\n') ? '' : '\n';
    return { content: content + sep + block + '\n', action: 'added' };
}
// ---------------------------------------------------------------------------
// Group sections by phase
// ---------------------------------------------------------------------------
function groupByPhase(sections) {
    const map = new Map();
    for (const section of sections) {
        const existing = map.get(section.phase) ?? [];
        existing.push(section);
        map.set(section.phase, existing);
    }
    return map;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Previews what mergeHooks would do WITHOUT writing any files.
 */
export async function previewHooks(projectPath, sections) {
    const grouped = groupByPhase(sections);
    const previews = [];
    for (const [phase, phaseSections] of grouped) {
        const filePath = await hookFilePath(projectPath, phase);
        const currentContent = await readHookFile(filePath);
        const conflicts = [];
        const proposedAdditions = [];
        for (const section of phaseSections) {
            const start = markerStart(section.id);
            if (currentContent?.includes(start) === true) {
                conflicts.push(section.id);
            }
            else {
                proposedAdditions.push(section);
            }
        }
        previews.push({
            phase,
            filePath,
            currentContent,
            proposedAdditions,
            wouldModify: proposedAdditions.length > 0 || conflicts.length > 0,
            conflicts,
        });
    }
    return previews;
}
/**
 * Merges HookSection entries into the appropriate hook files.
 * Uses idempotent markers so manual edits outside markers are preserved.
 *
 * @param dryRun When true, computes results but does NOT write to disk.
 */
export async function mergeHooks(projectPath, sections, dryRun) {
    const grouped = groupByPhase(sections);
    const results = [];
    for (const [phase, phaseSections] of grouped) {
        const filePath = await hookFilePath(projectPath, phase);
        const existing = await readHookFile(filePath);
        let content = existing ?? shellHeader();
        const sectionsAdded = [];
        const sectionsUpdated = [];
        const sectionsSkipped = [];
        for (const section of phaseSections) {
            const outcome = injectSection(content, section);
            content = outcome.content;
            if (outcome.action === 'added') {
                sectionsAdded.push(section.id);
            }
            else if (outcome.action === 'updated') {
                sectionsUpdated.push(section.id);
            }
            else {
                sectionsSkipped.push(section.id);
            }
        }
        if (!dryRun && (sectionsAdded.length > 0 || sectionsUpdated.length > 0)) {
            await mkdir(dirname(filePath), { recursive: true });
            await writeFile(filePath, content, { encoding: 'utf8', mode: 0o755 });
        }
        results.push({ filePath, sectionsAdded, sectionsUpdated, sectionsSkipped, content });
    }
    return results;
}
//# sourceMappingURL=hook-merger.js.map