"use strict";
/**
 * Management API Client for CLIProxyAPI
 *
 * HTTP client for CLIProxy Management API endpoints.
 * Handles authentication, error mapping, and provides typed methods for CRUD operations.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createManagementClient = exports.ManagementApiClient = void 0;
const https = __importStar(require("https"));
const port_manager_1 = require("../config/port-manager");
/** Default timeout for management operations (longer than health check) */
const DEFAULT_TIMEOUT_MS = 5000;
const ROUTING_STRATEGY_PATH = '/v0/management/routing/strategy';
/** Default port for HTTPS protocol */
const DEFAULT_HTTPS_PORT = 443;
/** Avoid duplicate warnings for repeated invalid port inputs */
const WARNED_INVALID_PORTS = new Set();
function isValidPort(port) {
    return port !== undefined && Number.isInteger(port) && port > 0 && port <= 65535;
}
/**
 * Get effective port based on config and protocol.
 */
function getEffectivePort(port, protocol) {
    if (isValidPort(port)) {
        return port;
    }
    const fallbackPort = protocol === 'https' ? DEFAULT_HTTPS_PORT : port_manager_1.CLIPROXY_DEFAULT_PORT;
    if (port !== undefined) {
        const warningKey = `${protocol}:${String(port)}`;
        if (!WARNED_INVALID_PORTS.has(warningKey)) {
            WARNED_INVALID_PORTS.add(warningKey);
            console.warn(`[management-api-client] Invalid port "${String(port)}", using default ${fallbackPort}`);
        }
    }
    return fallbackPort;
}
/**
 * Build URL for Management API endpoint.
 */
function buildUrl(config, path) {
    const port = getEffectivePort(config.port, config.protocol);
    // Only omit port if it matches standard web ports
    if ((config.protocol === 'https' && port === 443) ||
        (config.protocol === 'http' && port === 80)) {
        return `${config.protocol}://${config.host}${path}`;
    }
    return `${config.protocol}://${config.host}:${port}${path}`;
}
/**
 * Map error to ManagementApiErrorCode.
 */
function mapErrorToCode(error, statusCode) {
    const message = error.message.toLowerCase();
    const rawCode = error.code;
    const code = typeof rawCode === 'string' ? rawCode.toLowerCase() : undefined;
    // DNS resolution failed
    if (code === 'enotfound' || code === 'eai_again' || message.includes('dns')) {
        return 'DNS_FAILED';
    }
    // Network unreachable
    if (code === 'enetunreach' || code === 'ehostunreach' || message.includes('unreachable')) {
        return 'NETWORK_UNREACHABLE';
    }
    // Connection refused
    if (code === 'econnrefused' || message.includes('connection refused')) {
        return 'CONNECTION_REFUSED';
    }
    // Timeout
    if (code === 'etimedout' || message.includes('timeout') || message.includes('aborted')) {
        return 'TIMEOUT';
    }
    // HTTP status codes
    if (statusCode === 401 || statusCode === 403) {
        return 'AUTH_FAILED';
    }
    if (statusCode === 404) {
        return 'NOT_FOUND';
    }
    if (statusCode === 400) {
        return 'BAD_REQUEST';
    }
    if (statusCode && statusCode >= 500) {
        return 'SERVER_ERROR';
    }
    return 'UNKNOWN';
}
/**
 * Get human-readable error message from error code.
 */
function getErrorMessage(errorCode, rawError) {
    switch (errorCode) {
        case 'CONNECTION_REFUSED':
            return 'Connection refused - is CLIProxy running?';
        case 'TIMEOUT':
            return 'Request timed out - server may be slow or unreachable';
        case 'AUTH_FAILED':
            return 'Authentication failed - check management key';
        case 'DNS_FAILED':
            return 'DNS lookup failed - check hostname';
        case 'NETWORK_UNREACHABLE':
            return 'Network unreachable - check if host is accessible';
        case 'NOT_FOUND':
            return 'Endpoint not found - check CLIProxy version';
        case 'BAD_REQUEST':
            return 'Invalid request - check payload format';
        case 'SERVER_ERROR':
            return 'Server error - check CLIProxy logs';
        default:
            return rawError || 'Request failed';
    }
}
/**
 * Management API Client for CLIProxyAPI.
 * Provides typed methods for CRUD operations on claude-api-key configuration.
 */
class ManagementApiClient {
    constructor(config) {
        this.config = config;
        this.timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
    }
    /**
     * Build base URL for display purposes.
     */
    getBaseUrl() {
        return buildUrl(this.config, '');
    }
    /**
     * Check health of Management API.
     * Uses GET /v0/management/claude-api-key as health check.
     */
    async health() {
        const startTime = Date.now();
        try {
            const response = await this.request('GET', '/v0/management/claude-api-key');
            const latencyMs = Date.now() - startTime;
            return {
                healthy: true,
                latencyMs,
                version: response.headers?.['x-cpa-version'],
                commit: response.headers?.['x-cpa-commit'],
            };
        }
        catch (error) {
            const err = error;
            return {
                healthy: false,
                error: err.message,
                errorCode: err.errorCode ?? 'UNKNOWN',
            };
        }
    }
    /**
     * Get all claude-api-key entries from remote CLIProxy.
     */
    async getClaudeKeys() {
        return this.getSection('claude-api-key');
    }
    /**
     * Replace all claude-api-key entries on remote CLIProxy.
     * This is an atomic operation - all entries are replaced at once.
     */
    async putClaudeKeys(keys) {
        await this.putSection('claude-api-key', keys);
    }
    /**
     * Update a single claude-api-key entry by index or api-key match.
     */
    async patchClaudeKey(patch) {
        await this.request('PATCH', '/v0/management/claude-api-key', patch);
    }
    /**
     * Delete a claude-api-key entry by api-key value.
     */
    async deleteClaudeKey(apiKey) {
        const encodedKey = encodeURIComponent(apiKey);
        await this.request('DELETE', `/v0/management/claude-api-key?api-key=${encodedKey}`);
    }
    /**
     * Get model definitions for a channel from CLIProxyAPI.
     * GET /v0/management/model-definitions/:channel
     */
    async getModelDefinitions(channel) {
        const encodedChannel = encodeURIComponent(channel);
        const response = await this.request('GET', `/v0/management/model-definitions/${encodedChannel}`);
        return response.data?.models ?? [];
    }
    /**
     * Get the global credential routing strategy from CLIProxy.
     */
    async getRoutingStrategy() {
        const response = await this.request('GET', ROUTING_STRATEGY_PATH);
        return response.data?.strategy === 'fill-first' ? 'fill-first' : 'round-robin';
    }
    /**
     * Update the global credential routing strategy on CLIProxy.
     */
    async putRoutingStrategy(strategy) {
        await this.request('PUT', ROUTING_STRATEGY_PATH, { value: strategy });
        return strategy;
    }
    /**
     * Get a management section from CLIProxyAPI.
     * Example sections: claude-api-key, gemini-api-key, codex-api-key.
     */
    async getSection(section) {
        const response = await this.request('GET', `/v0/management/${section}`);
        return response.data?.[section] ?? [];
    }
    /**
     * Replace an entire management section on CLIProxyAPI.
     */
    async putSection(section, entries) {
        await this.request('PUT', `/v0/management/${section}`, entries);
    }
    /**
     * Make an HTTP request to the Management API.
     */
    async request(method, path, body) {
        const url = buildUrl(this.config, path);
        const headers = {
            Accept: 'application/json',
            Authorization: `Bearer ${this.config.managementKey}`,
        };
        if (body !== undefined) {
            headers['Content-Type'] = 'application/json';
        }
        // Use native https for self-signed cert support
        if (this.config.protocol === 'https' && this.config.allowSelfSigned) {
            return this.requestWithHttps(method, url, headers, body);
        }
        return this.requestWithFetch(method, url, headers, body);
    }
    /**
     * Make request using native fetch API.
     */
    async requestWithFetch(method, url, headers, body) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.timeout);
        try {
            const response = await fetch(url, {
                method,
                headers,
                body: body !== undefined ? JSON.stringify(body) : undefined,
                signal: controller.signal,
            });
            clearTimeout(timeoutId);
            if (!response.ok) {
                const errorCode = mapErrorToCode(new Error(response.statusText), response.status);
                const error = new Error(getErrorMessage(errorCode));
                error.statusCode = response.status;
                error.errorCode = errorCode;
                throw error;
            }
            // Extract headers we care about
            const responseHeaders = {};
            const version = response.headers.get('x-cpa-version');
            const commit = response.headers.get('x-cpa-commit');
            if (version)
                responseHeaders['x-cpa-version'] = version;
            if (commit)
                responseHeaders['x-cpa-commit'] = commit;
            // Parse JSON response if present
            const text = await response.text();
            let data;
            if (text) {
                try {
                    data = JSON.parse(text);
                }
                catch {
                    // Non-JSON response is ok for PUT/DELETE
                }
            }
            return { data, headers: responseHeaders };
        }
        catch (error) {
            clearTimeout(timeoutId);
            const err = error;
            if (!err.errorCode) {
                err.errorCode = mapErrorToCode(err, err.statusCode);
                err.message = getErrorMessage(err.errorCode, err.message);
            }
            throw err;
        }
    }
    /**
     * Make request using native https module for self-signed cert support.
     */
    async requestWithHttps(method, url, headers, body) {
        return new Promise((resolve, reject) => {
            const agent = new https.Agent({ rejectUnauthorized: false });
            const bodyStr = body !== undefined ? JSON.stringify(body) : undefined;
            if (bodyStr) {
                headers['Content-Length'] = Buffer.byteLength(bodyStr).toString();
            }
            const reqTimeout = setTimeout(() => {
                reject(new Error('Request timeout'));
            }, this.timeout);
            const req = https.request(url, {
                method,
                headers,
                agent,
                timeout: this.timeout,
            }, (res) => {
                clearTimeout(reqTimeout);
                let data = '';
                res.on('data', (chunk) => (data += chunk));
                res.on('end', () => {
                    if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
                        const errorCode = mapErrorToCode(new Error(res.statusMessage || ''), res.statusCode);
                        const error = new Error(getErrorMessage(errorCode));
                        error.statusCode = res.statusCode;
                        error.errorCode = errorCode;
                        reject(error);
                        return;
                    }
                    const responseHeaders = {};
                    const version = res.headers['x-cpa-version'];
                    const commit = res.headers['x-cpa-commit'];
                    if (typeof version === 'string')
                        responseHeaders['x-cpa-version'] = version;
                    if (typeof commit === 'string')
                        responseHeaders['x-cpa-commit'] = commit;
                    let parsed;
                    if (data) {
                        try {
                            parsed = JSON.parse(data);
                        }
                        catch {
                            // Non-JSON response is ok
                        }
                    }
                    resolve({ data: parsed, headers: responseHeaders });
                });
            });
            req.on('error', (err) => {
                clearTimeout(reqTimeout);
                const error = err;
                error.errorCode = mapErrorToCode(err);
                error.message = getErrorMessage(error.errorCode, err.message);
                reject(error);
            });
            req.on('timeout', () => {
                req.destroy();
                const error = new Error('Request timeout');
                error.errorCode = 'TIMEOUT';
                reject(error);
            });
            if (bodyStr) {
                req.write(bodyStr);
            }
            req.end();
        });
    }
}
exports.ManagementApiClient = ManagementApiClient;
/**
 * Create a ManagementApiClient from CCS config.
 * Uses cliproxy_server.remote settings.
 */
function createManagementClient(remoteConfig, allowSelfSigned = true) {
    return new ManagementApiClient({
        host: remoteConfig.host,
        port: remoteConfig.port,
        protocol: remoteConfig.protocol,
        managementKey: remoteConfig.management_key || remoteConfig.auth_token || '',
        timeout: remoteConfig.timeout,
        allowSelfSigned,
    });
}
exports.createManagementClient = createManagementClient;
//# sourceMappingURL=management-api-client.js.map