import yaml from "yaml";
import { safeJsonStringify } from "../../../../util/log-sanitizer.js";
import { getPrefixedLogger } from "../../../../util/logger.js";
import { Paths } from "../../../const.js";
import { AGENT_NEEDS_USER_INPUT, ClarkStateNames, } from "../../../state-machine/clark-fsm.js";
import { sendUserMessageChannel } from "../../../state-machine/helpers/peer.js";
import { renderPath } from "../../../transform/shared.js";
import { ApiExecutor } from "./api-executor.js";
export var ApiValidationStage;
(function (ApiValidationStage) {
    ApiValidationStage["REGISTRATION"] = "registration";
    ApiValidationStage["EXECUTE"] = "execute";
    ApiValidationStage["AUTH"] = "auth";
    ApiValidationStage["RESULTS"] = "results";
    ApiValidationStage["METADATA"] = "metadata";
})(ApiValidationStage || (ApiValidationStage = {}));
export class ApiValidationEscalatedError extends Error {
    issues;
    summary;
    constructor(summary, issues) {
        super(`${summary}\n\nAutomation has requested user input via the UI prompt. Wait for the user's response before proceeding.`);
        this.name = "ApiValidationEscalatedError";
        this.issues = issues;
        this.summary = summary;
    }
}
const DEFAULT_OPTIONS = {
    maxRegistrationAttempts: 5,
    maxExecutionAttempts: 3,
    validateMetadata: false,
};
const initAttempts = () => ({
    [ApiValidationStage.REGISTRATION]: 0,
    [ApiValidationStage.EXECUTE]: 0,
    [ApiValidationStage.AUTH]: 0,
    [ApiValidationStage.RESULTS]: 0,
    [ApiValidationStage.METADATA]: 0,
});
/**
 * Execute the automated API validation pipeline, progressing through execution,
 * authentication, parameter, metadata, and result validation stages.
 */
export const runApiValidationPipeline = async (params) => {
    const { clark, services, identifier } = params;
    const logger = params.logger ?? getPrefixedLogger(`[validateApi-${identifier}]`);
    logger.info("Starting API validation pipeline", identifier);
    const validationKey = makeValidationKey(identifier);
    activeValidations.add(validationKey);
    try {
        return await (async () => {
            const options = {
                ...DEFAULT_OPTIONS,
                ...params.options,
            };
            const attempts = initAttempts();
            const issues = [];
            if (options.validateMetadata) {
                let apiDefinition;
                try {
                    apiDefinition = await loadApiDefinition(identifier, services);
                    logger.info("Loaded API definition for validation", safeJsonStringify({ hasDefinition: apiDefinition !== undefined }, 2));
                }
                catch (error) {
                    logger.error(`Failed to load API definition: ${error instanceof Error ? error.message : String(error)}`);
                    issues.push({
                        stage: ApiValidationStage.EXECUTE,
                        reason: "Unable to load API definition",
                        recoverable: false,
                        attempt: attempts[ApiValidationStage.EXECUTE],
                        error: error instanceof Error ? error.message : safeJsonStringify(error),
                    });
                    return await escalateToUser({
                        clark,
                        services,
                        identifier,
                        issues,
                        attempts,
                        logger,
                    });
                }
                try {
                    await ensureIntegrationMetadataReady({
                        api: apiDefinition,
                        clark,
                        services,
                        attempts,
                        logger,
                        issues,
                    });
                }
                catch (_error) {
                    return await escalateToUser({
                        clark,
                        services,
                        identifier,
                        issues,
                        attempts,
                        logger,
                    });
                }
            }
            const editorClient = clark.context.peer;
            if (!editorClient) {
                throw new Error("No editor client available to execute APIs");
            }
            const executor = new ApiExecutor(editorClient, services.integrationStore);
            let lastExecution;
            const inputs = params.inputs ?? {};
            while (attempts[ApiValidationStage.EXECUTE] < options.maxExecutionAttempts) {
                attempts[ApiValidationStage.EXECUTE] += 1;
                logger.info("Executing API validation attempt", safeJsonStringify({ attempt: attempts[ApiValidationStage.EXECUTE] }, 2));
                const execution = await executor.execute({ type: "fetch", apiName: identifier }, inputs, {
                    includeStepOutputs: !options.omitStepOutputs,
                    timeout: options.timeout,
                });
                if (execution.success) {
                    const hasUsefulResult = hasMeaningfulResult(execution);
                    if (!hasUsefulResult) {
                        attempts[ApiValidationStage.RESULTS] += 1;
                        issues.push({
                            stage: ApiValidationStage.RESULTS,
                            reason: "API executed but returned no usable data",
                            recoverable: false,
                            attempt: attempts[ApiValidationStage.RESULTS],
                            error: "Execution outputs were empty or undefined.",
                        });
                        return {
                            success: false,
                            summary: `Validation failed after ${attempts[ApiValidationStage.EXECUTE]} execution attempt(s).`,
                            issues,
                            attempts,
                            executionResult: execution,
                            escalated: false,
                        };
                    }
                    logger.info("API validation succeeded", safeJsonStringify({ attempts: attempts[ApiValidationStage.EXECUTE] }, 2));
                    return {
                        success: true,
                        summary: `Validation succeeded after ${attempts[ApiValidationStage.EXECUTE]} execution attempt(s).`,
                        issues,
                        attempts,
                        executionResult: execution,
                        escalated: false,
                    };
                }
                lastExecution = execution;
                const errors = execution.errors ?? [];
                const unhandledErrors = errors.filter((error) => !error.handled);
                const primaryError = selectPrimaryError(unhandledErrors);
                const additionalErrors = primaryError
                    ? unhandledErrors.filter((error) => error !== primaryError)
                    : [];
                if (primaryError) {
                    switch (primaryError.type) {
                        case "auth":
                            issues.push({
                                stage: ApiValidationStage.AUTH,
                                reason: "Access error",
                                recoverable: false,
                                attempt: attempts[ApiValidationStage.AUTH],
                                error: primaryError.message,
                                suggestion: primaryError.suggestion,
                            });
                            addAdditionalIssues({
                                errors: additionalErrors,
                                issues,
                                attempts,
                            });
                            return await escalateToUser({
                                clark,
                                services,
                                identifier,
                                issues,
                                attempts,
                                logger,
                            });
                        case "rate_limit": {
                            attempts[ApiValidationStage.EXECUTE] += 1;
                            issues.push({
                                stage: primaryError.type,
                                reason: primaryError.displayMessage ||
                                    primaryError.message ||
                                    "Rate limit encountered",
                                recoverable: attempts[ApiValidationStage.EXECUTE] <
                                    options.maxExecutionAttempts,
                                attempt: attempts[ApiValidationStage.EXECUTE],
                                error: primaryError.bodyDetails || primaryError.message,
                                suggestion: primaryError.suggestion,
                            });
                            addAdditionalIssues({
                                errors: additionalErrors,
                                issues,
                                attempts,
                            });
                            if (attempts[ApiValidationStage.EXECUTE] >=
                                options.maxExecutionAttempts) {
                                return await escalateToUser({
                                    clark,
                                    services,
                                    identifier,
                                    issues,
                                    attempts,
                                    logger,
                                });
                            }
                            await delay(500 * attempts[ApiValidationStage.EXECUTE]);
                            continue;
                        }
                        case "validation":
                        case "binding":
                        case "execution": {
                            const registrationFailure = /API ['"][^'"]+['"] not found/i.test(primaryError.message ?? "");
                            if (registrationFailure) {
                                attempts[ApiValidationStage.REGISTRATION] += 1;
                                logger.warn("API not yet registered in editor; retrying", safeJsonStringify({
                                    registrationAttempt: attempts[ApiValidationStage.REGISTRATION],
                                    executionAttempt: attempts[ApiValidationStage.EXECUTE],
                                    message: primaryError.message,
                                }, 2));
                                if (attempts[ApiValidationStage.REGISTRATION] >=
                                    options.maxRegistrationAttempts) {
                                    // Registration failures are not user-actionable, they indicate a system timing issue
                                    // Log the issue but don't escalate - throw an error for the LLM to see
                                    const detail = primaryError.bodyDetails ??
                                        primaryError.message ??
                                        undefined;
                                    const alreadyLogged = hasIssueWithDetail(issues, ApiValidationStage.REGISTRATION, detail);
                                    issues.push({
                                        stage: ApiValidationStage.REGISTRATION,
                                        reason: "API failed to register in the editor after multiple attempts",
                                        recoverable: false,
                                        attempt: attempts[ApiValidationStage.REGISTRATION],
                                        error: alreadyLogged ? undefined : detail,
                                        suggestion: "This indicates a race condition or system issue. The API was built successfully but did not become available for execution.",
                                    });
                                    addAdditionalIssues({
                                        errors: additionalErrors,
                                        issues,
                                        attempts,
                                    });
                                    throw new Error(`API registration timeout: ${identifier} failed to register after ${attempts[ApiValidationStage.REGISTRATION]} attempts. ` +
                                        `This typically indicates a race condition or system issue, not a user error.`);
                                }
                                // Use exponential backoff for registration retries: 200ms, 400ms, 800ms, 1600ms, 3200ms
                                await delay(200 *
                                    Math.pow(2, attempts[ApiValidationStage.REGISTRATION] - 1));
                                continue;
                            }
                            return {
                                success: false,
                                summary: `Validation failed with a ${primaryError.type} error`,
                                issues,
                                attempts,
                                executionResult: execution,
                                escalated: false,
                            };
                        }
                        default:
                            break;
                    }
                }
            }
            return {
                success: false,
                summary: "Exceeded maximum execution retries",
                issues,
                attempts,
                executionResult: lastExecution,
                escalated: false,
            };
        })();
    }
    finally {
        activeValidations.delete(validationKey);
        await flushPendingEscalations({
            clark,
            services,
            logger,
        });
    }
};
const LOAD_FAILURE_MESSAGE = "Unable to parse the generated API YAML.";
const escalateToUser = async (params) => {
    const { identifier, issues, attempts, logger } = params;
    logger.warn("Escalating API validation failure to the user", safeJsonStringify({ issues }, 2));
    const key = makeValidationKey(identifier);
    pendingEscalations.set(key, {
        identifier,
        issues: issues.map((issue) => ({ ...issue })),
    });
    logger.info("Queued API validation escalation for aggregated handling", safeJsonStringify({
        apiName: identifier,
        pendingEscalations: pendingEscalations.size,
    }));
    const summary = `Validation failed after ${attempts[ApiValidationStage.EXECUTE]} execution attempt(s). User input required.`;
    logger.info("Validation escalation recorded", safeJsonStringify({ summary, issueCount: issues.length }, 2));
    throw new ApiValidationEscalatedError(summary, issues);
};
const getErrorMessageFromStage = (identifier, stage) => {
    switch (stage) {
        case ApiValidationStage.AUTH:
            return `I can't successfully test ${identifier} due to authentication errors. How would you like to proceed?`;
        case ApiValidationStage.EXECUTE:
        case ApiValidationStage.REGISTRATION:
            return `I can't successfully test ${identifier} due to connection errors. How would you like to proceed?`;
        case ApiValidationStage.METADATA:
            return `I can't successfully test ${identifier} due to metadata errors. How would you like to proceed?`;
        case ApiValidationStage.RESULTS:
            return `I can't successfully test ${identifier} due to invalid response errors. How would you like to proceed?`;
        default:
            return `I can't successfully test ${identifier} due to an unknown error. How would you like to proceed?`;
    }
};
const _guessBetterParams = (message, currentInputs) => {
    const guess = { ...currentInputs };
    let mutated = false;
    const requiredParamMatch = message.match(/"([^"]+)" is required/);
    const paramName = requiredParamMatch?.[1];
    if (typeof paramName === "string") {
        if (guess[paramName] === undefined) {
            guess[paramName] = "sample";
            mutated = true;
        }
    }
    const missingBindingMatch = message.match(/ReferenceError: (\w+) is not defined/);
    const variableName = missingBindingMatch?.[1];
    if (typeof variableName === "string") {
        guess[variableName] = "sample";
        mutated = true;
    }
    return mutated ? guess : undefined;
};
const hasMeaningfulResult = (result) => {
    if (!result.outputs) {
        return false;
    }
    return Object.values(result.outputs).some((value) => {
        if (value === null || value === undefined) {
            return false;
        }
        if (Array.isArray(value)) {
            return value.length > 0;
        }
        if (typeof value === "object") {
            return Object.keys(value).length > 0;
        }
        return true;
    });
};
const loadApiDefinition = async (identifier, services) => {
    const filePath = renderPath(Paths.ApiYamls, {
        apiName: identifier,
    });
    const files = await services.appShell.readFiles([filePath]);
    if (!files?.[0]) {
        throw new Error(LOAD_FAILURE_MESSAGE);
    }
    try {
        const parsed = yaml.parse(files[0].content);
        return parsed;
    }
    catch (error) {
        throw new Error(error instanceof Error ? error.message : LOAD_FAILURE_MESSAGE);
    }
};
const delay = (ms) => new Promise((resolve) => {
    setTimeout(resolve, ms);
});
const ensureIntegrationMetadataReady = async ({ api, clark, services, attempts, logger, issues, }) => {
    const editorClient = clark.context.peer;
    if (!editorClient) {
        logger.warn("Skipping metadata fetch because no editor client is available");
        return;
    }
    const integrationIds = extractIntegrationIds(api);
    if (integrationIds.length === 0) {
        return;
    }
    logger.info("Ensuring integration metadata is available", safeJsonStringify({ integrationIds }, 2));
    const metadataResults = await Promise.all(integrationIds.map(async (integrationId) => {
        try {
            const metadata = await services.integrationStore.getMetadata(integrationId, editorClient);
            logger.info("Fetched integration metadata", safeJsonStringify({
                integrationId,
                gronPath: metadata?._gronPath,
            }, 2));
            return { integrationId, metadata };
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            logger.error(`Failed to fetch integration metadata for ${integrationId}: ${message}`);
            return { integrationId, error: message };
        }
    }));
    const failures = metadataResults.filter((result) => result.error !== undefined);
    if (failures.length === 0) {
        return;
    }
    attempts[ApiValidationStage.METADATA] += 1;
    failures.forEach(({ integrationId, error }) => {
        issues.push({
            stage: ApiValidationStage.METADATA,
            reason: `Failed to load metadata for integration ${integrationId}`,
            recoverable: false,
            attempt: attempts[ApiValidationStage.METADATA],
            error: error,
            suggestion: "Confirm the integration is accessible and the credentials are valid. Try reloading metadata manually from the integrations panel.",
        });
    });
    throw new ApiValidationEscalatedError("Integration metadata is required to validate the API.", issues);
};
const extractIntegrationIds = (api) => {
    const ids = new Set();
    for (const block of api.blocks || []) {
        const integration = block?.step?.integration;
        if (integration &&
            integration !== "javascript" &&
            integration !== "python") {
            ids.add(integration);
        }
    }
    return Array.from(ids);
};
const ERROR_PRIORITY = [
    "auth",
    "rate_limit",
    "validation",
    "binding",
    "execution",
    "proto",
    "unknown",
];
const selectPrimaryError = (errors) => {
    for (const type of ERROR_PRIORITY) {
        const match = errors.find((error) => error.type === type);
        if (match) {
            return match;
        }
    }
    return undefined;
};
const addAdditionalIssues = ({ errors, issues, attempts, }) => {
    for (const error of errors) {
        const stage = mapErrorTypeToStage(error.type);
        if (hasIssueWithDetail(issues, stage, error.message)) {
            continue;
        }
        issues.push({
            stage,
            reason: error.displayMessage ||
                error.message ||
                `Additional ${error.type.replace("_", " ")} error encountered`,
            recoverable: false,
            attempt: attempts[stage],
            error: error.bodyDetails || error.message,
            suggestion: error.suggestion,
        });
    }
};
const mapErrorTypeToStage = (type) => {
    switch (type) {
        case "auth":
            return ApiValidationStage.AUTH;
        default:
            return ApiValidationStage.EXECUTE;
    }
};
const hasIssueWithDetail = (issues, stage, detail) => {
    if (!detail) {
        return false;
    }
    return issues.some((issue) => issue.stage === stage && issue.error === detail);
};
const activeValidations = new Set();
const pendingEscalations = new Map();
const makeValidationKey = (identifier) => identifier;
/**
 * Checks if there are any pending escalations.
 */
export const hasPendingEscalations = () => {
    return pendingEscalations.size > 0;
};
/**
 * Flushes pending escalations by sending a multi_choice message to the user.
 * Returns true if an escalation was sent, false otherwise.
 */
export const flushPendingEscalations = async ({ clark, services, logger, force = false, skipStateTransition = false, }) => {
    if (activeValidations.size > 0) {
        return false;
    }
    if (pendingEscalations.size === 0) {
        return false;
    }
    if (!force && clark.state === ClarkStateNames.AwaitingUser) {
        logger.info("Clark already awaiting user input; deferring aggregated escalation", safeJsonStringify({ pendingEscalations: pendingEscalations.size }, 2));
        return false;
    }
    const entries = Array.from(pendingEscalations.values());
    pendingEscalations.clear();
    const identifier = entries[0]?.identifier;
    const primaryIssue = entries[0]?.issues[0];
    if (!primaryIssue || !identifier) {
        return false;
    }
    const question = getErrorMessageFromStage(identifier, primaryIssue.stage);
    const description = `${primaryIssue.reason}. ${primaryIssue.suggestion}`;
    const error = primaryIssue.error;
    const choices = [
        {
            text: "Ignore and continue",
            subText: "Will result in runtime errors when running the API",
        },
        {
            text: "Use mock data",
            subText: "Allows you to test the UI but will require an API update later to connect to real data",
        },
        {
            text: "Try again",
            subText: "Try the API validation again to see if the issue persists",
        },
    ];
    try {
        const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
        await sendUserMessage({
            role: "assistant",
            type: "multi_choice",
            content: question,
            choices,
            selectionType: "single",
            questionType: "api_integration_error",
            details: error,
            subtitle: description,
        });
    }
    catch (error) {
        logger.error(`Aggregated escalation prompt threw an error: ${error instanceof Error ? error.message : String(error)}`);
        return false;
    }
    if (!skipStateTransition) {
        void clark.send({
            type: AGENT_NEEDS_USER_INPUT,
            advice: "",
        });
    }
    return true;
};
//# sourceMappingURL=api-validation-orchestrator.js.map