import { log } from '@mondaydotcomorg/atp-runtime';
import { readFile } from 'node:fs/promises';
import yaml from 'js-yaml';
/**
 * Type guard to check if spec is OpenAPI 3.0+
 */
function isOpenAPI3(spec) {
    return 'openapi' in spec;
}
/**
 * Type guard to check if spec is Swagger 2.0
 */
function isSwagger2(spec) {
    return 'swagger' in spec;
}
/**
 * Load OpenAPI specification and convert to ATP API group
 */
export async function loadOpenAPI(source, options = {}) {
    const spec = await loadSpec(source);
    const name = options.name || spec.info.title.toLowerCase().replace(/\s+/g, '-');
    let baseURL = options.baseURL;
    if (!baseURL) {
        if (isOpenAPI3(spec) && spec.servers && spec.servers[0]) {
            baseURL = spec.servers[0].url;
        }
        else if (isSwagger2(spec) && spec.host) {
            const scheme = spec.schemes?.[0] || 'https';
            const host = spec.host;
            const basePath = spec.basePath || '';
            baseURL = `${scheme}://${host}${basePath}`;
        }
        else {
            baseURL = '';
        }
    }
    // Detect auth first so we can pass it to handlers
    const auth = detectAuth(spec, options.authProvider);
    const functions = [];
    for (const [path, methods] of Object.entries(spec.paths)) {
        for (const [method, operation] of Object.entries(methods)) {
            if (['parameters', 'servers', 'summary', 'description'].includes(method)) {
                continue;
            }
            if (!shouldIncludeOperation(operation, path, method, options.filter)) {
                continue;
            }
            const func = convertOperation(path, method, operation, spec, baseURL, options, auth);
            if (func) {
                functions.push(func);
            }
        }
    }
    return {
        name,
        type: 'openapi',
        functions,
        auth,
    };
}
/**
 * Load OpenAPI spec from file or URL
 */
async function loadSpec(source) {
    let content;
    let isYaml = false;
    if (source.startsWith('http://') || source.startsWith('https://')) {
        const response = await fetch(source);
        if (!response.ok) {
            throw new Error(`Failed to load OpenAPI spec from ${source}: ${response.statusText}`);
        }
        content = await response.text();
        const contentType = response.headers.get('content-type');
        isYaml =
            contentType?.includes('yaml') ||
                contentType?.includes('yml') ||
                source.endsWith('.yaml') ||
                source.endsWith('.yml');
    }
    else {
        content = await readFile(source, 'utf-8');
        isYaml = source.endsWith('.yaml') || source.endsWith('.yml');
    }
    try {
        if (isYaml) {
            return yaml.load(content);
        }
        else {
            try {
                return JSON.parse(content);
            }
            catch {
                return yaml.load(content);
            }
        }
    }
    catch (error) {
        throw new Error(`Failed to parse OpenAPI spec: ${error.message}`);
    }
}
/**
 * Check if operation should be included based on filters
 */
function shouldIncludeOperation(operation, path, method, filter) {
    if (!filter)
        return true;
    if (filter.tags && filter.tags.length > 0) {
        if (!operation.tags || !operation.tags.some((t) => filter.tags.includes(t))) {
            return false;
        }
    }
    if (filter.paths && filter.paths.length > 0) {
        if (!filter.paths.some((pattern) => matchPathPattern(path, pattern))) {
            return false;
        }
    }
    if (filter.exclude && filter.exclude.length > 0) {
        if (filter.exclude.some((pattern) => matchPathPattern(path, pattern))) {
            return false;
        }
    }
    if (filter.methods && filter.methods.length > 0) {
        if (!filter.methods.includes(method.toUpperCase())) {
            return false;
        }
    }
    if (operation.deprecated) {
        return false;
    }
    if (filter.operation) {
        return filter.operation(operation, path, method);
    }
    return true;
}
/**
 * Match path against pattern (supports wildcards)
 */
function matchPathPattern(path, pattern) {
    const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
    return new RegExp(`^${regexPattern}$`).test(path);
}
/**
 * Convert OpenAPI operation to ATP function
 */
function convertOperation(path, method, operation, spec, baseURL, options, auth) {
    const functionName = operation.operationId || `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`;
    const operationKey = `${method.toUpperCase()} ${path}`;
    const description = options.descriptions?.[operationKey] ||
        operation.summary ||
        operation.description ||
        `${method.toUpperCase()} ${path}`;
    const inputSchema = buildInputSchema(operation, spec);
    const outputSchema = buildOutputSchema(operation, spec);
    const annotations = extractAnnotations(operation, operationKey, options.annotations);
    const handler = async (params, handlerContext) => {
        const input = params || {};
        let requestPath = path;
        const queryParams = {};
        let body = undefined;
        const headers = {
            'Content-Type': 'application/json',
        };
        let context;
        if (options.contextProvider) {
            context = await options.contextProvider(handlerContext?.requestContext);
        }
        if (options.headerProvider) {
            const dynamicHeaders = await options.headerProvider(input, context);
            Object.assign(headers, dynamicHeaders);
            log.debug('Added headers from headerProvider', { keys: Object.keys(dynamicHeaders) });
        }
        if (auth && !headers['Authorization']) {
            if (auth.scheme === 'bearer' && auth.envVar) {
                let token = null;
                if (options.authProvider) {
                    token = await options.authProvider.getCredential(auth.envVar);
                    log.debug(`Got token from authProvider for ${auth.envVar}`, {
                        found: !!token,
                        preview: token ? token.substring(0, 20) + '...' : undefined,
                    });
                }
                if (!token) {
                    token = process.env[auth.envVar] || null;
                    log.debug(`Got token from process.env[${auth.envVar}]`, {
                        found: !!token,
                        preview: token ? token.substring(0, 20) + '...' : undefined,
                    });
                }
                if (token) {
                    headers['Authorization'] = `Bearer ${token}`;
                }
                else {
                    log.warn(`${auth.envVar} not found! Set it in authProvider or environment.`);
                }
            }
            else if (auth.scheme === 'basic') {
                let username = null;
                let password = null;
                if (options.authProvider && auth.usernameEnvVar && auth.passwordEnvVar) {
                    username = await options.authProvider.getCredential(auth.usernameEnvVar);
                    password = await options.authProvider.getCredential(auth.passwordEnvVar);
                }
                if (!username && auth.usernameEnvVar) {
                    username = process.env[auth.usernameEnvVar] || null;
                }
                if (!password && auth.passwordEnvVar) {
                    password = process.env[auth.passwordEnvVar] || null;
                }
                if (username && password) {
                    const credentials = Buffer.from(`${username}:${password}`).toString('base64');
                    headers['Authorization'] = `Basic ${credentials}`;
                }
            }
            else if (auth.scheme === 'apiKey') {
                let apiKey = null;
                const apiKeyEnvVar = auth.envVar || 'API_KEY';
                if (options.authProvider) {
                    apiKey = await options.authProvider.getCredential(apiKeyEnvVar);
                }
                if (!apiKey) {
                    apiKey = process.env[apiKeyEnvVar] || null;
                }
                if (apiKey) {
                    if (auth.in === 'header') {
                        headers[auth.name] = apiKey;
                    }
                    else if (auth.in === 'query') {
                        queryParams[auth.name] = apiKey;
                    }
                }
            }
        }
        if (operation.parameters) {
            for (const param of operation.parameters) {
                if (param.in === 'path' && input[param.name]) {
                    requestPath = requestPath.replace(`{${param.name}}`, encodeURIComponent(String(input[param.name])));
                }
                else if (param.in === 'query' && input[param.name] !== undefined) {
                    queryParams[param.name] = String(input[param.name]);
                }
                else if (param.in === 'header' && input[param.name]) {
                    headers[param.name] = String(input[param.name]);
                }
            }
        }
        if (operation.requestBody && ['post', 'put', 'patch'].includes(method.toLowerCase())) {
            const bodyParams = {};
            if (operation.parameters) {
                const paramNames = operation.parameters.map((p) => p.name);
                for (const key in input) {
                    if (!paramNames.includes(key)) {
                        bodyParams[key] = input[key];
                    }
                }
            }
            else {
                Object.assign(bodyParams, input);
            }
            if (Object.keys(bodyParams).length > 0) {
                body = bodyParams;
            }
        }
        if (!baseURL) {
            throw new Error(`No baseURL configured for OpenAPI spec. Check that the spec has a 'servers' section with a valid URL.`);
        }
        const baseUrlObj = new URL(baseURL);
        const basePath = baseUrlObj.pathname.replace(/\/$/, '');
        const fullPath = basePath + requestPath;
        const url = new URL(fullPath, baseUrlObj.origin);
        for (const [key, value] of Object.entries(queryParams)) {
            url.searchParams.append(key, value);
        }
        try {
            let finalUrl = url.toString();
            let finalMethod = method.toUpperCase();
            let finalHeaders = { ...headers };
            let finalBody = body ? JSON.stringify(body) : undefined;
            if (options.requestTransformer) {
                const transformed = await options.requestTransformer({
                    url: finalUrl,
                    method: finalMethod,
                    headers: finalHeaders,
                    body,
                });
                if (transformed.url)
                    finalUrl = transformed.url;
                if (transformed.method)
                    finalMethod = transformed.method;
                if (transformed.headers)
                    finalHeaders = transformed.headers;
                if (transformed.body !== undefined)
                    finalBody = transformed.body;
            }
            const response = await fetch(finalUrl, {
                method: finalMethod,
                headers: finalHeaders,
                body: finalBody,
            });
            if (!response.ok) {
                const errorText = await response.text();
                throw new Error(`HTTP ${response.status}: ${errorText}`);
            }
            if (response.status === 204) {
                return { success: true };
            }
            const contentType = response.headers.get('content-type');
            if (contentType?.includes('application/json')) {
                const text = await response.text();
                if (!text || text.trim() === '') {
                    return { success: true };
                }
                return JSON.parse(text);
            }
            else {
                return await response.text();
            }
        }
        catch (error) {
            throw new Error(`Failed to execute ${method.toUpperCase()} ${path}: ${error.message}`);
        }
    };
    return {
        name: functionName,
        description,
        inputSchema,
        outputSchema,
        handler,
        keywords: operation.tags || [],
    };
}
/**
 * Build input JSON schema from parameters and requestBody
 */
function buildInputSchema(operation, spec) {
    const properties = {};
    const required = [];
    if (operation.parameters) {
        for (const param of operation.parameters) {
            if (param.schema) {
                const paramSchema = resolveSchema(param.schema, spec);
                properties[param.name] =
                    typeof paramSchema === 'object' && paramSchema !== null
                        ? { ...paramSchema, description: param.description || paramSchema.description }
                        : paramSchema;
            }
            else {
                properties[param.name] = { type: 'string', description: param.description };
            }
            if (param.required) {
                required.push(param.name);
            }
        }
    }
    if (operation.requestBody?.content?.['application/json']?.schema) {
        const bodySchema = resolveSchema(operation.requestBody.content['application/json'].schema, spec);
        if (typeof bodySchema === 'object' && bodySchema !== null) {
            if ('allOf' in bodySchema) {
                if (Object.keys(properties).length > 0) {
                    return {
                        type: 'object',
                        properties,
                        required: required.length > 0 ? required : undefined,
                        allOf: bodySchema.allOf,
                    };
                }
                return bodySchema;
            }
            if ('additionalProperties' in bodySchema && !('properties' in bodySchema)) {
                if (Object.keys(properties).length > 0) {
                    return {
                        type: 'object',
                        properties,
                        required: required.length > 0 ? required : undefined,
                        additionalProperties: bodySchema.additionalProperties,
                    };
                }
                return bodySchema;
            }
            if ('properties' in bodySchema) {
                Object.assign(properties, bodySchema.properties);
                if ('required' in bodySchema && Array.isArray(bodySchema.required)) {
                    required.push(...bodySchema.required);
                }
            }
        }
    }
    return {
        type: 'object',
        properties,
        required: required.length > 0 ? required : undefined,
    };
}
/**
 * Build output JSON schema from responses
 */
function buildOutputSchema(operation, spec) {
    const successResponse = operation.responses?.['200'] ||
        operation.responses?.['201'] ||
        operation.responses?.['default'];
    if (!successResponse?.content?.['application/json']?.schema) {
        return undefined;
    }
    return resolveSchema(successResponse.content['application/json'].schema, spec, new Set());
}
/**
 * Resolve schema references ($ref) with circular reference detection.
 * Preserves all JSON Schema fields.
 */
function resolveSchema(schema, spec, visited = new Set()) {
    if (schema.$ref) {
        if (visited.has(schema.$ref)) {
            return { type: 'object', description: 'Circular reference: ' + schema.$ref };
        }
        const refPath = schema.$ref.split('/').slice(1);
        let resolved = spec;
        for (const part of refPath) {
            resolved = resolved?.[part];
        }
        if (resolved) {
            visited.add(schema.$ref);
            const result = resolveSchema(resolved, spec, visited);
            visited.delete(schema.$ref);
            return result;
        }
    }
    if (schema.allOf) {
        const result = {
            allOf: schema.allOf.map((s) => resolveSchema(s, spec, visited)),
        };
        if (schema.description)
            result.description = schema.description;
        return result;
    }
    const jsonSchema = {};
    if (schema.type) {
        jsonSchema.type = schema.type;
    }
    else if (!schema.oneOf && !schema.anyOf) {
        jsonSchema.type = 'object';
    }
    if (schema.properties) {
        const properties = {};
        for (const [key, value] of Object.entries(schema.properties)) {
            properties[key] = resolveSchema(value, spec, visited);
        }
        jsonSchema.properties = properties;
    }
    if (schema.items) {
        jsonSchema.items = resolveSchema(schema.items, spec, visited);
    }
    if (schema.oneOf) {
        jsonSchema.oneOf = schema.oneOf.map((s) => resolveSchema(s, spec, visited));
    }
    if (schema.anyOf) {
        jsonSchema.anyOf = schema.anyOf.map((s) => resolveSchema(s, spec, visited));
    }
    if (schema.additionalProperties !== undefined) {
        jsonSchema.additionalProperties =
            typeof schema.additionalProperties === 'object'
                ? resolveSchema(schema.additionalProperties, spec, visited)
                : schema.additionalProperties;
    }
    const directCopyFields = [
        'required',
        'enum',
        'description',
        'format',
        'nullable',
        'default',
        'minimum',
        'maximum',
        'exclusiveMinimum',
        'exclusiveMaximum',
        'multipleOf',
        'minLength',
        'maxLength',
        'pattern',
        'minItems',
        'maxItems',
        'uniqueItems',
        'readOnly',
        'writeOnly',
        'example',
        'deprecated',
    ];
    for (const field of directCopyFields) {
        if (schema[field] !== undefined) {
            jsonSchema[field] = schema[field];
        }
    }
    return jsonSchema;
}
/**
 * Extract annotations from OpenAPI extensions
 */
function extractAnnotations(operation, operationKey, annotationOptions) {
    const annotations = {};
    if (annotationOptions?.global) {
        Object.assign(annotations, annotationOptions.global);
    }
    if (annotationOptions?.operations?.[operationKey]) {
        Object.assign(annotations, annotationOptions.operations[operationKey]);
    }
    if (annotationOptions?.fromExtensions) {
        for (const [extensionKey, annotationKey] of Object.entries(annotationOptions.fromExtensions)) {
            if (extensionKey in operation) {
                annotations[annotationKey] = operation[extensionKey];
            }
        }
    }
    else {
        if (operation['x-destructive']) {
            annotations.destructive = operation['x-destructive'];
        }
        if (operation['x-requires-approval']) {
            annotations.requiresApproval = operation['x-requires-approval'];
        }
        if (operation['x-risk-level']) {
            annotations.risk = operation['x-risk-level'];
        }
        if (operation['x-confirm-prompt']) {
            annotations.confirmPrompt = operation['x-confirm-prompt'];
        }
    }
    return annotations;
}
/**
 * Detect authentication from OpenAPI securitySchemes or Swagger securityDefinitions
 */
function detectAuth(spec, authProvider) {
    let schemeName;
    // Try to get scheme from security requirements
    if (spec.security && spec.security.length > 0) {
        const securityReq = spec.security[0];
        if (securityReq) {
            schemeName = Object.keys(securityReq)[0];
        }
    }
    const securitySchemes = isOpenAPI3(spec)
        ? spec.components?.securitySchemes
        : isSwagger2(spec)
            ? spec.securityDefinitions
            : undefined;
    if (!schemeName && securitySchemes) {
        const schemes = Object.keys(securitySchemes);
        if (schemes.length > 0) {
            schemeName = schemes[0];
            log.debug(`No security requirements found, using first securityScheme: ${schemeName}`);
        }
    }
    if (!schemeName || !securitySchemes) {
        return undefined;
    }
    const scheme = securitySchemes[schemeName];
    if (!scheme) {
        return undefined;
    }
    // Get API name for environment variable prefix
    const apiName = spec.info.title.toUpperCase().replace(/[^A-Z0-9]/g, '_');
    switch (scheme.type) {
        case 'http':
            if (scheme.scheme === 'bearer') {
                const authConfig = {
                    scheme: 'bearer',
                    envVar: `${apiName}_TOKEN`,
                };
                log.debug(`Detected Bearer token auth`, { envVar: authConfig.envVar });
                return authConfig;
            }
            else if (scheme.scheme === 'basic') {
                const authConfig = {
                    scheme: 'basic',
                    usernameEnvVar: `${apiName}_USERNAME`,
                    passwordEnvVar: `${apiName}_PASSWORD`,
                };
                log.debug(`Detected Basic auth`, {
                    usernameEnvVar: authConfig.usernameEnvVar,
                    passwordEnvVar: authConfig.passwordEnvVar,
                });
                return authConfig;
            }
            break;
        case 'apiKey': {
            const authConfig = {
                scheme: 'apiKey',
                in: scheme.in === 'query' ? 'query' : 'header',
                name: scheme.name || 'X-API-Key',
                envVar: `${apiName}_API_KEY`,
            };
            return authConfig;
        }
    }
    return undefined;
}
//# sourceMappingURL=openapi-loader.js.map