import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
import { errorDetails, logger } from './logging.js';
function safeName(value) {
    const cleaned = value.replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
    if (!cleaned)
        throw new Error('extension name must contain alphanumeric characters');
    return cleaned;
}
function readJsonFile(path) {
    if (!existsSync(path))
        return {};
    try {
        const text = readFileSync(path, 'utf8').trim();
        if (!text)
            return {};
        const parsed = JSON.parse(text);
        return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
            ? parsed
            : {};
    }
    catch (error) {
        logger.warn('extensions.json_read_failed', { path, ...errorDetails(error) });
        return {};
    }
}
function writeJsonFile(path, value) {
    mkdirSync(dirname(path), { recursive: true });
    writeFileSync(path, JSON.stringify(value, null, 2) + '\n', 'utf8');
}
function readTomlFile(path) {
    if (!existsSync(path))
        return {};
    try {
        const text = readFileSync(path, 'utf8');
        if (!text.trim())
            return {};
        return parseToml(text);
    }
    catch (error) {
        logger.warn('extensions.toml_read_failed', { path, ...errorDetails(error) });
        return {};
    }
}
function writeTomlFile(path, value) {
    mkdirSync(dirname(path), { recursive: true });
    writeFileSync(path, stringifyToml(value) + '\n', 'utf8');
}
function readSkillMarkdown(path) {
    try {
        const text = readFileSync(path, 'utf8');
        const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
        if (!match)
            return { name: null, description: null };
        const body = match[1];
        const fields = {};
        for (const line of body.split(/\r?\n/)) {
            const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
            if (!m)
                continue;
            fields[m[1]] = m[2].trim().replace(/^['"]|['"]$/g, '');
        }
        return { name: fields.name || null, description: fields.description || null };
    }
    catch (error) {
        logger.warn('extensions.skill_read_failed', { path, ...errorDetails(error) });
        return { name: null, description: null };
    }
}
function isPlainObject(value) {
    return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
export function defaultExtensionPaths(home = homedir()) {
    return {
        home,
        claudeUserConfig: join(home, '.claude.json'),
        claudeUserSkills: join(home, '.claude', 'skills'),
        codexUserConfig: join(home, '.codex', 'config.toml'),
        agentsUserSkills: join(home, '.agents', 'skills')
    };
}
export class ExtensionManager {
    db;
    paths;
    constructor(db, paths = defaultExtensionPaths()) {
        this.db = db;
        this.paths = paths;
    }
    // ---- listing ----
    listInstalledSkills() {
        const map = new Map();
        this.collectSkillsFromDir(this.paths.claudeUserSkills, 'claude', map);
        this.collectSkillsFromDir(this.paths.agentsUserSkills, 'agents', map);
        return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
    }
    collectSkillsFromDir(dir, kind, map) {
        if (!existsSync(dir))
            return;
        let entries;
        try {
            entries = readdirSync(dir, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (!entry.isDirectory())
                continue;
            const skillDir = join(dir, entry.name);
            const skillMd = join(skillDir, 'SKILL.md');
            if (!existsSync(skillMd))
                continue;
            const parsed = readSkillMarkdown(skillMd);
            const name = parsed.name || entry.name;
            const existing = map.get(name);
            const source = existing?.source ?? this.lookupSource('skill', name);
            const paths = existing?.paths ?? { claude: null, agents: null };
            paths[kind] = skillDir;
            map.set(name, {
                name,
                description: parsed.description ?? existing?.description ?? null,
                scope: 'user',
                paths,
                source
            });
        }
    }
    listInstalledMcpServers() {
        const result = new Map();
        const claudeJson = readJsonFile(this.paths.claudeUserConfig);
        const claudeServers = isPlainObject(claudeJson.mcpServers)
            ? claudeJson.mcpServers
            : {};
        for (const [name, entry] of Object.entries(claudeServers)) {
            const server = this.mcpServerFromRawEntry(name, entry);
            server.presentIn.claudeUser = true;
            result.set(name, server);
        }
        const codexToml = readTomlFile(this.paths.codexUserConfig);
        const codexServers = isPlainObject(codexToml.mcp_servers)
            ? codexToml.mcp_servers
            : {};
        for (const [name, entry] of Object.entries(codexServers)) {
            const transport = typeof entry.url === 'string' ? 'http' : 'stdio';
            const existing = result.get(name);
            const merged = existing ?? {
                name,
                transport,
                command: typeof entry.command === 'string' ? entry.command : null,
                args: Array.isArray(entry.args) ? entry.args : [],
                env: isPlainObject(entry.env) ? entry.env : {},
                url: typeof entry.url === 'string' ? entry.url : null,
                headers: isPlainObject(entry.http_headers)
                    ? entry.http_headers
                    : {},
                presentIn: { claudeUser: false, codexUser: false },
                source: this.lookupSource('mcp', name)
            };
            merged.presentIn.codexUser = true;
            result.set(name, merged);
        }
        return [...result.values()].sort((a, b) => a.name.localeCompare(b.name));
    }
    mcpServerFromRawEntry(name, entry) {
        const transport = entry.type === 'http' || entry.type === 'sse' || typeof entry.url === 'string'
            ? 'http'
            : 'stdio';
        return {
            name,
            transport,
            command: typeof entry.command === 'string' ? entry.command : null,
            args: Array.isArray(entry.args) ? entry.args : [],
            env: isPlainObject(entry.env) ? entry.env : {},
            url: typeof entry.url === 'string' ? entry.url : null,
            headers: isPlainObject(entry.headers) ? entry.headers : {},
            presentIn: { claudeUser: false, codexUser: false },
            source: this.lookupSource('mcp', name)
        };
    }
    lookupSource(kind, name) {
        const row = this.db.getExtensionRegistrySource(kind, name);
        if (!row)
            return { registry: null, ref: null, version: null, url: null };
        return { registry: row.registry, ref: row.ref, version: row.version, url: row.url };
    }
    // ---- install / uninstall ----
    installSkill(spec) {
        const name = safeName(spec.name);
        const claudeDir = join(this.paths.claudeUserSkills, name);
        const agentsDir = join(this.paths.agentsUserSkills, name);
        for (const dir of [claudeDir, agentsDir]) {
            mkdirSync(dir, { recursive: true });
            writeFileSync(join(dir, 'SKILL.md'), spec.skillMarkdown, 'utf8');
            for (const file of spec.extraFiles ?? []) {
                const target = join(dir, file.relativePath);
                mkdirSync(dirname(target), { recursive: true });
                writeFileSync(target, file.contents, 'utf8');
            }
        }
        this.db.upsertExtensionRegistrySource({
            kind: 'skill',
            name,
            registry: spec.source?.registry ?? null,
            ref: spec.source?.ref ?? null,
            version: spec.source?.version ?? null,
            url: spec.source?.url ?? null
        });
        const parsed = readSkillMarkdown(join(claudeDir, 'SKILL.md'));
        return {
            name,
            description: parsed.description,
            scope: 'user',
            paths: { claude: claudeDir, agents: agentsDir },
            source: {
                registry: spec.source?.registry ?? null,
                ref: spec.source?.ref ?? null,
                version: spec.source?.version ?? null,
                url: spec.source?.url ?? null
            }
        };
    }
    installMcpServer(spec) {
        const name = safeName(spec.name);
        this.writeClaudeMcpEntry(this.paths.claudeUserConfig, name, spec);
        this.writeCodexMcpEntry(this.paths.codexUserConfig, name, spec, true);
        this.db.upsertExtensionRegistrySource({
            kind: 'mcp',
            name,
            registry: spec.source?.registry ?? null,
            ref: spec.source?.ref ?? null,
            version: spec.source?.version ?? null,
            url: spec.source?.url ?? null
        });
        return {
            name,
            transport: spec.transport,
            command: spec.command ?? null,
            args: spec.args ?? [],
            env: spec.env ?? {},
            url: spec.url ?? null,
            headers: spec.headers ?? {},
            presentIn: { claudeUser: true, codexUser: true },
            source: {
                registry: spec.source?.registry ?? null,
                ref: spec.source?.ref ?? null,
                version: spec.source?.version ?? null,
                url: spec.source?.url ?? null
            }
        };
    }
    writeClaudeMcpEntry(path, name, spec) {
        const json = readJsonFile(path);
        const servers = isPlainObject(json.mcpServers)
            ? json.mcpServers
            : {};
        if (spec.transport === 'http') {
            servers[name] = {
                type: 'http',
                url: spec.url,
                ...(spec.headers && Object.keys(spec.headers).length > 0 ? { headers: spec.headers } : {})
            };
        }
        else {
            servers[name] = {
                type: 'stdio',
                command: spec.command,
                args: spec.args ?? [],
                ...(spec.env && Object.keys(spec.env).length > 0 ? { env: spec.env } : {})
            };
        }
        json.mcpServers = servers;
        writeJsonFile(path, json);
    }
    writeCodexMcpEntry(path, name, spec, enabledByDefault) {
        const toml = readTomlFile(path);
        const servers = isPlainObject(toml.mcp_servers)
            ? toml.mcp_servers
            : {};
        const entry = { enabled: enabledByDefault };
        if (spec.transport === 'http') {
            entry.url = spec.url;
            if (spec.headers && Object.keys(spec.headers).length > 0) {
                entry.http_headers = spec.headers;
            }
        }
        else {
            entry.command = spec.command;
            entry.args = spec.args ?? [];
            if (spec.env && Object.keys(spec.env).length > 0)
                entry.env = spec.env;
        }
        servers[name] = entry;
        toml.mcp_servers = servers;
        writeTomlFile(path, toml);
    }
    uninstall(kind, name) {
        const safe = safeName(name);
        if (kind === 'skill') {
            for (const dir of [
                join(this.paths.claudeUserSkills, safe),
                join(this.paths.agentsUserSkills, safe)
            ]) {
                if (existsSync(dir))
                    rmSync(dir, { recursive: true, force: true });
            }
        }
        else {
            this.removeClaudeMcpEntry(this.paths.claudeUserConfig, safe);
            this.removeCodexMcpEntry(this.paths.codexUserConfig, safe);
        }
        this.db.deleteExtensionEverywhere(kind, safe);
    }
    removeClaudeMcpEntry(path, name) {
        if (!existsSync(path))
            return;
        const json = readJsonFile(path);
        if (!isPlainObject(json.mcpServers))
            return;
        const servers = json.mcpServers;
        if (!(name in servers))
            return;
        delete servers[name];
        json.mcpServers = servers;
        writeJsonFile(path, json);
    }
    removeCodexMcpEntry(path, name) {
        if (!existsSync(path))
            return;
        const toml = readTomlFile(path);
        if (!isPlainObject(toml.mcp_servers))
            return;
        const servers = toml.mcp_servers;
        if (!(name in servers))
            return;
        delete servers[name];
        toml.mcp_servers = servers;
        writeTomlFile(path, toml);
    }
    // ---- project enable ----
    setProjectExtension(projectId, projectPath, kind, name, enabled) {
        const safe = safeName(name);
        this.db.setProjectExtension(projectId, kind, safe, enabled);
        if (kind === 'skill') {
            this.writeProjectSkillOverride(projectPath, safe, enabled);
        }
        else {
            this.writeProjectMcp(projectPath, safe, enabled);
            this.writeProjectCodexMcp(projectPath, safe, enabled);
        }
    }
    writeProjectSkillOverride(projectPath, name, enabled) {
        const settingsPath = join(projectPath, '.claude', 'settings.json');
        const settings = readJsonFile(settingsPath);
        const overrides = isPlainObject(settings.skillOverrides)
            ? settings.skillOverrides
            : {};
        if (enabled) {
            overrides[name] = 'on';
        }
        else {
            delete overrides[name];
        }
        if (Object.keys(overrides).length === 0) {
            delete settings.skillOverrides;
        }
        else {
            settings.skillOverrides = overrides;
        }
        writeJsonFile(settingsPath, settings);
    }
    writeProjectMcp(projectPath, name, enabled) {
        const mcpPath = join(projectPath, '.mcp.json');
        const json = readJsonFile(mcpPath);
        const servers = isPlainObject(json.mcpServers)
            ? json.mcpServers
            : {};
        const userServers = this.userClaudeMcpServers();
        if (enabled) {
            const userEntry = userServers[name];
            if (userEntry === undefined) {
                logger.warn('extensions.project_enable_missing_global', { name });
                return;
            }
            servers[name] = userEntry;
        }
        else {
            delete servers[name];
        }
        if (Object.keys(servers).length === 0) {
            delete json.mcpServers;
        }
        else {
            json.mcpServers = servers;
        }
        writeJsonFile(mcpPath, json);
    }
    writeProjectCodexMcp(projectPath, name, enabled) {
        const path = join(projectPath, '.codex', 'config.toml');
        const toml = readTomlFile(path);
        const servers = isPlainObject(toml.mcp_servers)
            ? toml.mcp_servers
            : {};
        const userServers = this.userCodexMcpServers();
        if (enabled) {
            const userEntry = userServers[name];
            if (userEntry === undefined) {
                logger.warn('extensions.project_enable_missing_global', { name });
                return;
            }
            servers[name] = { ...userEntry, enabled: true };
        }
        else {
            delete servers[name];
        }
        if (Object.keys(servers).length === 0) {
            delete toml.mcp_servers;
        }
        else {
            toml.mcp_servers = servers;
        }
        writeTomlFile(path, toml);
    }
    userClaudeMcpServers() {
        const json = readJsonFile(this.paths.claudeUserConfig);
        return isPlainObject(json.mcpServers) ? json.mcpServers : {};
    }
    userCodexMcpServers() {
        const toml = readTomlFile(this.paths.codexUserConfig);
        return isPlainObject(toml.mcp_servers)
            ? toml.mcp_servers
            : {};
    }
    // ---- session view + overrides ----
    sessionExtensions(sessionId, projectId) {
        const overrides = this.db.listSessionExtensionOverrides(sessionId);
        const projectExtensions = this.db.listProjectExtensions(projectId);
        const installedSkills = this.listInstalledSkills();
        const installedMcp = this.listInstalledMcpServers();
        const skills = installedSkills.map((skill) => {
            const project = projectExtensions.find((pe) => pe.kind === 'skill' && pe.name === skill.name);
            const projectState = project?.enabled ? 'on' : 'off';
            const override = overrides.find((o) => o.kind === 'skill' && o.name === skill.name);
            const sessionState = override?.skillState ?? null;
            const effectiveState = sessionState ?? projectState;
            return {
                name: skill.name,
                description: skill.description,
                projectState,
                sessionState,
                effectiveState
            };
        });
        const mcpServers = installedMcp.map((mcp) => {
            const project = projectExtensions.find((pe) => pe.kind === 'mcp' && pe.name === mcp.name);
            const projectEnabled = project?.enabled ?? false;
            const override = overrides.find((o) => o.kind === 'mcp' && o.name === mcp.name);
            const sessionEnabled = override?.mcpEnabled ?? null;
            const effectiveEnabled = sessionEnabled !== null ? sessionEnabled : projectEnabled;
            return {
                name: mcp.name,
                transport: mcp.transport,
                projectEnabled,
                sessionEnabled,
                effectiveEnabled
            };
        });
        return { skills, mcpServers };
    }
    setSessionOverride(input) {
        const safe = safeName(input.name);
        if (input.kind === 'skill') {
            if (input.skillState === undefined || input.skillState === null) {
                this.db.deleteSessionExtensionOverride(input.sessionId, 'skill', safe);
                return;
            }
            this.db.setSessionExtensionOverride({
                sessionId: input.sessionId,
                kind: 'skill',
                name: safe,
                skillState: input.skillState
            });
        }
        else {
            if (input.mcpEnabled === undefined || input.mcpEnabled === null) {
                this.db.deleteSessionExtensionOverride(input.sessionId, 'mcp', safe);
                return;
            }
            this.db.setSessionExtensionOverride({
                sessionId: input.sessionId,
                kind: 'mcp',
                name: safe,
                mcpEnabled: input.mcpEnabled
            });
        }
    }
    // ---- spawn-time helpers ----
    buildClaudeSpawnArgs(sessionId, projectId, projectPath) {
        const view = this.sessionExtensions(sessionId, projectId);
        const skillOverrides = {};
        for (const skill of view.skills) {
            if (skill.sessionState !== null) {
                skillOverrides[skill.name] = skill.sessionState;
            }
        }
        const settingsArg = Object.keys(skillOverrides).length > 0 ? JSON.stringify({ skillOverrides }) : null;
        const hasSessionMcpOverride = view.mcpServers.some((s) => s.sessionEnabled !== null);
        if (!hasSessionMcpOverride) {
            return { settingsArg, mcpConfigFile: null, strict: false };
        }
        const userServers = this.userClaudeMcpServers();
        const enabledServers = {};
        for (const s of view.mcpServers) {
            if (s.effectiveEnabled && userServers[s.name] !== undefined) {
                enabledServers[s.name] = userServers[s.name];
            }
        }
        const dir = join(this.paths.home, '.fatmux', 'sessions', sessionId);
        mkdirSync(dir, { recursive: true });
        const file = join(dir, 'mcp.json');
        writeJsonFile(file, { mcpServers: enabledServers });
        void projectPath;
        return { settingsArg, mcpConfigFile: file, strict: true };
    }
    buildCodexSpawnArgs(sessionId, projectId) {
        const view = this.sessionExtensions(sessionId, projectId);
        const args = [];
        for (const mcp of view.mcpServers) {
            if (mcp.sessionEnabled === null)
                continue;
            args.push('-c', `mcp_servers.${mcp.name}.enabled=${mcp.effectiveEnabled ? 'true' : 'false'}`);
        }
        return args;
    }
}
export function statIsDir(path) {
    try {
        return statSync(path).isDirectory();
    }
    catch {
        return false;
    }
}
//# sourceMappingURL=extension-manager.js.map