// engine/hooks-reconciler.ts — Detect drift between CI/quality pipeline and git hooks.
// Read-only analysis by default; patchHooks() writes fixes when autoFix is enabled.
import { readFileSync, existsSync, readdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function readFileSafe(path) {
    try {
        return readFileSync(path, 'utf-8');
    }
    catch {
        return '';
    }
}
function readPackageJson(projectPath) {
    try {
        return JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf-8'));
    }
    catch {
        return {};
    }
}
function detectHookManager(projectPath, pkg) {
    if (existsSync(join(projectPath, '.husky'))) {
        return 'husky';
    }
    if (existsSync(join(projectPath, 'lefthook.yml')) ||
        existsSync(join(projectPath, 'lefthook.yaml'))) {
        return 'lefthook';
    }
    const sgh = pkg['simple-git-hooks'];
    if (sgh && typeof sgh === 'object' && Object.keys(sgh).length > 0) {
        return 'simple-git-hooks';
    }
    return 'native';
}
function detectCapabilities(projectPath, pkg) {
    const allDeps = {
        ...(typeof pkg.dependencies === 'object' && pkg.dependencies !== null
            ? pkg.dependencies
            : {}),
        ...(typeof pkg.devDependencies === 'object' && pkg.devDependencies !== null
            ? pkg.devDependencies
            : {}),
    };
    const hasTypeScript = 'typescript' in allDeps ||
        existsSync(join(projectPath, 'tsconfig.json')) ||
        existsSync(join(projectPath, 'tsconfig.build.json'));
    const hasEsLint = 'eslint' in allDeps ||
        existsSync(join(projectPath, 'eslint.config.js')) ||
        existsSync(join(projectPath, '.eslintrc.js')) ||
        existsSync(join(projectPath, '.eslintrc.json'));
    const hasTestRunner = 'vitest' in allDeps ||
        'jest' in allDeps ||
        'mocha' in allDeps ||
        existsSync(join(projectPath, 'vitest.config.ts')) ||
        existsSync(join(projectPath, 'jest.config.js'));
    const hasCommitlint = '@commitlint/cli' in allDeps ||
        existsSync(join(projectPath, 'commitlint.config.js')) ||
        existsSync(join(projectPath, 'commitlint.config.ts')) ||
        existsSync(join(projectPath, '.commitlintrc.json'));
    const hasPrettier = 'prettier' in allDeps ||
        existsSync(join(projectPath, 'prettier.config.js')) ||
        existsSync(join(projectPath, '.prettierrc'));
    return { hasTypeScript, hasEsLint, hasTestRunner, hasCommitlint, hasPrettier };
}
function readHookScripts(projectPath, manager, pkg) {
    if (manager === 'husky') {
        const huskyDir = join(projectPath, '.husky');
        return {
            'pre-commit': readFileSafe(join(huskyDir, 'pre-commit')),
            'commit-msg': readFileSafe(join(huskyDir, 'commit-msg')),
            'pre-push': readFileSafe(join(huskyDir, 'pre-push')),
        };
    }
    if (manager === 'lefthook') {
        const raw = readFileSafe(join(projectPath, 'lefthook.yml')) ||
            readFileSafe(join(projectPath, 'lefthook.yaml'));
        // For lefthook, return the full YAML as all three "scripts" for keyword scanning
        return { 'pre-commit': raw, 'commit-msg': raw, 'pre-push': raw };
    }
    if (manager === 'simple-git-hooks') {
        const sgh = (pkg['simple-git-hooks'] ?? {});
        return {
            'pre-commit': sgh['pre-commit'] ?? '',
            'commit-msg': sgh['commit-msg'] ?? '',
            'pre-push': sgh['pre-push'] ?? '',
        };
    }
    // native — read from .git/hooks/
    const hooksDir = join(projectPath, '.git', 'hooks');
    return {
        'pre-commit': readFileSafe(join(hooksDir, 'pre-commit')),
        'commit-msg': readFileSafe(join(hooksDir, 'commit-msg')),
        'pre-push': readFileSafe(join(hooksDir, 'pre-push')),
    };
}
function hasHuskyDir(projectPath) {
    try {
        return readdirSync(join(projectPath, '.husky')).length > 0;
    }
    catch {
        return false;
    }
}
// ---------------------------------------------------------------------------
// Gap detection
// ---------------------------------------------------------------------------
function detectMissingChecks(scripts, caps, manager) {
    const missing = [];
    const preCommit = scripts['pre-commit'];
    const commitMsg = scripts['commit-msg'];
    const prePush = scripts['pre-push'];
    const hasPreCommit = preCommit.length > 0;
    const hasPrePush = prePush.length > 0;
    const hasCommitMsg = commitMsg.length > 0;
    if (!hasPreCommit && !hasPrePush && !hasCommitMsg && manager !== 'native') {
        missing.push('No hook scripts found — run configure_hooks or setup-hooks to initialize');
        return missing;
    }
    if (caps.hasTypeScript && hasPreCommit && !/typecheck|tsc/.test(preCommit)) {
        missing.push('pre-commit: TypeScript detected but no typecheck step (tsc / pnpm typecheck)');
    }
    if (caps.hasEsLint && hasPreCommit && !/eslint|lint/.test(preCommit)) {
        missing.push('pre-commit: ESLint detected but no lint step');
    }
    if (caps.hasTestRunner && hasPrePush && !/vitest|jest|mocha|test/.test(prePush)) {
        missing.push('pre-push: test runner detected but no test execution step');
    }
    if (caps.hasCommitlint && !hasCommitMsg) {
        missing.push('commit-msg: commitlint detected but no commit-msg hook found');
    }
    if (caps.hasCommitlint && hasCommitMsg && !commitMsg.includes('commitlint')) {
        missing.push('commit-msg: commitlint detected but hook does not invoke commitlint');
    }
    return missing;
}
function detectOutdatedChecks(scripts, caps) {
    const outdated = [];
    const preCommit = scripts['pre-commit'];
    const prePush = scripts['pre-push'];
    if (caps.hasPrettier && preCommit.includes('lint') && !preCommit.includes('prettier')) {
        outdated.push('pre-commit: Prettier detected — consider adding a format check alongside lint');
    }
    if (caps.hasTypeScript &&
        caps.hasTestRunner &&
        /typecheck|tsc/.test(preCommit) &&
        !prePush.includes('test')) {
        outdated.push('pre-push: TypeScript + test runner present — consider running tests on pre-push');
    }
    return outdated;
}
// ---------------------------------------------------------------------------
// Lint-staged detection helpers
// ---------------------------------------------------------------------------
function lintStagedCoversPrettier(pkg) {
    const ls = pkg['lint-staged'];
    if (!ls || typeof ls !== 'object') {
        return false;
    }
    const entries = Object.values(ls);
    return entries.some((v) => {
        if (typeof v === 'string') {
            return v.includes('prettier');
        }
        if (Array.isArray(v)) {
            return v.some((cmd) => typeof cmd === 'string' && cmd.includes('prettier'));
        }
        return false;
    });
}
function lintStagedCoversEslint(pkg) {
    const ls = pkg['lint-staged'];
    if (!ls || typeof ls !== 'object') {
        return false;
    }
    const entries = Object.values(ls);
    return entries.some((v) => {
        if (typeof v === 'string') {
            return /eslint|lint/.test(v);
        }
        if (Array.isArray(v)) {
            return v.some((cmd) => typeof cmd === 'string' && /eslint|lint/.test(cmd));
        }
        return false;
    });
}
// ---------------------------------------------------------------------------
// Patch helpers
// ---------------------------------------------------------------------------
const HUSKY_SHEBANG = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n';
function readHookFile(hookPath) {
    try {
        return readFileSync(hookPath, 'utf-8');
    }
    catch {
        return '';
    }
}
function writeHookFile(hookPath, content) {
    writeFileSync(hookPath, content, { encoding: 'utf-8', mode: 0o755 });
}
function buildPatchCandidates(missingChecks, pkg) {
    const candidates = [];
    const prettierCoveredByLintStaged = lintStagedCoversPrettier(pkg);
    const eslintCoveredByLintStaged = lintStagedCoversEslint(pkg);
    for (const check of missingChecks) {
        if (check.includes('TypeScript') && check.includes('pre-commit')) {
            candidates.push({ hook: 'pre-commit', line: 'pnpm typecheck', description: check });
        }
        else if (check.includes('ESLint') && check.includes('pre-commit')) {
            if (!eslintCoveredByLintStaged) {
                candidates.push({ hook: 'pre-commit', line: 'pnpm lint', description: check });
            }
        }
        else if (check.includes('test') && check.includes('pre-push')) {
            candidates.push({ hook: 'pre-push', line: 'pnpm test', description: check });
        }
        else if (check.includes('commit-msg') && check.includes('no commit-msg hook')) {
            candidates.push({
                hook: 'commit-msg',
                line: 'npx --no -- commitlint --edit "$1"',
                description: check,
            });
        }
        else if (check.includes('commit-msg') && check.includes('does not invoke commitlint')) {
            candidates.push({
                hook: 'commit-msg',
                line: 'npx --no -- commitlint --edit "$1"',
                description: check,
            });
        }
        else if (check.includes('prettier') && !prettierCoveredByLintStaged) {
            candidates.push({
                hook: 'pre-commit',
                line: 'npx prettier --check .',
                description: check,
            });
        }
    }
    return candidates;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Auto-patch husky hook files with missing quality checks.
 * Idempotent: checks already present in the file are reported as skipped.
 * Only works for husky manager — returns empty patch for other managers.
 */
export function patchHooks(missingChecks, projectPath) {
    const pkg = readPackageJson(projectPath);
    const manager = detectHookManager(projectPath, pkg);
    if (manager !== 'husky') {
        return { patched: [], skipped: missingChecks.slice(), healthScore: 0 };
    }
    const huskyDir = join(projectPath, '.husky');
    const candidates = buildPatchCandidates(missingChecks, pkg);
    const patched = [];
    const skipped = [];
    for (const candidate of candidates) {
        const hookPath = join(huskyDir, candidate.hook);
        let current = readHookFile(hookPath);
        if (current.includes(candidate.line)) {
            skipped.push(candidate.description);
            continue;
        }
        if (current === '') {
            current = HUSKY_SHEBANG;
        }
        const updated = current.endsWith('\n')
            ? `${current}${candidate.line}\n`
            : `${current}\n${candidate.line}\n`;
        writeHookFile(hookPath, updated);
        patched.push({ hook: candidate.hook, added: candidate.line });
    }
    // Re-run analysis to get updated health score
    const afterReconcile = reconcileHooks(projectPath);
    return { patched, skipped, healthScore: afterReconcile.healthScore };
}
/**
 * Analyse git hooks against detected project capabilities and report drift.
 * Never throws — returns a zero-score result on unexpected errors.
 */
export function reconcileHooks(projectPath) {
    try {
        const pkg = readPackageJson(projectPath);
        const manager = detectHookManager(projectPath, pkg);
        const caps = detectCapabilities(projectPath, pkg);
        const scripts = readHookScripts(projectPath, manager, pkg);
        // If husky is configured but directory is empty, report that
        if (manager === 'husky' && !hasHuskyDir(projectPath)) {
            return {
                hooksInSync: false,
                missingHooks: ['Husky directory exists but contains no hook scripts'],
                outdatedHooks: [],
                healthScore: 0,
            };
        }
        const missingHooks = detectMissingChecks(scripts, caps, manager);
        const outdatedHooks = detectOutdatedChecks(scripts, caps);
        const totalIssues = missingHooks.length + outdatedHooks.length;
        const maxExpected = (caps.hasTypeScript ? 1 : 0) +
            (caps.hasEsLint ? 1 : 0) +
            (caps.hasTestRunner ? 1 : 0) +
            (caps.hasCommitlint ? 2 : 0) +
            (caps.hasPrettier ? 1 : 0);
        const healthScore = maxExpected === 0
            ? 100
            : Math.max(0, Math.round(((maxExpected - totalIssues) / maxExpected) * 100));
        return {
            hooksInSync: totalIssues === 0,
            missingHooks,
            outdatedHooks,
            healthScore,
        };
    }
    catch {
        return {
            hooksInSync: false,
            missingHooks: [],
            outdatedHooks: [],
            healthScore: 0,
        };
    }
}
//# sourceMappingURL=hooks-reconciler.js.map