import { execFileSync, spawnSync } from 'child_process';
import { getCliLaunch } from './cli-entry.js';
import { buildAddGenericPasswordArgs, buildAddGenericPasswordSpawnOptions, } from './secrets/index.js';
export const OPENCLAW_KEYCHAIN_ACCOUNT = 'openclaw';
export const OPENCLAW_KEYCHAIN_PROVIDER = 'agents_keychain';
export const OPENCLAW_KEYCHAIN_ENV_SERVICES = {
    OPENROUTER_API_KEY: 'openrouter-api-key',
    LINEAR_API_KEY: 'linear-api-key',
    GRAFANA_API_KEY: 'grafana-api-key',
    POSTHOG_API_KEY: 'posthog-api-key',
};
function isRecord(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function secretRef(provider, service) {
    return { source: 'exec', provider, id: service };
}
function openClawPath(parts) {
    return parts.join('.');
}
function collectOpenClawEnv(config) {
    const env = isRecord(config.env) ? config.env : null;
    const out = new Map();
    if (!env)
        return out;
    for (const [key, service] of Object.entries(OPENCLAW_KEYCHAIN_ENV_SERVICES)) {
        void service;
        const top = env[key];
        if (typeof top === 'string' && top.trim()) {
            out.set(key, { path: ['env', key], value: top });
            continue;
        }
        const vars = isRecord(env.vars) ? env.vars : null;
        const nested = vars?.[key];
        if (typeof nested === 'string' && nested.trim()) {
            out.set(key, { path: ['env', 'vars', key], value: nested });
        }
    }
    return out;
}
function deletePath(config, parts) {
    let current = config;
    for (const part of parts.slice(0, -1)) {
        if (!isRecord(current))
            return false;
        current = current[part];
    }
    if (!isRecord(current))
        return false;
    const key = parts[parts.length - 1];
    if (!(key in current))
        return false;
    delete current[key];
    return true;
}
function normalizeObject(value) {
    return isRecord(value) ? value : {};
}
function ensureOpenClawKeychainProvider(config, opts, agentsBin) {
    const launch = getCliLaunch(['secrets', 'openclaw-keychain', 'resolve', '--account', opts.account], agentsBin);
    const secrets = normalizeObject(config.secrets);
    const providers = normalizeObject(secrets.providers);
    providers[opts.provider] = {
        source: 'exec',
        command: launch.command,
        args: launch.args,
        jsonOnly: true,
    };
    secrets.providers = providers;
    config.secrets = secrets;
}
function providerEnvKeyForPath(parts) {
    if (parts.length === 8 &&
        parts[0] === 'plugins' &&
        parts[1] === 'entries' &&
        parts[3] === 'config' &&
        parts[4] === 'mcpServers' &&
        parts[6] === 'env') {
        return parts[7];
    }
    return null;
}
function serviceForSupportedPath(parts) {
    const envKey = providerEnvKeyForPath(parts);
    if (envKey)
        return OPENCLAW_KEYCHAIN_ENV_SERVICES[envKey] ?? null;
    if (parts.length === 4 && parts[0] === 'models' && parts[1] === 'providers' && parts[3] === 'apiKey') {
        if (parts[2] === 'openrouter')
            return OPENCLAW_KEYCHAIN_ENV_SERVICES.OPENROUTER_API_KEY;
    }
    return null;
}
function envKeyForService(service) {
    return Object.entries(OPENCLAW_KEYCHAIN_ENV_SERVICES).find(([, known]) => known === service)?.[0] ?? service;
}
function assertSameOpenClawSecretValue(envKey, service, existingPath, existingValue, nextPath, nextValue) {
    if (existingValue === nextValue)
        return;
    throw new Error(`OpenClaw credential value mismatch for ${envKey} (${service}): ${existingPath} and ${nextPath} differ. ` +
        `Resolve the plaintext values before migrating to Keychain.`);
}
function canReplaceByValue(parts) {
    if (parts.length === 4 && parts[0] === 'models' && parts[1] === 'providers' && parts[3] === 'apiKey')
        return true;
    if (parts.length === 6 && parts[0] === 'plugins' && parts[1] === 'entries' && parts[3] === 'config' && parts[4] === 'webSearch' && parts[5] === 'apiKey')
        return true;
    if (parts.length === 5 && parts[0] === 'tools' && parts[1] === 'web' && parts[2] === 'search' && parts[4] === 'apiKey')
        return true;
    if (parts.length === 4 && parts[0] === 'tools' && parts[1] === 'web' && parts[2] === 'search' && parts[3] === 'apiKey')
        return true;
    return providerEnvKeyForPath(parts) !== null;
}
function replaceSupportedSecrets(current, parts, provider, envByValue, env, services, servicePaths, replacedPaths) {
    if (!isRecord(current) && !Array.isArray(current))
        return;
    const entries = Array.isArray(current)
        ? current.map((value, index) => [String(index), value])
        : Object.entries(current);
    for (const [key, value] of entries) {
        const childParts = [...parts, key];
        if (typeof value === 'string' && value.trim() && canReplaceByValue(childParts)) {
            const directService = serviceForSupportedPath(childParts);
            const matchedService = directService ?? envByValue.get(value);
            if (matchedService) {
                const envKey = envKeyForService(matchedService);
                const childPath = openClawPath(childParts);
                const existingService = services.get(matchedService);
                if (existingService) {
                    assertSameOpenClawSecretValue(envKey, matchedService, servicePaths.get(matchedService) ?? matchedService, existingService.value, childPath, value);
                }
                if (directService) {
                    const envEntry = env.get(envKey);
                    if (envEntry) {
                        assertSameOpenClawSecretValue(envKey, matchedService, openClawPath(envEntry.path), envEntry.value, childPath, value);
                    }
                }
                services.set(matchedService, { envKey, service: matchedService, value });
                servicePaths.set(matchedService, childPath);
                current[key] = secretRef(provider, matchedService);
                replacedPaths.push(childPath);
                continue;
            }
        }
        replaceSupportedSecrets(value, childParts, provider, envByValue, env, services, servicePaths, replacedPaths);
    }
}
export function migrateOpenClawConfigToKeychainRefs(config, opts = {}) {
    const account = opts.account ?? OPENCLAW_KEYCHAIN_ACCOUNT;
    const provider = opts.provider ?? OPENCLAW_KEYCHAIN_PROVIDER;
    const env = collectOpenClawEnv(config);
    const envByValue = new Map();
    for (const [envKey, entry] of env) {
        envByValue.set(entry.value, OPENCLAW_KEYCHAIN_ENV_SERVICES[envKey]);
    }
    const services = new Map();
    const servicePaths = new Map();
    const replacedPaths = [];
    replaceSupportedSecrets(config, [], provider, envByValue, env, services, servicePaths, replacedPaths);
    const removedEnvPaths = [];
    const unsupportedEnvKeys = [];
    for (const [envKey, entry] of env) {
        const service = OPENCLAW_KEYCHAIN_ENV_SERVICES[envKey];
        const mappedService = services.get(service);
        if (!mappedService) {
            unsupportedEnvKeys.push(envKey);
            continue;
        }
        assertSameOpenClawSecretValue(envKey, service, servicePaths.get(service) ?? service, mappedService.value, openClawPath(entry.path), entry.value);
        if (deletePath(config, entry.path))
            removedEnvPaths.push(openClawPath(entry.path));
    }
    if (services.size > 0) {
        ensureOpenClawKeychainProvider(config, { account, provider }, opts.agentsBin);
    }
    return {
        services: [...services.values()],
        replacedPaths,
        removedEnvPaths,
        unsupportedEnvKeys,
        changed: services.size > 0 || replacedPaths.length > 0 || removedEnvPaths.length > 0,
    };
}
export function buildOpenClawKeychainStoreInvocation(service, value, account = OPENCLAW_KEYCHAIN_ACCOUNT) {
    return {
        command: '/usr/bin/security',
        args: buildAddGenericPasswordArgs(account, service),
        options: buildAddGenericPasswordSpawnOptions(value),
    };
}
export function storeOpenClawKeychainServices(services, account = OPENCLAW_KEYCHAIN_ACCOUNT) {
    if (process.platform !== 'darwin') {
        throw new Error('OpenClaw Keychain migration must run on macOS.');
    }
    for (const { service, value } of services) {
        const write = buildOpenClawKeychainStoreInvocation(service, value, account);
        const result = spawnSync(write.command, write.args, write.options);
        if (result.status !== 0) {
            const msg = result.stderr?.toString().trim();
            throw new Error(msg || `Failed to write OpenClaw Keychain service '${service}'.`);
        }
    }
}
function stripSecurityTrailingNewline(value) {
    return value.replace(/\r?\n$/, '');
}
export function readOpenClawKeychainService(service, account = OPENCLAW_KEYCHAIN_ACCOUNT) {
    if (!/^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$/.test(service)) {
        throw new Error(`Invalid OpenClaw Keychain service id '${service}'.`);
    }
    if (process.platform !== 'darwin') {
        throw new Error('OpenClaw Keychain resolver must run on macOS.');
    }
    const value = execFileSync('/usr/bin/security', ['find-generic-password', '-s', service, '-a', account, '-w'], {
        encoding: 'utf-8',
        stdio: ['ignore', 'pipe', 'pipe'],
    });
    return stripSecurityTrailingNewline(value);
}
export function resolveOpenClawKeychainRequest(request, lookup) {
    const ids = Array.isArray(request.ids) ? request.ids.filter((id) => typeof id === 'string') : [];
    const values = {};
    const errors = {};
    for (const id of ids) {
        try {
            values[id] = lookup(id);
        }
        catch {
            errors[id] = { code: 'NOT_FOUND' };
        }
    }
    return {
        protocolVersion: 1,
        values,
        ...(Object.keys(errors).length > 0 ? { errors } : {}),
    };
}
