import { execFile } from 'node:child_process';
import { chmod, lstat, readFile, readlink, rm, writeFile, } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { promisify } from 'node:util';
import { ensureDir } from '../fs/io.js';
const execFileAsync = promisify(execFile);
export const HOOK_MARKER_START = '# >>> oat pre-commit hook >>>';
export const HOOK_MARKER_END = '# <<< oat pre-commit hook <<<';
export const HOOK_DRIFT_WARNING = "oat: managed provider views are out of sync - run 'oat sync --scope project'";
export const HOOK_STRAY_INFO = "oat: unmanaged provider files detected - run 'oat status --scope project' to review";
export const REPO_GITHOOKS_PATH = '.githooks';
function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function createHookSnippet(options = {}) {
    const lines = [
        HOOK_MARKER_START,
        'if command -v oat >/dev/null 2>&1; then',
        '  oat status --scope project --hook || true',
        'fi',
        HOOK_MARKER_END,
    ];
    if (!options.includeShebang) {
        return lines.join('\n');
    }
    return ['#!/bin/sh', '', ...lines].join('\n');
}
async function resolveGitDirectory(projectRoot) {
    const gitPath = join(projectRoot, '.git');
    try {
        const gitStat = await lstat(gitPath);
        if (gitStat.isSymbolicLink()) {
            const symlinkTarget = await readlink(gitPath);
            return resolve(dirname(gitPath), symlinkTarget);
        }
        if (gitStat.isFile()) {
            const content = await readFile(gitPath, 'utf8');
            const match = /^gitdir:\s*(.+)$/m.exec(content.trim());
            if (match?.[1]) {
                return resolve(projectRoot, match[1].trim());
            }
        }
    }
    catch (error) {
        if (typeof error === 'object' &&
            error !== null &&
            'code' in error &&
            error.code === 'ENOENT') {
            return gitPath;
        }
        throw error;
    }
    return gitPath;
}
async function resolveConfiguredHooksPath(projectRoot) {
    try {
        const { stdout } = await execFileAsync('git', ['config', '--get', 'core.hooksPath'], { cwd: projectRoot });
        const configured = stdout.trim();
        if (configured) {
            return resolve(projectRoot, configured);
        }
    }
    catch {
        // Not set or git not available — fall through to default resolution.
    }
    return null;
}
async function resolveHooksDirectory(projectRoot, options = {}) {
    const shouldCreate = options.createIfMissing ?? false;
    const configuredPath = await resolveConfiguredHooksPath(projectRoot);
    if (configuredPath) {
        if (shouldCreate) {
            await ensureDir(configuredPath);
        }
        return configuredPath;
    }
    try {
        const { stdout } = await execFileAsync('git', ['rev-parse', '--path-format=absolute', '--git-path', 'hooks'], { cwd: projectRoot });
        const gitHooksDir = stdout.trim();
        if (gitHooksDir) {
            if (shouldCreate) {
                await ensureDir(gitHooksDir);
            }
            return gitHooksDir;
        }
    }
    catch {
        // Fall back to filesystem resolution for tests and nonstandard fixtures.
    }
    const gitDir = await resolveGitDirectory(projectRoot);
    const hooksDir = join(gitDir, 'hooks');
    try {
        const hooksStat = await lstat(hooksDir);
        if (!hooksStat.isSymbolicLink()) {
            return hooksDir;
        }
        const symlinkTarget = await readlink(hooksDir);
        const resolvedTarget = resolve(dirname(hooksDir), symlinkTarget);
        if (shouldCreate) {
            await ensureDir(resolvedTarget);
        }
        return resolvedTarget;
    }
    catch (error) {
        if (typeof error === 'object' &&
            error !== null &&
            'code' in error &&
            error.code === 'ENOENT') {
            if (shouldCreate) {
                await ensureDir(hooksDir);
            }
            return hooksDir;
        }
        throw error;
    }
}
export async function getHookInstallInfo(projectRoot) {
    const hooksDir = await resolveHooksDirectory(projectRoot);
    const hookPath = join(hooksDir, 'pre-commit');
    const suggestedHookPath = join(projectRoot, REPO_GITHOOKS_PATH, 'pre-commit');
    let suggestedHooksPath = null;
    if (!(await resolveConfiguredHooksPath(projectRoot))) {
        try {
            await lstat(suggestedHookPath);
            suggestedHooksPath = REPO_GITHOOKS_PATH;
        }
        catch (error) {
            if (typeof error === 'object' &&
                error !== null &&
                'code' in error &&
                error.code !== 'ENOENT') {
                throw error;
            }
        }
    }
    return {
        hookPath,
        suggestedHooksPath,
        suggestedHookPath: suggestedHooksPath ? suggestedHookPath : null,
    };
}
export async function configureLocalHooksPath(projectRoot, hooksPath) {
    await execFileAsync('git', ['config', '--local', 'core.hooksPath', hooksPath], {
        cwd: projectRoot,
    });
}
export async function isHookInstalled(projectRoot) {
    const { hookPath } = await getHookInstallInfo(projectRoot);
    try {
        const content = await readFile(hookPath, 'utf8');
        return content.includes(HOOK_MARKER_START);
    }
    catch (error) {
        if (typeof error === 'object' &&
            error !== null &&
            'code' in error &&
            error.code === 'ENOENT') {
            return false;
        }
        throw error;
    }
}
export async function installHook(projectRoot) {
    const hookPath = (await getHookInstallInfo(projectRoot)).hookPath;
    let current = '';
    try {
        current = await readFile(hookPath, 'utf8');
    }
    catch (error) {
        if (typeof error === 'object' &&
            error !== null &&
            'code' in error &&
            error.code !== 'ENOENT') {
            throw error;
        }
    }
    if (current.includes(HOOK_MARKER_START)) {
        await chmod(hookPath, 0o755);
        return hookPath;
    }
    const includeShebang = current.trim().length === 0;
    const snippet = createHookSnippet({ includeShebang });
    await ensureDir(dirname(hookPath));
    const next = current.trim().length > 0
        ? `${current.trimEnd()}\n\n${snippet}\n`
        : `${snippet}\n`;
    await writeFile(hookPath, next, 'utf8');
    await chmod(hookPath, 0o755);
    return hookPath;
}
function removeHookSnippet(content) {
    const markerStart = escapeRegExp(HOOK_MARKER_START);
    const markerEnd = escapeRegExp(HOOK_MARKER_END);
    const normalized = content.replaceAll('\r\n', '\n');
    const wholeSnippetPattern = new RegExp(`^#!/bin/sh\\n\\n${markerStart}[\\s\\S]*?${markerEnd}\\n?$`);
    if (wholeSnippetPattern.test(normalized)) {
        return '';
    }
    const markerBlockPattern = new RegExp(`\\n?${markerStart}[\\s\\S]*?${markerEnd}\\n?`, 'm');
    const withoutMarker = normalized.replace(markerBlockPattern, '\n');
    const compacted = withoutMarker.replace(/\n{3,}/g, '\n\n').trimEnd();
    return compacted.length > 0 ? `${compacted}\n` : '';
}
export async function uninstallHook(projectRoot) {
    const hooksDir = await resolveHooksDirectory(projectRoot);
    const hookPath = join(hooksDir, 'pre-commit');
    let current = '';
    try {
        current = await readFile(hookPath, 'utf8');
    }
    catch (error) {
        if (typeof error === 'object' &&
            error !== null &&
            'code' in error &&
            error.code === 'ENOENT') {
            return;
        }
        throw error;
    }
    if (!current.includes(HOOK_MARKER_START)) {
        return;
    }
    const next = removeHookSnippet(current);
    if (next.trim().length === 0) {
        const hookStat = await lstat(hookPath);
        if (hookStat.isSymbolicLink()) {
            // Preserve the symlink — write empty content through it instead of
            // deleting the link itself (which would break managed hook setups).
            await writeFile(hookPath, '', 'utf8');
        }
        else {
            await rm(hookPath, { force: true });
        }
        return;
    }
    await writeFile(hookPath, next, 'utf8');
}
async function runStatusCommandDefault(cwd) {
    try {
        const { stdout } = await execFileAsync('oat', ['status', '--scope', 'project', '--json'], { cwd });
        const payload = JSON.parse(stdout);
        const summary = payload.summary ?? {};
        const drifted = summary.drifted ?? 0;
        const missing = summary.missing ?? 0;
        const stray = summary.stray ?? 0;
        if (drifted + missing > 0) {
            return 'managed_drift';
        }
        if (stray > 0) {
            return 'stray_only';
        }
        return 'in_sync';
    }
    catch {
        // On any failure (oat missing, JSON parse error, unexpected exit), fall
        // back to treating the state as managed drift so the operator notices.
        return 'managed_drift';
    }
}
export async function runHookCheck(cwd, options = {}) {
    const runStatusCommand = options.runStatusCommand ?? runStatusCommandDefault;
    const warn = options.warn ??
        ((message) => {
            process.stderr.write(`${message}\n`);
        });
    const info = options.info ??
        ((message) => {
            process.stderr.write(`${message}\n`);
        });
    let status;
    try {
        status = await runStatusCommand(cwd);
    }
    catch {
        status = 'managed_drift';
    }
    if (status === 'managed_drift') {
        warn(HOOK_DRIFT_WARNING);
    }
    else if (status === 'stray_only') {
        info(HOOK_STRAY_INFO);
    }
    return { status };
}
