/**
 * Minimal Gateway Client for OS CLI
 * Self-contained client for connecting to 4Runr Gateway
 */
import axios from 'axios';
import { loadConfig } from './config.js';
import { validateGatewayUrl } from './security/ssrf-protection.js';
/** Seeded by `npm run seed:api-key` in local Gateway bundles. */
export const DEFAULT_LOCAL_DEV_API_KEY = 'test-key-ce67dabb0d4e27e3d72877c921b89cae';
function isLoopbackGatewayUrl(url) {
    try {
        const h = new URL(url).hostname.toLowerCase();
        return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0.0.0.0';
    }
    catch {
        return false;
    }
}
/**
 * Resolve Gateway auth for API routes (`/api/runs`, etc.).
 * Local loopback falls back to the default dev key so the TUI works after portal connect.
 */
export function resolveGatewayApiKey(options) {
    if (options?.authToken) {
        return { authToken: options.authToken };
    }
    if (options?.apiKey) {
        return { apiKey: options.apiKey };
    }
    const envApiKey = process.env.GATEWAY_API_KEY || process.env.API_KEY || process.env.TEST_API_KEY;
    if (envApiKey) {
        return { apiKey: envApiKey };
    }
    const envToken = process.env.GATEWAY_TOKEN || process.env.AUTH_TOKEN;
    if (envToken) {
        return { authToken: envToken };
    }
    try {
        const cfg = loadConfig();
        if (typeof cfg.gatewayApiKey === 'string' && cfg.gatewayApiKey.trim()) {
            return { apiKey: cfg.gatewayApiKey.trim() };
        }
    }
    catch {
        /* config optional */
    }
    if (options?.gatewayUrl && isLoopbackGatewayUrl(options.gatewayUrl)) {
        return { apiKey: DEFAULT_LOCAL_DEV_API_KEY };
    }
    return {};
}
/**
 * Use 127.0.0.1 instead of localhost for HTTP calls to the bundled Gateway.
 * On Windows, `localhost` often resolves to ::1 first while the Gateway binds 127.0.0.1 only,
 * so /health never succeeds until this normalization.
 */
/** True if JSON body is from 4Runr Gateway GET /health (not another app on the port). */
export function isGatewayHealthPayload(data) {
    if (!data || typeof data !== 'object')
        return false;
    const d = data;
    if (d.ok === true)
        return true;
    const s = d.status;
    if (typeof s === 'string') {
        const sl = s.toLowerCase();
        return sl === 'ok' || sl === 'healthy' || sl === 'alive';
    }
    return false;
}
export function preferIpv4LoopbackGatewayUrl(url) {
    try {
        const u = new URL(url);
        if (u.hostname === 'localhost') {
            u.hostname = '127.0.0.1';
        }
        let out = u.toString();
        if (out.endsWith('/')) {
            out = out.slice(0, -1);
        }
        return out;
    }
    catch {
        return url.replace(/\/+$/, '');
    }
}
export class GatewayClient {
    http;
    gatewayUrl;
    runs;
    constructor(config) {
        // SECURITY: Validate gateway URL to prevent SSRF
        const validation = validateGatewayUrl(config.gatewayUrl);
        if (!validation.valid) {
            throw new Error(`Invalid gateway URL: ${validation.error}`);
        }
        this.gatewayUrl = config.gatewayUrl;
        const axiosBaseUrl = preferIpv4LoopbackGatewayUrl(config.gatewayUrl);
        const headers = {
            'Content-Type': 'application/json',
        };
        const resolved = resolveGatewayApiKey({
            gatewayUrl: config.gatewayUrl,
            apiKey: config.apiKey,
            authToken: config.authToken,
        });
        if (resolved.apiKey) {
            headers['x-api-key'] = resolved.apiKey;
        }
        else if (resolved.authToken) {
            headers['Authorization'] = `Bearer ${resolved.authToken}`;
        }
        this.http = axios.create({
            baseURL: axiosBaseUrl,
            timeout: config.timeout || 30000, // Default 30s, but can be overridden
            headers,
        });
        // Disable logging by default (can be enabled via env var)
        if (!process.env.ENABLE_GATEWAY_LOGGING) {
            // Suppress axios logs
            this.http.interceptors.request.use((config) => config, (error) => Promise.reject(error));
            this.http.interceptors.response.use((response) => response, (error) => Promise.reject(error));
        }
        // Create runs API
        this.runs = {
            create: async (data) => {
                const response = await this.http.post('/api/runs', data);
                const body = response.data;
                if (body && typeof body === 'object' && 'run' in body && body.run) {
                    return body.run;
                }
                return body;
            },
            start: async (id) => {
                await this.http.post(`/api/runs/${id}/start`);
            },
            cancel: async (id) => {
                await this.http.post(`/api/runs/${id}/cancel`, {});
            },
            get: async (id) => {
                const response = await this.http.get(`/api/runs/${id}`);
                return response.data;
            },
            list: async (options) => {
                const params = new URLSearchParams();
                if (options?.limit)
                    params.append('limit', options.limit.toString());
                if (options?.status)
                    params.append('status', options.status);
                const response = await this.http.get(`/api/runs?${params.toString()}`);
                // Gateway returns { runs: Run[] }, extract the array
                // Handle both formats: { runs: [...] } or direct array
                if (Array.isArray(response.data)) {
                    return response.data;
                }
                if (response.data && Array.isArray(response.data.runs)) {
                    return response.data.runs;
                }
                // Fallback: return empty array if format is unexpected
                console.warn('[GatewayClient] Unexpected response format from /api/runs:', response.data);
                return [];
            },
        };
    }
    async health() {
        const response = await this.http.get('/health');
        return response.data;
    }
    /**
     * Live proof the configured URL is the 4Runr Gateway: validates /health shape, then GET /ready.
     */
    async verify(options) {
        const gatewayUrl = this.gatewayUrl;
        const t0 = Date.now();
        try {
            const health = await this.health();
            const healthLatencyMs = Date.now() - t0;
            if (!isGatewayHealthPayload(health)) {
                return {
                    ok: false,
                    gatewayUrl,
                    healthLatencyMs,
                    health,
                    error: '/health responded but body is not a 4Runr Gateway health payload',
                };
            }
            const t1 = Date.now();
            const timeout = options?.readyTimeoutMs ?? 10000;
            const readyRes = await this.http.get('/ready', { validateStatus: () => true, timeout });
            const readyLatencyMs = Date.now() - t1;
            const data = readyRes.data;
            let ready;
            let readyChecks;
            if (data && typeof data === 'object' && 'ready' in data) {
                const o = data;
                ready = {
                    ready: Boolean(o.ready),
                    httpStatus: readyRes.status,
                    statusText: typeof o.status === 'string' ? o.status : undefined,
                };
                const checks = o.checks;
                if (checks && typeof checks === 'object') {
                    const lines = [];
                    const pick = (name, raw) => {
                        if (!raw || typeof raw !== 'object')
                            return;
                        const c = raw;
                        const st = typeof c.status === 'string' ? c.status : String(c.status ?? '');
                        lines.push({
                            name,
                            status: st,
                            message: typeof c.message === 'string' ? c.message : undefined,
                            error: typeof c.error === 'string' ? c.error : undefined,
                        });
                    };
                    const ch = checks;
                    pick('database', ch.database);
                    pick('redis', ch.redis);
                    pick('queue', ch.queue);
                    if (lines.length > 0)
                        readyChecks = lines;
                }
            }
            else {
                ready = { ready: false, httpStatus: readyRes.status };
            }
            return {
                ok: true,
                gatewayUrl,
                healthLatencyMs,
                health,
                ready,
                readyChecks,
                readyLatencyMs,
            };
        }
        catch (e) {
            const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e);
            return {
                ok: false,
                gatewayUrl,
                healthLatencyMs: Date.now() - t0,
                health: null,
                error: msg,
            };
        }
    }
    /**
     * Raw Prometheus text from GET /metrics (public on Gateway; no auth).
     */
    async prometheusMetrics() {
        const response = await this.http.get('/metrics', {
            responseType: 'text',
            validateStatus: () => true,
            timeout: 12000,
        });
        if (response.status !== 200) {
            throw new Error(`/metrics returned HTTP ${response.status}`);
        }
        const body = response.data;
        return typeof body === 'string' ? body : String(body ?? '');
    }
    /** GET JSON (monitoring APIs, etc.). Uses same auth headers as the rest of the client. */
    async getJson(path) {
        const response = await this.http.get(path, {
            validateStatus: () => true,
            timeout: 15000,
        });
        if (response.status >= 400) {
            const body = response.data;
            const detail = body && typeof body === 'object'
                ? String(body.error ??
                    body.message ??
                    '')
                : typeof body === 'string'
                    ? body.slice(0, 240)
                    : '';
            const authHint = response.status === 401 || response.status === 403
                ? ' (authentication failed or expired; reconnect to the Gateway and retry)'
                : '';
            throw new Error(`${path} returned HTTP ${response.status}${authHint}${detail ? `: ${detail}` : ''}`);
        }
        return response.data;
    }
    /** POST JSON with auth headers. */
    async postJson(path, body) {
        const response = await this.http.post(path, body, {
            validateStatus: () => true,
            timeout: 15000,
        });
        if (response.status >= 400) {
            const data = response.data;
            const detail = data && typeof data === 'object'
                ? String(data.error ??
                    data.message ??
                    '')
                : typeof data === 'string'
                    ? data.slice(0, 240)
                    : '';
            throw new Error(`${path} returned HTTP ${response.status}${detail ? `: ${detail}` : ''}`);
        }
        return response.data;
    }
}
//# sourceMappingURL=gateway-client.js.map