"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.pangolinClient = void 0;
const env_1 = require("../config/env");
const logger_1 = require("../utils/logger");
// --- Client ---
class PangolinClient {
    get baseUrl() {
        return env_1.env.PANGOLIN_API_URL;
    }
    get apiKey() {
        return env_1.env.PANGOLIN_API_KEY;
    }
    get orgId() {
        return env_1.env.PANGOLIN_ORG_ID;
    }
    get configured() {
        return !!(this.baseUrl && this.apiKey && this.orgId);
    }
    async request(method, path, body) {
        if (!this.baseUrl || !this.apiKey) {
            throw new Error('Pangolin API not configured. Set PANGOLIN_API_URL and PANGOLIN_API_KEY.');
        }
        const url = `${this.baseUrl}${path}`;
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 15000);
        try {
            logger_1.logger.debug(`Pangolin ${method} ${path}${body ? ` body=${JSON.stringify(body)}` : ''}`);
            const res = await fetch(url, {
                method,
                headers: {
                    'Authorization': `Bearer ${this.apiKey}`,
                    'Content-Type': 'application/json',
                },
                body: body ? JSON.stringify(body) : undefined,
                signal: controller.signal,
            });
            if (!res.ok) {
                const text = await res.text().catch(() => '');
                logger_1.logger.error(`Pangolin API ${method} ${path} returned ${res.status}: ${text}`);
                throw new Error(`Pangolin API ${method} ${path} returned ${res.status}: ${text}`);
            }
            const contentType = res.headers.get('content-type') || '';
            if (contentType.includes('application/json')) {
                const json = await res.json();
                // Pangolin wraps responses in { data: {...}, success, status }
                // Unwrap if present
                return this.unwrapResponse(json);
            }
            return {};
        }
        finally {
            clearTimeout(timeout);
        }
    }
    /**
     * Unwrap Pangolin's response envelope: { data: {...}, success, status }
     * Returns the inner data object, or the raw response if not wrapped.
     */
    unwrapResponse(json) {
        if (json && typeof json === 'object' && !Array.isArray(json)) {
            const obj = json;
            // If it has a 'data' key and 'success' key, it's a wrapped response
            if ('data' in obj && 'success' in obj) {
                logger_1.logger.debug('Unwrapped Pangolin response envelope');
                return obj.data;
            }
        }
        return json;
    }
    async healthCheck() {
        try {
            const controller = new AbortController();
            const timeout = setTimeout(() => controller.abort(), 5000);
            try {
                const res = await fetch(`${this.baseUrl}/`, {
                    headers: { 'Authorization': `Bearer ${this.apiKey}` },
                    signal: controller.signal,
                });
                return res.ok;
            }
            finally {
                clearTimeout(timeout);
            }
        }
        catch (err) {
            logger_1.logger.warn('Pangolin health check failed:', err instanceof Error ? err.message : err);
            return false;
        }
    }
    // --- Site Defaults ---
    /**
     * Get pre-generated Newt credentials for a new site.
     * Must be called BEFORE createSite() and passed into it.
     * Endpoint: GET /org/{orgId}/pick-site-defaults
     */
    async pickSiteDefaults() {
        const res = await this.request('GET', `/org/${this.orgId}/pick-site-defaults`);
        const obj = res;
        // Response format: { newtId, newtSecret, clientAddress, subnet, ... }
        // Note: `address` is the exit node address, `clientAddress` is the site address
        const newtId = obj.newtId || '';
        const newtSecret = obj.newtSecret || obj.secret || '';
        const address = obj.clientAddress || obj.address || '';
        if (!newtId || !newtSecret) {
            logger_1.logger.warn('pickSiteDefaults response missing newtId/newtSecret:', JSON.stringify(res));
            throw new Error('Pangolin did not return Newt credentials from pick-site-defaults');
        }
        logger_1.logger.info(`pickSiteDefaults: newtId=${newtId}, address=${address}`);
        return { newtId, newtSecret, address };
    }
    // --- Sites ---
    async listSites() {
        const res = await this.request('GET', `/org/${this.orgId}/sites`);
        return this.extractArray(res, 'sites', 'listSites');
    }
    async listExitNodes() {
        try {
            const res = await this.request('GET', `/org/${this.orgId}/exit-nodes`);
            return this.extractArray(res, 'exitNodes', 'listExitNodes');
        }
        catch (err) {
            if (err instanceof Error && err.message.includes('404')) {
                logger_1.logger.info('Pangolin exit-nodes endpoint not available (self-hosted mode)');
                return [];
            }
            logger_1.logger.warn('Failed to fetch exit nodes:', err);
            return [];
        }
    }
    async getSite(siteId) {
        return this.request('GET', `/site/${siteId}`);
    }
    async createSite(data) {
        return this.request('PUT', `/org/${this.orgId}/site`, data);
    }
    async deleteSite(siteId) {
        await this.request('DELETE', `/site/${siteId}`);
    }
    // --- HTTP Resources (public proxy) ---
    /**
     * List HTTP proxy resources (public resources).
     * Endpoint: GET /org/{orgId}/resources (NOT /site-resources)
     */
    async listResources() {
        const res = await this.request('GET', `/org/${this.orgId}/resources`);
        return this.extractArray(res, 'resources', 'listResources');
    }
    async getResource(resourceId) {
        return this.request('GET', `/resource/${resourceId}`);
    }
    /**
     * Create an HTTP proxy resource (public resource).
     * Endpoint: PUT /org/{orgId}/resource (NOT /site-resource)
     */
    async createResource(data) {
        logger_1.logger.info(`createResource: ${data.name} (subdomain: ${data.subdomain || '(root)'})`);
        return this.request('PUT', `/org/${this.orgId}/resource`, data);
    }
    async updateResource(resourceId, data) {
        return this.request('POST', `/resource/${resourceId}`, data);
    }
    async deleteResource(resourceId) {
        await this.request('DELETE', `/resource/${resourceId}`);
    }
    // --- Targets ---
    /**
     * Create a target for a resource (routes traffic to a backend).
     * Endpoint: PUT /resource/{resourceId}/target (PUT, not POST)
     */
    async createTarget(resourceId, data) {
        logger_1.logger.info(`createTarget: resource=${resourceId}, ip=${data.ip}:${data.port}, method=${data.method}`);
        // Pangolin expects siteId as a number, not a string
        const payload = { ...data, siteId: Number(data.siteId) };
        return this.request('PUT', `/resource/${resourceId}/target`, payload);
    }
    /**
     * List targets for a resource.
     * Endpoint: GET /resource/{resourceId}/targets (plural — NOT /target)
     */
    async listTargets(resourceId) {
        const res = await this.request('GET', `/resource/${resourceId}/targets`);
        return this.extractArray(res, 'targets', 'listTargets');
    }
    /**
     * Delete a target by ID.
     * Endpoint: DELETE /target/{targetId} (NOT nested under /resource/)
     */
    async deleteTarget(targetId) {
        await this.request('DELETE', `/target/${targetId}`);
    }
    // --- Domains ---
    async listDomains() {
        const res = await this.request('GET', `/org/${this.orgId}/domains`);
        return this.extractArray(res, 'domains', 'listDomains');
    }
    // --- Certificates ---
    async getCertificate(domainId, domain) {
        return this.request('GET', `/org/${this.orgId}/certificate/${domainId}/${domain}`);
    }
    async updateCertificate(certId, data) {
        return this.request('POST', `/certificate/${certId}`, data);
    }
    // --- Clients ---
    async listClients(resourceId) {
        const res = await this.request('GET', `/resource/${resourceId}/clients`);
        return this.extractArray(res, 'clients', 'listClients');
    }
    // --- Helpers ---
    /**
     * Extract an array from a Pangolin response that may be:
     * - A direct array
     * - An object with the array under a named key (e.g., { sites: [...] })
     * - An object with { data: [...] }
     */
    extractArray(res, key, context) {
        if (Array.isArray(res)) {
            return res;
        }
        if (res && typeof res === 'object') {
            const obj = res;
            // Check named key (e.g., "sites", "resources", "domains")
            if (Array.isArray(obj[key])) {
                return obj[key];
            }
            // Check nested data.{key}
            if (obj.data && typeof obj.data === 'object') {
                const dataObj = obj.data;
                if (Array.isArray(dataObj[key])) {
                    return dataObj[key];
                }
            }
            // Fallback: { data: [...] }
            if (Array.isArray(obj.data)) {
                return obj.data;
            }
        }
        logger_1.logger.warn(`${context}: could not extract array from response, returning empty`);
        return [];
    }
}
exports.pangolinClient = new PangolinClient();
//# sourceMappingURL=pangolin.client.js.map