import { lstat, readdir, readFile, readlink, realpath, stat, } from 'node:fs/promises';
import { dirname, join, relative, resolve } from 'node:path';
export const EXPECTED_CLAUDE_CONTENT = '@AGENTS.md\n';
export const DEFAULT_INSTRUCTION_SYNC_STRATEGY = 'pointer';
const ROOT_EXCLUDED_DIRECTORIES = new Set(['.git', '.oat', '.worktrees']);
const GLOBAL_EXCLUDED_DIRECTORIES = new Set(['node_modules']);
// The only excluded-subtree re-entry points: a root-level directory is excluded
// wholesale, but a named subdirectory holds tracked instruction files that
// sync/validate must manage, so it is carved back into the scan. `.oat` is
// excluded at the repo root, yet `.oat/repo/**` carries AGENTS.md/CLAUDE.md
// pairs; `.oat/templates`, `.oat/projects`, and `.oat/sync` stay unscanned.
const ROOT_EXCLUDED_DIRECTORY_CARVE_INS = new Map([
    ['.oat', 'repo'],
]);
async function directoryExists(dependencies, directoryPath) {
    try {
        const stats = await dependencies.stat(directoryPath);
        return stats.isDirectory();
    }
    catch {
        return false;
    }
}
function getErrorCode(error) {
    return error && typeof error === 'object' && 'code' in error
        ? String(error.code)
        : null;
}
function normalizeLineEndings(content) {
    return content.replaceAll('\r\n', '\n');
}
export function resolveInstructionSyncStrategy(strategy) {
    return strategy ?? DEFAULT_INSTRUCTION_SYNC_STRATEGY;
}
function getValidInstructionDetail(strategy) {
    switch (strategy) {
        case 'symlink':
            return 'symlink valid';
        case 'copy':
            return 'copy valid';
        default:
            return 'pointer valid';
    }
}
function getInvalidInstructionDetail(strategy) {
    switch (strategy) {
        case 'symlink':
            return 'expected symlink to AGENTS.md';
        case 'copy':
            return 'expected hard copy of AGENTS.md content';
        default:
            return `expected ${JSON.stringify(EXPECTED_CLAUDE_CONTENT)}`;
    }
}
function toPosixPath(pathValue) {
    return pathValue.replaceAll('\\', '/');
}
function normalizeEntries(entries) {
    return [...entries].sort((left, right) => {
        const leftSortPath = left.agentsPath ?? left.claudePath;
        const rightSortPath = right.agentsPath ?? right.claudePath;
        return (leftSortPath.localeCompare(rightSortPath) ||
            left.claudePath.localeCompare(right.claudePath) ||
            left.status.localeCompare(right.status) ||
            left.detail.localeCompare(right.detail));
    });
}
function normalizeActions(actions) {
    return [...actions].sort((left, right) => {
        return (left.target.localeCompare(right.target) ||
            left.type.localeCompare(right.type) ||
            left.result.localeCompare(right.result) ||
            left.reason.localeCompare(right.reason));
    });
}
function recordInstructionFile(directoryEntries, directoryPath, entryName, entryPath) {
    const current = directoryEntries.get(directoryPath) ?? {};
    if (entryName === 'AGENTS.md') {
        current.agentsPath = entryPath;
        current.brokenAgentsPath = undefined;
        current.brokenAgentsErrorCode = undefined;
    }
    else {
        current.claudePath = entryPath;
        current.brokenClaudePath = undefined;
        current.brokenClaudeErrorCode = undefined;
    }
    directoryEntries.set(directoryPath, current);
}
async function scanInstructionDirectories(repoRoot, dependencies, debug) {
    const queue = [repoRoot];
    const directoryEntries = new Map();
    while (queue.length > 0) {
        const currentDirectory = queue.shift();
        if (!currentDirectory) {
            continue;
        }
        let entries;
        try {
            entries = await dependencies.readdir(currentDirectory, {
                withFileTypes: true,
            });
        }
        catch (error) {
            const errorCode = getErrorCode(error);
            debug?.(`Skipping directory scan for ${toPosixPath(currentDirectory)} (${errorCode ?? 'unknown error'})`);
            continue;
        }
        for (const entry of entries) {
            const entryPath = join(currentDirectory, entry.name);
            const isRootLevel = currentDirectory === repoRoot;
            if (entry.isDirectory()) {
                if (GLOBAL_EXCLUDED_DIRECTORIES.has(entry.name)) {
                    continue;
                }
                if (isRootLevel && ROOT_EXCLUDED_DIRECTORIES.has(entry.name)) {
                    const carveIn = ROOT_EXCLUDED_DIRECTORY_CARVE_INS.get(entry.name);
                    if (carveIn) {
                        const carveInPath = join(entryPath, carveIn);
                        if (await directoryExists(dependencies, carveInPath)) {
                            queue.push(carveInPath);
                        }
                    }
                    continue;
                }
                queue.push(entryPath);
                continue;
            }
            if (entry.isFile()) {
                if (entry.name === 'AGENTS.md' || entry.name === 'CLAUDE.md') {
                    recordInstructionFile(directoryEntries, currentDirectory, entry.name, entryPath);
                }
                continue;
            }
            if (!entry.isSymbolicLink()) {
                continue;
            }
            let entryStats;
            try {
                entryStats = await dependencies.stat(entryPath);
            }
            catch (error) {
                const errorCode = getErrorCode(error);
                if (entry.name === 'CLAUDE.md') {
                    const current = directoryEntries.get(currentDirectory) ?? {};
                    current.claudePath = entryPath;
                    current.brokenClaudePath = entryPath;
                    current.brokenClaudeErrorCode = errorCode ?? 'unknown error';
                    directoryEntries.set(currentDirectory, current);
                    continue;
                }
                if (entry.name === 'AGENTS.md') {
                    const current = directoryEntries.get(currentDirectory) ?? {};
                    current.brokenAgentsPath = entryPath;
                    current.brokenAgentsErrorCode = errorCode ?? 'unknown error';
                    directoryEntries.set(currentDirectory, current);
                    continue;
                }
                debug?.(`Skipping symlink target stat for ${toPosixPath(entryPath)} (${errorCode ?? 'unknown error'})`);
                continue;
            }
            if (entryStats.isDirectory()) {
                continue;
            }
            if (entryStats.isFile() &&
                (entry.name === 'AGENTS.md' || entry.name === 'CLAUDE.md')) {
                recordInstructionFile(directoryEntries, currentDirectory, entry.name, entryPath);
            }
        }
    }
    return directoryEntries;
}
export async function scanInstructionFiles(repoRoot, options = {}, overrides = {}) {
    const strategy = resolveInstructionSyncStrategy(options.strategy);
    const dependencies = {
        lstat,
        realpath,
        readdir,
        readFile,
        readlink,
        stat,
        ...overrides,
    };
    const instructionDirectories = await scanInstructionDirectories(repoRoot, dependencies, options.debug);
    const entries = [];
    for (const [directoryPath, directoryEntry] of instructionDirectories) {
        const agentsPath = directoryEntry.agentsPath ?? null;
        const brokenAgentsPath = directoryEntry.brokenAgentsPath ?? null;
        const brokenAgentsErrorCode = directoryEntry.brokenAgentsErrorCode ?? 'unknown error';
        const brokenClaudePath = directoryEntry.brokenClaudePath ?? null;
        const brokenClaudeErrorCode = directoryEntry.brokenClaudeErrorCode ?? 'unknown error';
        const claudePath = directoryEntry.claudePath ?? join(directoryPath, 'CLAUDE.md');
        if (!agentsPath && brokenAgentsPath) {
            entries.push({
                agentsPath: brokenAgentsPath,
                claudePath,
                status: 'content_mismatch',
                detail: brokenAgentsErrorCode === 'ENOENT'
                    ? 'broken AGENTS.md symlink'
                    : `unreadable AGENTS.md symlink target (${brokenAgentsErrorCode})`,
            });
            continue;
        }
        if (brokenClaudePath) {
            entries.push({
                agentsPath,
                claudePath,
                status: 'content_mismatch',
                detail: brokenClaudeErrorCode === 'ENOENT'
                    ? 'broken CLAUDE.md symlink'
                    : `unreadable CLAUDE.md symlink target (${brokenClaudeErrorCode})`,
            });
            continue;
        }
        if (!agentsPath) {
            try {
                await dependencies.readFile(claudePath, 'utf8');
            }
            catch (error) {
                entries.push({
                    agentsPath: null,
                    claudePath,
                    status: 'content_mismatch',
                    detail: `unable to read CLAUDE.md (${getErrorCode(error) ?? 'unknown'})`,
                });
                continue;
            }
            entries.push({
                agentsPath: null,
                claudePath,
                status: 'stray',
                detail: 'CLAUDE.md found without AGENTS.md',
            });
            continue;
        }
        let claudeStats;
        try {
            claudeStats = await dependencies.lstat(claudePath);
        }
        catch (error) {
            const errorCode = getErrorCode(error);
            if (errorCode === 'ENOENT') {
                entries.push({
                    agentsPath,
                    claudePath,
                    status: 'missing',
                    detail: 'CLAUDE.md missing',
                });
            }
            else {
                entries.push({
                    agentsPath,
                    claudePath,
                    status: 'content_mismatch',
                    detail: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
                });
            }
            continue;
        }
        if (strategy === 'symlink') {
            if (!claudeStats.isSymbolicLink()) {
                entries.push({
                    agentsPath,
                    claudePath,
                    status: 'content_mismatch',
                    detail: getInvalidInstructionDetail(strategy),
                });
                continue;
            }
            let claudeTarget;
            try {
                claudeTarget = await dependencies.readlink(claudePath);
            }
            catch (error) {
                const errorCode = getErrorCode(error);
                entries.push({
                    agentsPath,
                    claudePath,
                    status: 'content_mismatch',
                    detail: `unable to read CLAUDE.md symlink target (${errorCode ?? 'unknown error'})`,
                });
                continue;
            }
            const resolvedTarget = resolve(dirname(claudePath), claudeTarget);
            const [canonicalTarget, canonicalAgentsPath] = await Promise.all([
                dependencies.realpath(resolvedTarget).catch(() => resolvedTarget),
                dependencies.realpath(agentsPath).catch(() => agentsPath),
            ]);
            if (canonicalTarget === canonicalAgentsPath) {
                entries.push({
                    agentsPath,
                    claudePath,
                    status: 'ok',
                    detail: getValidInstructionDetail(strategy),
                });
            }
            else {
                entries.push({
                    agentsPath,
                    claudePath,
                    status: 'content_mismatch',
                    detail: getInvalidInstructionDetail(strategy),
                });
            }
            continue;
        }
        if (claudeStats.isSymbolicLink()) {
            entries.push({
                agentsPath,
                claudePath,
                status: 'content_mismatch',
                detail: getInvalidInstructionDetail(strategy),
            });
            continue;
        }
        let claudeContent;
        try {
            claudeContent = await dependencies.readFile(claudePath, 'utf8');
        }
        catch (error) {
            const errorCode = getErrorCode(error);
            if (errorCode === 'ENOENT') {
                entries.push({
                    agentsPath,
                    claudePath,
                    status: 'missing',
                    detail: 'CLAUDE.md missing',
                });
            }
            else {
                entries.push({
                    agentsPath,
                    claudePath,
                    status: 'content_mismatch',
                    detail: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
                });
            }
            continue;
        }
        const expectedContent = strategy === 'copy'
            ? await (async () => {
                try {
                    return await dependencies.readFile(agentsPath, 'utf8');
                }
                catch (error) {
                    const errorCode = getErrorCode(error);
                    entries.push({
                        agentsPath,
                        claudePath,
                        status: 'content_mismatch',
                        detail: `unable to read AGENTS.md (${errorCode ?? 'unknown error'})`,
                    });
                    return null;
                }
            })()
            : EXPECTED_CLAUDE_CONTENT;
        if (expectedContent === null) {
            continue;
        }
        if (normalizeLineEndings(claudeContent) ===
            normalizeLineEndings(expectedContent)) {
            entries.push({
                agentsPath,
                claudePath,
                status: 'ok',
                detail: getValidInstructionDetail(strategy),
            });
        }
        else {
            entries.push({
                agentsPath,
                claudePath,
                status: 'content_mismatch',
                detail: getInvalidInstructionDetail(strategy),
            });
        }
    }
    return normalizeEntries(entries);
}
export function buildInstructionsSummary(entries, actions) {
    const normalizedEntries = normalizeEntries(entries);
    const normalizedActions = normalizeActions(actions);
    return {
        scanned: normalizedEntries.length,
        ok: normalizedEntries.filter((entry) => entry.status === 'ok').length,
        missing: normalizedEntries.filter((entry) => entry.status === 'missing')
            .length,
        contentMismatch: normalizedEntries.filter((entry) => entry.status === 'content_mismatch').length,
        stray: normalizedEntries.filter((entry) => entry.status === 'stray').length,
        created: normalizedActions.filter((action) => action.type === 'create')
            .length,
        updated: normalizedActions.filter((action) => action.type === 'update')
            .length,
        skipped: normalizedActions.filter((action) => action.result === 'skipped')
            .length,
    };
}
function deriveInstructionsStatus(entries, actions) {
    if (entries.some((entry) => entry.status !== 'ok') ||
        actions.some((action) => action.result === 'skipped')) {
        return 'drift';
    }
    return 'ok';
}
export function buildInstructionsPayload({ mode, entries, actions, }) {
    const normalizedEntries = normalizeEntries(entries);
    const normalizedActions = normalizeActions(actions);
    return {
        mode,
        status: deriveInstructionsStatus(normalizedEntries, normalizedActions),
        summary: buildInstructionsSummary(normalizedEntries, normalizedActions),
        entries: normalizedEntries,
        actions: normalizedActions,
    };
}
export function formatInstructionsReport(payload, repoRoot) {
    const lines = [
        `instructions ${payload.mode}`,
        `status: ${payload.status}`,
        `summary: scanned=${payload.summary.scanned}, ok=${payload.summary.ok}, missing=${payload.summary.missing}, content_mismatch=${payload.summary.contentMismatch}, stray=${payload.summary.stray}, created=${payload.summary.created}, updated=${payload.summary.updated}, skipped=${payload.summary.skipped}`,
    ];
    if (payload.entries.length === 0) {
        lines.push('entries: none');
    }
    else {
        lines.push('entries:');
        for (const entry of payload.entries) {
            const displayPath = entry.agentsPath ?? entry.claudePath;
            const relativePath = repoRoot
                ? toPosixPath(relative(repoRoot, displayPath)) || '.'
                : toPosixPath(displayPath);
            lines.push(`- ${relativePath} -> ${entry.status} (${entry.detail})`);
        }
    }
    if (payload.actions.length === 0) {
        lines.push('actions: none');
    }
    else {
        lines.push('actions:');
        for (const action of payload.actions) {
            const target = repoRoot
                ? toPosixPath(relative(repoRoot, action.target)) || '.'
                : toPosixPath(action.target);
            lines.push(`- ${action.type} ${target} (${action.reason}) [${action.result}]`);
        }
    }
    return lines.join('\n');
}
