import { zodToJsonSchema } from "zod-to-json-schema";
import Ajv from "ajv";
import { getAgentProviderDefinition } from "./provider-manifest.js";
export class StructuredAgentResponseError extends Error {
    constructor(message, options) {
        super(message);
        this.name = "StructuredAgentResponseError";
        this.lastResponse = options.lastResponse;
        this.validationErrors = options.validationErrors;
    }
}
export class StructuredAgentFallbackError extends Error {
    constructor(attempts) {
        const summary = attempts
            .map((attempt) => {
            const modelSuffix = attempt.model ? ` (${attempt.model})` : "";
            if (!attempt.available) {
                return `${attempt.provider}${modelSuffix}: unavailable${attempt.error ? ` (${attempt.error})` : ""}`;
            }
            return `${attempt.provider}${modelSuffix}: failed${attempt.error ? ` (${attempt.error})` : ""}`;
        })
            .join("; ");
        super(summary.length > 0
            ? `Structured generation failed for all providers: ${summary}`
            : "Structured generation failed for all providers");
        this.name = "StructuredAgentFallbackError";
        this.attempts = attempts;
    }
}
export const DEFAULT_STRUCTURED_GENERATION_PROVIDERS = [
    { provider: "claude", model: "haiku" },
    { provider: "codex", model: "gpt-5.4-mini", thinkingOptionId: "low" },
];
function isZodSchema(value) {
    return typeof value?.safeParse === "function";
}
function buildZodValidator(schema, schemaName) {
    const zodToJsonSchemaAny = zodToJsonSchema;
    const jsonSchema = zodToJsonSchemaAny(schema, schemaName);
    return {
        jsonSchema,
        validate: (value) => {
            const result = schema.safeParse(value);
            if (result.success) {
                return { ok: true, value: result.data };
            }
            const errors = result.error.issues.map((issue) => {
                const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
                return `${path}: ${issue.message}`;
            });
            return { ok: false, errors };
        },
    };
}
function buildJsonSchemaValidator(schema) {
    const AjvConstructor = Ajv;
    const ajv = new AjvConstructor({ allErrors: true, strict: false });
    const validate = ajv.compile(schema);
    return {
        jsonSchema: schema,
        validate: (value) => {
            const ok = validate(value);
            if (ok) {
                return { ok: true, value: value };
            }
            const errors = (validate.errors ?? []).map((error) => {
                const path = error.instancePath && error.instancePath.length > 0 ? error.instancePath : "(root)";
                const message = error.message ?? "is invalid";
                return `${path}: ${message}`;
            });
            return { ok: false, errors };
        },
    };
}
function buildValidator(schema, schemaName) {
    if (isZodSchema(schema)) {
        return buildZodValidator(schema, schemaName);
    }
    return buildJsonSchemaValidator(schema);
}
function buildBasePrompt(prompt, jsonSchema) {
    const schemaText = JSON.stringify(jsonSchema, null, 2);
    return [
        prompt.trim(),
        "",
        "You must respond with JSON only that matches this JSON Schema:",
        schemaText,
    ].join("\n");
}
function buildRetryPrompt(basePrompt, errors) {
    const formattedErrors = errors.map((error) => `- ${error}`).join("\n");
    return [
        basePrompt,
        "",
        "Previous response was invalid with validation errors:",
        formattedErrors.length > 0 ? formattedErrors : "- Unknown validation error",
        "",
        "Respond again with JSON only that matches the schema.",
    ].join("\n");
}
function extractJsonFromMarkdown(text) {
    const fencedMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
    if (fencedMatch) {
        return fencedMatch[1].trim();
    }
    const extracted = extractFirstJsonSnippet(text);
    if (extracted) {
        return extracted;
    }
    return text.trim();
}
function extractFirstJsonSnippet(text) {
    const source = text.trim();
    if (!source) {
        return null;
    }
    // Try to find the first valid JSON object/array within a larger response.
    // This is intentionally provider-agnostic and improves resilience when models
    // add extra prose before/after the JSON.
    const startIndexes = [];
    for (let i = 0; i < source.length; i += 1) {
        const ch = source[i];
        if (ch === "{" || ch === "[") {
            startIndexes.push(i);
        }
    }
    for (const start of startIndexes) {
        const open = source[start];
        const close = open === "{" ? "}" : "]";
        let depth = 0;
        let inString = false;
        let escaped = false;
        for (let i = start; i < source.length; i += 1) {
            const ch = source[i];
            if (inString) {
                if (escaped) {
                    escaped = false;
                    continue;
                }
                if (ch === "\\") {
                    escaped = true;
                    continue;
                }
                if (ch === '"') {
                    inString = false;
                }
                continue;
            }
            if (ch === '"') {
                inString = true;
                continue;
            }
            if (ch === open) {
                depth += 1;
                continue;
            }
            if (ch === close) {
                depth -= 1;
                if (depth === 0) {
                    const candidate = source.slice(start, i + 1).trim();
                    try {
                        JSON.parse(candidate);
                        return candidate;
                    }
                    catch {
                        // keep scanning; the snippet might not be JSON (e.g. braces in prose)
                    }
                }
            }
        }
    }
    return null;
}
export async function getStructuredAgentResponse(options) {
    const { caller, prompt, schema, maxRetries = 2, schemaName = "Response" } = options;
    const validator = buildValidator(schema, schemaName);
    const basePrompt = buildBasePrompt(prompt, validator.jsonSchema);
    let attemptPrompt = basePrompt;
    let lastResponse = "";
    let lastErrors = [];
    for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
        const response = await caller(attemptPrompt);
        lastResponse = response;
        const jsonText = extractJsonFromMarkdown(response);
        let parsed;
        try {
            parsed = JSON.parse(jsonText);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            lastErrors = [`Invalid JSON: ${message}`];
            if (attempt === maxRetries) {
                break;
            }
            attemptPrompt = buildRetryPrompt(basePrompt, lastErrors);
            continue;
        }
        const validation = validator.validate(parsed);
        if (validation.ok) {
            return validation.value;
        }
        lastErrors = validation.errors;
        if (attempt === maxRetries) {
            break;
        }
        attemptPrompt = buildRetryPrompt(basePrompt, lastErrors);
    }
    throw new StructuredAgentResponseError("Agent response did not match the required JSON schema", {
        lastResponse,
        validationErrors: lastErrors,
    });
}
export async function generateStructuredAgentResponse(options) {
    const { manager, agentConfig, agentId, prompt, schema, maxRetries, schemaName } = options;
    const modeId = agentConfig.modeId ??
        getAgentProviderDefinition(agentConfig.provider).defaultModeId ??
        undefined;
    const agent = await manager.createAgent({ ...agentConfig, modeId }, agentId);
    try {
        const caller = async (nextPrompt) => {
            const result = await manager.runAgent(agent.id, nextPrompt);
            if (typeof result.finalText === "string" && result.finalText.length > 0) {
                return result.finalText;
            }
            // Fallback for providers that may not populate finalText consistently.
            const lastAssistant = result.timeline
                .filter((item) => item.type === "assistant_message")
                .at(-1);
            return lastAssistant?.text ?? "";
        };
        return await getStructuredAgentResponse({
            caller,
            prompt,
            schema,
            maxRetries,
            schemaName,
        });
    }
    finally {
        try {
            await manager.closeAgent(agent.id);
        }
        catch {
            // ignore cleanup errors
        }
    }
}
function errorMessage(error) {
    if (error instanceof Error) {
        return error.message;
    }
    return String(error);
}
export async function generateStructuredAgentResponseWithFallback(options) {
    const { manager, cwd, prompt, schema, providers, agentConfigOverrides, maxRetries, schemaName, runner, } = options;
    if (providers.length === 0) {
        throw new StructuredAgentFallbackError([]);
    }
    const runStructured = runner ??
        ((input) => generateStructuredAgentResponse(input));
    const availability = await manager.listProviderAvailability();
    const availabilityByProvider = new Map(availability.map((entry) => [entry.provider, entry]));
    const attempts = [];
    for (const candidate of providers) {
        const availabilityEntry = availabilityByProvider.get(candidate.provider);
        if (availabilityEntry && !availabilityEntry.available) {
            attempts.push({
                provider: candidate.provider,
                model: candidate.model ?? null,
                available: false,
                error: availabilityEntry.error ?? null,
            });
            continue;
        }
        try {
            const result = await runStructured({
                manager,
                prompt,
                schema,
                maxRetries,
                schemaName,
                agentConfig: {
                    ...agentConfigOverrides,
                    provider: candidate.provider,
                    cwd,
                    ...(candidate.model ? { model: candidate.model } : {}),
                    ...(candidate.thinkingOptionId ? { thinkingOptionId: candidate.thinkingOptionId } : {}),
                },
            });
            return result;
        }
        catch (error) {
            attempts.push({
                provider: candidate.provider,
                model: candidate.model ?? null,
                available: true,
                error: errorMessage(error),
            });
        }
    }
    throw new StructuredAgentFallbackError(attempts);
}
//# sourceMappingURL=agent-response-loop.js.map