"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpProvider = exports.HttpProviderConfigSchema = void 0;
exports.createSessionParser = createSessionParser;
exports.createTransformResponse = createTransformResponse;
exports.processJsonBody = processJsonBody;
exports.processTextBody = processTextBody;
exports.createTransformRequest = createTransformRequest;
exports.determineRequestBody = determineRequestBody;
exports.createValidateStatus = createValidateStatus;
const http_z_1 = __importDefault(require("http-z"));
const path_1 = __importDefault(require("path"));
const zod_1 = require("zod");
const cache_1 = require("../cache");
const cliState_1 = __importDefault(require("../cliState"));
const esm_1 = require("../esm");
const logger_1 = __importDefault(require("../logger"));
const util_1 = require("../util");
const file_1 = require("../util/file");
const invariant_1 = __importDefault(require("../util/invariant"));
const json_1 = require("../util/json");
const templates_1 = require("../util/templates");
const shared_1 = require("./shared");
const nunjucks = (0, templates_1.getNunjucksEngine)();
exports.HttpProviderConfigSchema = zod_1.z.object({
    body: zod_1.z.union([zod_1.z.record(zod_1.z.any()), zod_1.z.string(), zod_1.z.array(zod_1.z.any())]).optional(),
    headers: zod_1.z.record(zod_1.z.string()).optional(),
    maxRetries: zod_1.z.number().min(0).optional(),
    method: zod_1.z.string().optional(),
    queryParams: zod_1.z.record(zod_1.z.string()).optional(),
    request: zod_1.z.string().optional(),
    sessionParser: zod_1.z.union([zod_1.z.string(), zod_1.z.function()]).optional(),
    transformRequest: zod_1.z.union([zod_1.z.string(), zod_1.z.function()]).optional(),
    transformResponse: zod_1.z.union([zod_1.z.string(), zod_1.z.function()]).optional(),
    url: zod_1.z.string().optional(),
    validateStatus: zod_1.z
        .union([zod_1.z.string(), zod_1.z.function().returns(zod_1.z.boolean()).args(zod_1.z.number())])
        .optional(),
    /**
     * @deprecated use transformResponse instead
     */
    responseParser: zod_1.z.union([zod_1.z.string(), zod_1.z.function()]).optional(),
});
function contentTypeIsJson(headers) {
    if (!headers) {
        return false;
    }
    return Object.keys(headers).some((key) => {
        if (key.toLowerCase().startsWith('content-type')) {
            return headers?.[key].includes('application/json');
        }
        return false;
    });
}
async function createSessionParser(parser) {
    if (!parser) {
        return () => '';
    }
    if (typeof parser === 'function') {
        return (response) => parser(response);
    }
    if (typeof parser === 'string' && parser.startsWith('file://')) {
        let filename = parser.slice('file://'.length);
        let functionName;
        if (filename.includes(':')) {
            const splits = filename.split(':');
            if (splits[0] && (0, file_1.isJavascriptFile)(splits[0])) {
                [filename, functionName] = splits;
            }
        }
        const requiredModule = await (0, esm_1.importModule)(path_1.default.resolve(cliState_1.default.basePath || '', filename), functionName);
        if (typeof requiredModule === 'function') {
            return requiredModule;
        }
        throw new Error(`Response transform malformed: ${filename} must export a function or have a default export as a function`);
    }
    else if (typeof parser === 'string') {
        return ({ headers }) => {
            return new Function('headers', `return headers[${JSON.stringify(parser)}]`)(headers);
        };
    }
    throw new Error(`Unsupported response transform type: ${typeof parser}. Expected a function, a string starting with 'file://' pointing to a JavaScript file, or a string containing a JavaScript expression.`);
}
async function createTransformResponse(parser) {
    if (!parser) {
        return (data, text) => ({ output: data || text });
    }
    if (typeof parser === 'function') {
        return (data, text) => {
            try {
                const result = parser(data, text);
                return { output: result };
            }
            catch (err) {
                logger_1.default.error(`Error in response transform function: ${String(err)}`);
                throw err;
            }
        };
    }
    if (typeof parser === 'string' && parser.startsWith('file://')) {
        let filename = parser.slice('file://'.length);
        let functionName;
        if (filename.includes(':')) {
            const splits = filename.split(':');
            if (splits[0] && (0, file_1.isJavascriptFile)(splits[0])) {
                [filename, functionName] = splits;
            }
        }
        const requiredModule = await (0, esm_1.importModule)(path_1.default.resolve(cliState_1.default.basePath || '', filename), functionName);
        if (typeof requiredModule === 'function') {
            return requiredModule;
        }
        throw new Error(`Response transform malformed: ${filename} must export a function or have a default export as a function`);
    }
    else if (typeof parser === 'string') {
        return (data, text) => {
            try {
                const trimmedParser = parser.trim();
                const transformFn = new Function('json', 'text', `try { return (${trimmedParser}); } catch(e) { throw new Error('Transform failed: ' + e.message); }`);
                return {
                    output: transformFn(data || null, text),
                };
            }
            catch (err) {
                logger_1.default.error(`Error in response transform: ${String(err)}`);
                throw new Error(`Failed to transform response: ${String(err)}`);
            }
        };
    }
    throw new Error(`Unsupported response transform type: ${typeof parser}. Expected a function, a string starting with 'file://' pointing to a JavaScript file, or a string containing a JavaScript expression.`);
}
function processValue(value, vars) {
    if (typeof value === 'string') {
        const renderedValue = nunjucks.renderString(value, vars || {});
        try {
            return JSON.parse(renderedValue);
        }
        catch {
            return renderedValue;
        }
    }
    return value;
}
function processObjects(body, vars) {
    if (Array.isArray(body)) {
        return body.map((item) => typeof item === 'object' && item !== null
            ? processObjects(item, vars)
            : processValue(item, vars));
    }
    const processedBody = {};
    for (const [key, value] of Object.entries(body)) {
        if (typeof value === 'object' && value !== null) {
            processedBody[key] = processObjects(value, vars);
        }
        else {
            processedBody[key] = processValue(value, vars);
        }
    }
    return processedBody;
}
function processJsonBody(body, vars) {
    // attempting to process a string as a stringifiedJSON object
    if (typeof body === 'string') {
        body = processValue(body, vars);
        if (typeof body == 'string') {
            return body;
        }
        return processObjects(body, vars);
    }
    return processObjects(body, vars);
}
function processTextBody(body, vars) {
    if (body == null) {
        return body;
    }
    (0, invariant_1.default)(typeof body !== 'object', 'Expected body to be a string when content type is not application/json');
    return nunjucks.renderString(body, vars);
}
function parseRawRequest(input) {
    const adjusted = input.trim().replace(/\n/g, '\r\n') + '\r\n\r\n';
    try {
        const messageModel = http_z_1.default.parse(adjusted);
        return {
            method: messageModel.method,
            url: messageModel.target,
            headers: messageModel.headers.reduce((acc, header) => {
                acc[header.name.toLowerCase()] = header.value;
                return acc;
            }, {}),
            body: messageModel.body,
        };
    }
    catch (err) {
        throw new Error(`Error parsing raw HTTP request: ${String(err)}`);
    }
}
async function createTransformRequest(transform) {
    if (!transform) {
        return (prompt) => prompt;
    }
    if (typeof transform === 'function') {
        return async (prompt) => {
            try {
                return await transform(prompt);
            }
            catch (err) {
                const errorMessage = err instanceof Error ? err.message : String(err);
                const wrappedError = new Error(`Error in request transform function: ${errorMessage}`);
                logger_1.default.error(wrappedError.message);
                throw wrappedError;
            }
        };
    }
    if (typeof transform === 'string') {
        if (transform.startsWith('file://')) {
            let filename = transform.slice('file://'.length);
            let functionName;
            if (filename.includes(':')) {
                const splits = filename.split(':');
                if (splits[0] && (0, file_1.isJavascriptFile)(splits[0])) {
                    [filename, functionName] = splits;
                }
            }
            const requiredModule = await (0, esm_1.importModule)(path_1.default.resolve(cliState_1.default.basePath || '', filename), functionName);
            if (typeof requiredModule === 'function') {
                return async (prompt) => {
                    try {
                        return await requiredModule(prompt);
                    }
                    catch (err) {
                        const errorMessage = err instanceof Error ? err.message : String(err);
                        const wrappedError = new Error(`Error in request transform function from ${filename}: ${errorMessage}`);
                        logger_1.default.error(wrappedError.message);
                        throw wrappedError;
                    }
                };
            }
            throw new Error(`Request transform malformed: ${filename} must export a function or have a default export as a function`);
        }
        // Handle string template
        return async (prompt) => {
            try {
                const rendered = nunjucks.renderString(transform, { prompt });
                return await new Function('prompt', `${rendered}`)(prompt);
            }
            catch (err) {
                const errorMessage = err instanceof Error ? err.message : String(err);
                const wrappedError = new Error(`Error in request transform string template: ${errorMessage}`);
                logger_1.default.error(wrappedError.message);
                throw wrappedError;
            }
        };
    }
    throw new Error(`Unsupported request transform type: ${typeof transform}. Expected a function, a string starting with 'file://' pointing to a JavaScript file, or a string containing a JavaScript expression.`);
}
function determineRequestBody(contentType, parsedPrompt, configBody, vars) {
    if (contentType) {
        // For JSON content type
        if (typeof parsedPrompt === 'object' && parsedPrompt !== null) {
            // If parser returned an object, merge it with config body
            return Object.assign({}, configBody || {}, parsedPrompt);
        }
        // Otherwise process the config body with parsed prompt
        return processJsonBody(configBody, {
            ...vars,
            prompt: parsedPrompt,
        });
    }
    // For non-JSON content type, process as text
    return processTextBody(configBody, {
        ...vars,
        prompt: parsedPrompt,
    });
}
async function createValidateStatus(validator) {
    if (!validator) {
        return (status) => true;
    }
    if (typeof validator === 'function') {
        return validator;
    }
    if (typeof validator === 'string') {
        if (validator.startsWith('file://')) {
            let filename = validator.slice('file://'.length);
            let functionName;
            if (filename.includes(':')) {
                const splits = filename.split(':');
                if (splits[0] && (0, file_1.isJavascriptFile)(splits[0])) {
                    [filename, functionName] = splits;
                }
            }
            try {
                const requiredModule = await (0, esm_1.importModule)(path_1.default.resolve(cliState_1.default.basePath || '', filename), functionName);
                if (typeof requiredModule === 'function') {
                    return requiredModule;
                }
                throw new Error('Exported value must be a function');
            }
            catch (err) {
                throw new Error(`Status validator malformed: ${filename} - ${err?.message || String(err)}`);
            }
        }
        // Handle string template - wrap in a function body
        try {
            const trimmedValidator = validator.trim();
            // Check if it's an arrow function or regular function
            if (trimmedValidator.includes('=>') || trimmedValidator.startsWith('function')) {
                // For arrow functions and regular functions, evaluate the whole function
                return new Function(`return ${trimmedValidator}`)();
            }
            // For expressions, wrap in a function body
            return new Function('status', `return ${trimmedValidator}`);
        }
        catch (err) {
            throw new Error(`Invalid status validator expression: ${err?.message || String(err)}`);
        }
    }
    throw new Error(`Unsupported status validator type: ${typeof validator}. Expected a function, a string starting with 'file://' pointing to a JavaScript file, or a string containing a JavaScript expression.`);
}
class HttpProvider {
    constructor(url, options) {
        this.config = exports.HttpProviderConfigSchema.parse(options.config);
        this.url = this.config.url || url;
        this.transformResponse = createTransformResponse(this.config.transformResponse || this.config.responseParser);
        this.sessionParser = createSessionParser(this.config.sessionParser);
        this.transformRequest = createTransformRequest(this.config.transformRequest);
        this.validateStatus = createValidateStatus(this.config.validateStatus);
        if (this.config.request) {
            this.config.request = (0, util_1.maybeLoadFromExternalFile)(this.config.request);
        }
        else {
            (0, invariant_1.default)(this.config.body || this.config.method === 'GET', `Expected HTTP provider ${this.url} to have a config containing {body}, but instead got ${(0, json_1.safeJsonStringify)(this.config)}`);
        }
    }
    id() {
        return this.url;
    }
    toString() {
        return `[HTTP Provider ${this.url}]`;
    }
    getDefaultHeaders(body) {
        if (this.config.method === 'GET') {
            return {};
        }
        if (typeof body === 'object' && body !== null) {
            return { 'content-type': 'application/json' };
        }
        else if (typeof body === 'string') {
            return { 'content-type': 'application/x-www-form-urlencoded' };
        }
        return {};
    }
    validateContentTypeAndBody(headers, body) {
        if (body != null) {
            if (typeof body == 'object' && !contentTypeIsJson(headers)) {
                throw new Error('Content-Type is not application/json, but body is an object or array. The body must be a string if the Content-Type is not application/json.');
            }
            if (typeof body === 'string' && contentTypeIsJson(headers)) {
                logger_1.default.warn('Content-Type is application/json, but body is a string. This is likely to cause unexpected results. It should be an object or array.');
            }
        }
    }
    getHeaders(defaultHeaders, vars) {
        const configHeaders = this.config.headers || {};
        // Convert all keys in configHeaders to lowercase
        const headers = Object.fromEntries(Object.entries(configHeaders).map(([key, value]) => [key.toLowerCase(), value]));
        return Object.fromEntries(Object.entries({ ...defaultHeaders, ...headers }).map(([key, value]) => [
            key,
            nunjucks.renderString(value, vars),
        ]));
    }
    async callApi(prompt, context) {
        const vars = {
            ...(context?.vars || {}),
            prompt,
        };
        if (this.config.request) {
            return this.callApiWithRawRequest(vars);
        }
        const defaultHeaders = this.getDefaultHeaders(this.config.body);
        const headers = this.getHeaders(defaultHeaders, vars);
        this.validateContentTypeAndBody(headers, this.config.body);
        // Transform prompt using request transform
        const transformedPrompt = await (await this.transformRequest)(prompt);
        logger_1.default.debug(`[HTTP Provider]: Transformed prompt: ${transformedPrompt}. Original prompt: ${prompt}`);
        const renderedConfig = {
            url: this.url,
            method: nunjucks.renderString(this.config.method || 'GET', vars),
            headers,
            body: determineRequestBody(contentTypeIsJson(headers), transformedPrompt, this.config.body, vars),
            queryParams: this.config.queryParams
                ? Object.fromEntries(Object.entries(this.config.queryParams).map(([key, value]) => [
                    key,
                    nunjucks.renderString(value, vars),
                ]))
                : undefined,
            transformResponse: this.config.transformResponse || this.config.responseParser,
        };
        const method = renderedConfig.method || 'POST';
        (0, invariant_1.default)(typeof method === 'string', 'Expected method to be a string');
        (0, invariant_1.default)(typeof headers === 'object', 'Expected headers to be an object');
        // Construct URL with query parameters for GET requests
        let url = this.url;
        if (renderedConfig.queryParams) {
            const queryString = new URLSearchParams(renderedConfig.queryParams).toString();
            url = `${url}?${queryString}`;
        }
        logger_1.default.debug(`[HTTP Provider]: Calling ${url} with config: ${(0, json_1.safeJsonStringify)(renderedConfig)}`);
        const response = await (0, cache_1.fetchWithCache)(url, {
            method: renderedConfig.method,
            headers: renderedConfig.headers,
            ...(method !== 'GET' && {
                body: contentTypeIsJson(headers)
                    ? JSON.stringify(renderedConfig.body)
                    : String(renderedConfig.body)?.trim(),
            }),
        }, shared_1.REQUEST_TIMEOUT_MS, 'text', context?.debug, this.config.maxRetries);
        logger_1.default.debug(`[HTTP Provider]: Response: ${response.data}`);
        if (!(await this.validateStatus)(response.status)) {
            throw new Error(`HTTP call failed with status ${response.status} ${response.statusText}: ${response.data}`);
        }
        logger_1.default.debug(`[HTTP Provider]: Response (HTTP ${response.status}): ${response.data}`);
        const ret = {};
        if (context?.debug) {
            ret.raw = response.data;
            ret.metadata = {
                headers: response.headers,
            };
        }
        const rawText = response.data;
        let parsedData;
        try {
            parsedData = JSON.parse(rawText);
        }
        catch {
            parsedData = null;
        }
        const parsedOutput = (await this.transformResponse)(parsedData, rawText);
        ret.output = parsedOutput.output || parsedOutput;
        try {
            const sessionId = response.headers && this.sessionParser !== null
                ? (await this.sessionParser)({ headers: response.headers })
                : undefined;
            if (sessionId) {
                ret.sessionId = sessionId;
            }
        }
        catch (err) {
            logger_1.default.error(`Error parsing session ID: ${String(err)}. Got headers: ${(0, json_1.safeJsonStringify)(response.headers)}`);
            throw err;
        }
        return ret;
    }
    async callApiWithRawRequest(vars) {
        (0, invariant_1.default)(this.config.request, 'Expected request to be set in http provider config');
        const renderedRequest = nunjucks.renderString(this.config.request, vars);
        const parsedRequest = parseRawRequest(renderedRequest.trim());
        const protocol = this.url.startsWith('https') ? 'https' : 'http';
        const url = new URL(parsedRequest.url, `${protocol}://${parsedRequest.headers['host']}`).toString();
        logger_1.default.debug(`[HTTP Provider]: Calling ${url} with raw request: ${parsedRequest}`);
        const response = await (0, cache_1.fetchWithCache)(url, {
            method: parsedRequest.method,
            headers: parsedRequest.headers,
            ...(parsedRequest.body && { body: parsedRequest.body.text.trim() }),
        }, shared_1.REQUEST_TIMEOUT_MS, 'text', undefined, this.config.maxRetries);
        logger_1.default.debug(`[HTTP Provider]: Response: ${response.data}`);
        if (!(await this.validateStatus)(response.status)) {
            throw new Error(`HTTP call failed with status ${response.status} ${response.statusText}: ${response.data}`);
        }
        const rawText = response.data;
        let parsedData;
        try {
            parsedData = JSON.parse(rawText);
        }
        catch {
            parsedData = null;
        }
        const parsedOutput = (await this.transformResponse)(parsedData, rawText);
        return {
            output: parsedOutput.output || parsedOutput,
        };
    }
}
exports.HttpProvider = HttpProvider;
//# sourceMappingURL=http.js.map