"use strict";
/**
 * Schema-driven data exchange with external service endpoints.
 *
 * Three pillars: endpoint + schema + identity.
 * Transport (fetch, Playwright, gRPC) is an implementation detail.
 * The value is schema enforcement and credential resolution —
 * validating both sides of the exchange and resolving auth from
 * the connection store at the last mile.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateSchema = validateSchema;
exports.exchange = exchange;
const ajv_1 = __importDefault(require("ajv"));
const context_1 = require("../../services/iam/context");
const credentials_1 = require("../../services/iam/credentials");
const DEFAULT_TIMEOUT = parseInt(process.env.LT_SCHEMA_EXCHANGE_TIMEOUT_MS || '30000', 10);
const ajv = new ajv_1.default({ allErrors: true, strict: false });
/**
 * Normalize a JSON Schema object before validation.
 *
 * HotMesh YAML maps serialize JSON arrays as indexed objects
 * (e.g. `required: {"0":"username","1":"password"}` instead of
 * `required: ["username","password"]`). This recursively converts
 * any indexed-object `required` fields back to arrays so Ajv
 * validates correctly.
 */
function normalizeSchema(obj) {
    if (obj === null || obj === undefined || typeof obj !== 'object')
        return obj;
    if (Array.isArray(obj))
        return obj.map(normalizeSchema);
    const result = {};
    for (const [key, value] of Object.entries(obj)) {
        if (key === 'required' && value && typeof value === 'object' && !Array.isArray(value)) {
            // Convert indexed object {"0":"a","1":"b"} to array ["a","b"]
            const keys = Object.keys(value);
            if (keys.every(k => /^\d+$/.test(k))) {
                result[key] = keys.sort((a, b) => Number(a) - Number(b)).map(k => value[k]);
                continue;
            }
        }
        result[key] = normalizeSchema(value);
    }
    return result;
}
/**
 * Validate data against a JSON Schema.
 *
 * Returns a list of human-readable error strings (empty when valid).
 */
function validateSchema(data, schema) {
    const normalized = normalizeSchema(schema);
    const validate = ajv.compile(normalized);
    const valid = validate(data);
    if (valid)
        return { valid: true, errors: [] };
    const errors = (validate.errors || []).map((e) => {
        const path = e.instancePath || '(root)';
        return `${path}: ${e.message}${e.params ? ` (${JSON.stringify(e.params)})` : ''}`;
    });
    return { valid: false, errors };
}
/**
 * Exchange data with an external service endpoint under schema enforcement.
 *
 * 1. If request_schema provided, validate body before sending.
 *    On failure: return immediately (never send the request).
 * 2. Make the HTTP call (transport layer — currently Node.js fetch).
 * 3. Parse response body (JSON auto-detect).
 * 4. If response_schema provided, validate response after receiving.
 * 5. Return result with validated flag and any validation_errors.
 */
async function exchange(args) {
    // 1. Validate request body against schema (if provided)
    if (args.request_schema && args.body !== undefined) {
        const result = validateSchema(args.body, args.request_schema);
        if (!result.valid) {
            return {
                status: 0,
                data: null,
                headers: {},
                elapsed_ms: 0,
                validated: false,
                validation_errors: result.errors.map((e) => `request: ${e}`),
            };
        }
    }
    // 2. Resolve credential from connection store (if credential_provider set)
    if (args.credential_provider) {
        const ctx = (0, context_1.getToolContext)();
        if (!ctx?.principal) {
            return {
                status: 0, data: null, headers: {}, elapsed_ms: 0,
                validated: false,
                validation_errors: ['credential: no identity context — cannot resolve credential_provider without a principal'],
            };
        }
        const cred = await (0, credentials_1.resolveCredential)(ctx.principal, args.credential_provider, args.credential_label, ctx.initiatingPrincipal ? { fallbackPrincipal: ctx.initiatingPrincipal } : undefined);
        if (!cred) {
            return {
                status: 0, data: null, headers: {}, elapsed_ms: 0,
                validated: false,
                validation_errors: [`credential: no credential found for provider "${args.credential_provider}" — register one at Settings > Connections`],
            };
        }
        const scheme = args.auth_scheme || 'Bearer';
        const headerName = args.auth_header || 'Authorization';
        args.headers = { ...args.headers, [headerName]: `${scheme} ${cred.value}` };
    }
    // 3. Build URL with query parameters (accept endpoint or url)
    let url = args.endpoint || args.url;
    if (!url) {
        return {
            status: 0, data: null, headers: {}, elapsed_ms: 0,
            validated: false, validation_errors: ['endpoint or url is required'],
        };
    }
    if (args.query && Object.keys(args.query).length > 0) {
        const params = new URLSearchParams(args.query);
        url += (url.includes('?') ? '&' : '?') + params.toString();
    }
    // 3. Make the HTTP call
    const start = performance.now();
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), args.timeout_ms || DEFAULT_TIMEOUT);
    let status;
    let responseHeaders = {};
    let rawBody;
    try {
        const fetchOpts = {
            method: args.method,
            headers: args.headers,
            signal: controller.signal,
        };
        if (args.body !== undefined && args.method !== 'GET' && args.method !== 'HEAD') {
            fetchOpts.body = typeof args.body === 'string' ? args.body : JSON.stringify(args.body);
            if (!args.headers?.['Content-Type'] && !args.headers?.['content-type']) {
                fetchOpts.headers = { ...fetchOpts.headers, 'Content-Type': 'application/json' };
            }
        }
        const response = await fetch(url, fetchOpts);
        status = response.status;
        response.headers.forEach((value, key) => { responseHeaders[key] = value; });
        rawBody = await response.text();
    }
    catch (err) {
        const elapsed = performance.now() - start;
        return {
            status: 0,
            data: null,
            headers: {},
            elapsed_ms: Math.round(elapsed),
            validated: false,
            validation_errors: [`transport: ${err.name === 'AbortError' ? 'request timed out' : err.message}`],
        };
    }
    finally {
        clearTimeout(timeout);
    }
    const elapsed = performance.now() - start;
    // 3b. Fail fast on auth errors — don't silently return a 401 as a "result"
    if (status === 401 || status === 403) {
        return {
            status,
            data: null,
            headers: responseHeaders,
            elapsed_ms: Math.round(elapsed),
            validated: false,
            validation_errors: [
                status === 401
                    ? 'authentication: token expired or invalid — refresh credentials and retry'
                    : 'authorization: insufficient permissions for this endpoint',
            ],
        };
    }
    // 4. Parse response body
    let data;
    const contentType = responseHeaders['content-type'] || '';
    if (contentType.includes('json') || (rawBody.startsWith('{') || rawBody.startsWith('['))) {
        try {
            data = JSON.parse(rawBody);
        }
        catch {
            data = rawBody;
        }
    }
    else {
        data = rawBody;
    }
    // 5. Validate response against schema (if provided)
    let validated = true;
    let validationErrors = [];
    if (args.response_schema) {
        const result = validateSchema(data, args.response_schema);
        validated = result.valid;
        validationErrors = result.errors.map((e) => `response: ${e}`);
    }
    return {
        status,
        data,
        headers: responseHeaders,
        elapsed_ms: Math.round(elapsed),
        validated,
        validation_errors: validationErrors,
    };
}
