/**
 * ServiceRegistry — named service credential resolution.
 *
 * Reads service configs from the configured services.json path.
 * Each service entry declares its authType and the SecretsManager key
 * that holds the credential.
 *
 * Example services.json:
 * {
 *   "openai": { "name": "openai", "baseUrl": "https://api.openai.com", "authType": "bearer", "tokenKey": "OPENAI_API_KEY" },
 *   "github":  { "name": "github",  "baseUrl": "https://api.github.com",  "authType": "bearer", "tokenKey": "GITHUB_TOKEN" },
 *   "slack":   { "name": "slack", "authType": "bearer", "tokenKey": "SLACK_BOT_TOKEN", "appTokenKey": "SLACK_APP_TOKEN", "tokenRef": { "source": "vaultwarden", "item": "GoodVibes Slack", "field": "password", "server": "https://vault.example.test" } }
 * }
 */
import { readFileSync } from 'fs';
import { SecretsManager } from './secrets.js';
import { describeSecretRef, isSecretRefInput, resolveSecretRef, } from './secret-refs.js';
import { SubscriptionManager } from './subscriptions.js';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
import { instrumentedFetch } from '../utils/fetch-with-timeout.js';
function readServicesFile(filePath) {
    try {
        const raw = readFileSync(filePath, 'utf-8');
        return JSON.parse(raw);
    }
    catch (err) {
        const code = err.code;
        if (code !== 'ENOENT') {
            logger.error('ServiceRegistry: failed to read services file — treating as empty', {
                path: filePath,
            });
        }
        return {};
    }
}
// ---------------------------------------------------------------------------
// ServiceRegistry
// ---------------------------------------------------------------------------
export class ServiceRegistry {
    servicesFilePath;
    secretsManager;
    subscriptionManager;
    constructor(servicesFilePath, options) {
        this.servicesFilePath = servicesFilePath;
        this.secretsManager = options.secretsManager;
        this.subscriptionManager = options.subscriptionManager;
    }
    /**
     * Return all registered service configs.
     */
    getAll() {
        return readServicesFile(this.servicesFilePath);
    }
    /**
     * Return the config for a named service, or null if not found.
     */
    get(serviceName) {
        const all = this.getAll();
        return all[serviceName] ?? null;
    }
    async resolveConfiguredSecret(serviceName, field, keyOrRef, ref) {
        const candidate = ref ?? (keyOrRef && isSecretRefInput(keyOrRef) ? keyOrRef : undefined);
        if (candidate) {
            try {
                const resolved = await resolveSecretRef(candidate, {
                    resolveLocalSecret: (key) => this.secretsManager.get(key),
                    homeDirectory: this.secretsManager.getGlobalHome?.() ?? undefined,
                });
                return resolved.value;
            }
            catch (error) {
                logger.warn('ServiceRegistry: failed to resolve secret reference', {
                    serviceName,
                    field,
                    ref: describeSecretRef(candidate),
                    error: summarizeError(error),
                });
                return null;
            }
        }
        return keyOrRef ? this.secretsManager.get(keyOrRef) : null;
    }
    /**
     * Resolve auth headers for a named service.
     * Looks up the service config, fetches credential from SecretsManager,
     * and returns the appropriate HTTP headers.
     *
     * Returns null if the service is not registered or credential is unavailable.
     */
    async resolveAuth(serviceName) {
        const config = this.get(serviceName);
        if (!config) {
            logger.debug('ServiceRegistry: unknown service', { serviceName });
            return null;
        }
        const providerOverride = this.subscriptionManager.getAccessToken(config.providerId ?? serviceName);
        if (providerOverride) {
            return { Authorization: `Bearer ${providerOverride}` };
        }
        switch (config.authType) {
            case 'bearer': {
                const token = await this.resolveConfiguredSecret(serviceName, 'primary', config.tokenKey, config.tokenRef);
                if (!token) {
                    logger.debug('ServiceRegistry: bearer token not found', { serviceName, key: config.tokenKey });
                    return null;
                }
                return { Authorization: `Bearer ${token}` };
            }
            case 'basic': {
                const username = await this.resolveConfiguredSecret(serviceName, 'primary', config.tokenKey, config.tokenRef);
                const password = await this.resolveConfiguredSecret(serviceName, 'password', config.passwordKey, config.passwordRef);
                if (!username) {
                    logger.debug('ServiceRegistry: basic username not found', { serviceName, key: config.tokenKey });
                    return null;
                }
                const encoded = Buffer.from(`${username}:${password ?? ''}`).toString('base64');
                return { Authorization: `Basic ${encoded}` };
            }
            case 'api-key': {
                const key = await this.resolveConfiguredSecret(serviceName, 'primary', config.tokenKey, config.tokenRef);
                if (!key) {
                    logger.debug('ServiceRegistry: api key not found', { serviceName, key: config.tokenKey });
                    return null;
                }
                const headerName = config.apiKeyHeader ?? 'X-API-Key';
                return { [headerName]: key };
            }
            case 'oauth': {
                const token = await this.resolveConfiguredSecret(serviceName, 'primary', config.tokenKey, config.tokenRef);
                if (!token) {
                    logger.debug('ServiceRegistry: oauth token not found', { serviceName, key: config.tokenKey });
                    return null;
                }
                return { Authorization: `Bearer ${token}` };
            }
            default:
                return null;
        }
    }
    async resolveSecret(serviceName, field) {
        const config = this.get(serviceName);
        if (!config)
            return null;
        switch (field) {
            case 'primary':
                return this.resolveConfiguredSecret(serviceName, field, config.tokenKey, config.tokenRef);
            case 'password':
                return this.resolveConfiguredSecret(serviceName, field, config.passwordKey, config.passwordRef);
            case 'webhookUrl':
                return this.resolveConfiguredSecret(serviceName, field, config.webhookUrlKey, config.webhookUrlRef);
            case 'signingSecret':
                return this.resolveConfiguredSecret(serviceName, field, config.signingSecretKey, config.signingSecretRef);
            case 'publicKey':
                return this.resolveConfiguredSecret(serviceName, field, config.publicKeyKey, config.publicKeyRef);
            case 'appToken':
                return this.resolveConfiguredSecret(serviceName, field, config.appTokenKey, config.appTokenRef);
        }
    }
    async inspect(serviceName) {
        const config = this.get(serviceName);
        if (!config)
            return null;
        const [primary, password, webhookUrl, signingSecret, publicKey, appToken,] = await Promise.all([
            this.resolveSecret(serviceName, 'primary'),
            this.resolveSecret(serviceName, 'password'),
            this.resolveSecret(serviceName, 'webhookUrl'),
            this.resolveSecret(serviceName, 'signingSecret'),
            this.resolveSecret(serviceName, 'publicKey'),
            this.resolveSecret(serviceName, 'appToken'),
        ]);
        return {
            config,
            hasPrimaryCredential: primary !== null && primary.length > 0,
            hasPasswordCredential: password !== null && password.length > 0,
            hasWebhookUrl: webhookUrl !== null && webhookUrl.length > 0,
            hasSigningSecret: signingSecret !== null && signingSecret.length > 0,
            hasPublicKey: publicKey !== null && publicKey.length > 0,
            hasAppToken: appToken !== null && appToken.length > 0,
        };
    }
    async testConnection(serviceName) {
        const config = this.get(serviceName);
        if (!config) {
            return { ok: false, status: null, testedUrl: null, error: 'Unknown service' };
        }
        const baseUrl = config.baseUrl?.trim() ?? '';
        if (!baseUrl) {
            return { ok: false, status: null, testedUrl: null, error: 'No baseUrl configured' };
        }
        const headers = await this.resolveAuth(serviceName);
        const reqHeaders = {
            Accept: 'application/json',
            ...(headers ?? {}),
        };
        const candidates = [
            `${baseUrl.replace(/\/$/, '')}/health`,
            baseUrl.replace(/\/$/, ''),
        ];
        for (const url of candidates) {
            try {
                const response = await instrumentedFetch(url, {
                    method: 'GET',
                    headers: reqHeaders,
                    signal: AbortSignal.timeout(5000),
                });
                return {
                    ok: response.ok,
                    status: response.status,
                    testedUrl: url,
                    ...(response.ok ? {} : { error: `HTTP ${response.status}` }),
                };
            }
            catch (error) {
                const message = summarizeError(error);
                logger.debug('ServiceRegistry: service test failed', { serviceName, url, error: message });
            }
        }
        return {
            ok: false,
            status: null,
            testedUrl: candidates[candidates.length - 1] ?? null,
            error: 'Connection failed',
        };
    }
}
