/**
 * registration — reverse IDE/editor settings that Nexus wrote.
 *
 * Every removal is scoped to a specific JSON key path — we never delete the
 * user's file or unrelated keys. If the file becomes empty after removal, we
 * leave it in place (still empty `{}`) so editors that rely on it keep working.
 */
import * as fs from 'fs';
import * as path from 'path';
import { enumerateRegistrationTargets } from './state-locator.js';
const NEXUS_CMD_MARKER = 'nexus-prime hook';
const MANAGED_BLOCKS = [
    ['<!-- nexus-prime:codex-bootstrap:start -->', '<!-- nexus-prime:codex-bootstrap:end -->'],
    ['<!-- nexus-prime:claude-bootstrap:start -->', '<!-- nexus-prime:claude-bootstrap:end -->'],
    ['<!-- nexus-prime:cline-bootstrap:start -->', '<!-- nexus-prime:cline-bootstrap:end -->'],
];
function readJson(filePath) {
    try {
        const raw = fs.readFileSync(filePath, 'utf8');
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
            return null;
        return parsed;
    }
    catch {
        return null;
    }
}
function writeJson(filePath, value) {
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
}
function stripTomlTable(doc, tableName) {
    const lines = doc.split(/\r?\n/);
    const kept = [];
    let changed = false;
    for (let index = 0; index < lines.length; index += 1) {
        const header = lines[index].match(/^\s*\[([^\]]+)\]\s*$/);
        if (!header) {
            kept.push(lines[index]);
            continue;
        }
        const sectionName = header[1].trim();
        if (sectionName !== tableName && !sectionName.startsWith(`${tableName}.`)) {
            kept.push(lines[index]);
            continue;
        }
        changed = true;
        index += 1;
        while (index < lines.length) {
            const nextHeader = lines[index].match(/^\s*\[([^\]]+)\]\s*$/);
            if (nextHeader) {
                const nextName = nextHeader[1].trim();
                if (nextName !== tableName && !nextName.startsWith(`${tableName}.`)) {
                    index -= 1;
                    break;
                }
            }
            index += 1;
        }
    }
    const content = kept.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
    return { changed, content: content ? `${content}\n` : '' };
}
export function removeManagedBlocksFromFile(filePath, options = {}) {
    if (!fs.existsSync(filePath)) {
        return [];
    }
    let content;
    try {
        content = fs.readFileSync(filePath, 'utf8');
    }
    catch {
        return [{ filePath, jsonPath: ['managedBlocks'], removed: false, reason: 'unreadable' }];
    }
    const reports = [];
    let nextContent = content;
    for (const [start, end] of MANAGED_BLOCKS) {
        let removed = false;
        let startIndex = nextContent.indexOf(start);
        let endIndex = startIndex >= 0 ? nextContent.indexOf(end, startIndex + start.length) : -1;
        while (startIndex >= 0 && endIndex >= 0) {
            const afterEnd = endIndex + end.length;
            nextContent = `${nextContent.slice(0, startIndex)}${nextContent.slice(afterEnd)}`;
            removed = true;
            startIndex = nextContent.indexOf(start);
            endIndex = startIndex >= 0 ? nextContent.indexOf(end, startIndex + start.length) : -1;
        }
        if (removed) {
            reports.push({ filePath, jsonPath: ['managedBlocks', start], removed: true });
        }
    }
    if (reports.length === 0) {
        return [];
    }
    nextContent = nextContent.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trimEnd();
    if (nextContent)
        nextContent += '\n';
    if (!options.dryRun) {
        fs.writeFileSync(filePath, nextContent, 'utf8');
    }
    return reports;
}
function stripNexusHookEntries(hooks) {
    let changed = false;
    const next = {};
    for (const [event, value] of Object.entries(hooks)) {
        if (!Array.isArray(value)) {
            next[event] = value;
            continue;
        }
        const filtered = value.filter((entry) => {
            if (!entry || typeof entry !== 'object')
                return true;
            const hooksList = entry.hooks;
            if (!Array.isArray(hooksList))
                return true;
            const hasNexus = hooksList.some((h) => {
                if (!h || typeof h !== 'object')
                    return false;
                const cmd = h.command;
                return typeof cmd === 'string' && cmd.trimStart().startsWith(NEXUS_CMD_MARKER);
            });
            if (hasNexus)
                changed = true;
            return !hasNexus;
        });
        if (filtered.length > 0)
            next[event] = filtered;
        else
            changed = changed || value.length > 0;
    }
    return { changed, hooks: next };
}
/**
 * Remove a single registration. Supports two shapes:
 *   - Claude Code: `{ hooks: { <event>: [ { hooks: [{ command: "nexus-prime hook ..." }] } ] } }`
 *     Removes only entries whose command starts with `nexus-prime hook`.
 *   - VS Code / Cursor: `{ mcpServers: { "nexus-prime": {...} } }`
 *     Removes only the `nexus-prime` key.
 */
export function removeRegistration(entry, options = {}) {
    const { filePath, jsonPath } = entry;
    if (!fs.existsSync(filePath)) {
        return { filePath, jsonPath, removed: false, reason: 'file-missing' };
    }
    if (filePath.endsWith('.toml') && jsonPath.join('.') === 'mcp_servers.nexus-prime') {
        try {
            const existing = fs.readFileSync(filePath, 'utf8');
            const { changed, content } = stripTomlTable(existing, 'mcp_servers.nexus-prime');
            if (!changed) {
                return { filePath, jsonPath, removed: false, reason: 'key-missing' };
            }
            if (!options.dryRun) {
                fs.writeFileSync(filePath, content, 'utf8');
            }
            return { filePath, jsonPath, removed: true };
        }
        catch {
            return { filePath, jsonPath, removed: false, reason: 'unreadable' };
        }
    }
    const doc = readJson(filePath);
    if (!doc) {
        return { filePath, jsonPath, removed: false, reason: 'unreadable' };
    }
    // Special case: Claude Code hooks — jsonPath = ['hooks']; strip nexus-prime hook commands across events.
    if (jsonPath.length === 1 && jsonPath[0] === 'hooks') {
        const hooks = doc.hooks;
        if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) {
            return { filePath, jsonPath, removed: false, reason: 'no-hooks-block' };
        }
        const { changed, hooks: nextHooks } = stripNexusHookEntries(hooks);
        if (!changed) {
            return { filePath, jsonPath, removed: false, reason: 'no-nexus-entries' };
        }
        if (Object.keys(nextHooks).length === 0) {
            delete doc.hooks;
        }
        else {
            doc.hooks = nextHooks;
        }
        if (!options.dryRun)
            writeJson(filePath, doc);
        return { filePath, jsonPath, removed: true };
    }
    // Generic: dotted path delete. Only the leaf key is removed.
    let cursor = doc;
    for (let i = 0; i < jsonPath.length - 1; i++) {
        const key = jsonPath[i];
        const next = cursor[key];
        if (!next || typeof next !== 'object' || Array.isArray(next)) {
            return { filePath, jsonPath, removed: false, reason: 'path-missing' };
        }
        cursor = next;
    }
    const leaf = jsonPath[jsonPath.length - 1];
    if (!(leaf in cursor)) {
        return { filePath, jsonPath, removed: false, reason: 'key-missing' };
    }
    delete cursor[leaf];
    // Collapse empty parent objects along the chain (only if they become empty)
    for (let i = jsonPath.length - 2; i >= 0; i--) {
        let parent = doc;
        for (let j = 0; j < i; j++)
            parent = parent[jsonPath[j]];
        const intermediate = parent[jsonPath[i]];
        if (intermediate && typeof intermediate === 'object' && !Array.isArray(intermediate) && Object.keys(intermediate).length === 0) {
            delete parent[jsonPath[i]];
        }
        else {
            break;
        }
    }
    if (!options.dryRun)
        writeJson(filePath, doc);
    return { filePath, jsonPath, removed: true };
}
/** Remove all registrations recorded in the manifest. */
export function removeManifestRegistrations(manifest, options = {}) {
    const reports = [];
    const mutated = new Set();
    for (const reg of manifest.registrations) {
        const report = removeRegistration(reg, options);
        reports.push(report);
        if (report.removed)
            mutated.add(reg.filePath);
    }
    return { reports, mutated: [...mutated] };
}
/**
 * Fallback heuristic: scan known IDE paths and remove nexus entries that the
 * manifest didn't know about. Used when manifest is empty/stale.
 */
export function removeHeuristicRegistrations(workspaceRoot, options = {}) {
    const targets = enumerateRegistrationTargets(workspaceRoot);
    const reports = [];
    const mutated = new Set();
    for (const target of targets) {
        const report = removeRegistration({ target: target.id, filePath: target.filePath, jsonPath: target.jsonPath }, options);
        reports.push(report);
        if (report.removed)
            mutated.add(target.filePath);
    }
    for (const filePath of [
        path.join(workspaceRoot, 'AGENTS.md'),
        path.join(workspaceRoot, 'CLAUDE.md'),
        path.join(workspaceRoot, 'Claude.md'),
        path.join(workspaceRoot, '.cline', 'rules.md'),
        path.join(workspaceRoot, '.clinerules'),
    ]) {
        const blockReports = removeManagedBlocksFromFile(filePath, options);
        reports.push(...blockReports);
        if (blockReports.some((report) => report.removed))
            mutated.add(filePath);
    }
    return { reports, mutated: [...mutated] };
}
