import { z } from "zod";
import { StateManager, hasSessionData, getSessionData, } from "../../utils/AnalysisStateManager.js";
import { logger } from "../../utils/logger.js";
import { buildRecommendationPrompt } from "./test-recommendation-prompt.js";
import { ScenarioSource, AnalysisScope } from "../../types/RepositoryAnalysis.js";
import { SCENARIO_CATEGORIES } from "../../types/TestRecommendation.js";
import { inferExpectedStatus } from "../../utils/httpDefaults.js";
export function mergeEnrichedScenarios(serverScenarios, raw) {
    const rejectionNotes = [];
    let parsed;
    try {
        const result = JSON.parse(raw);
        if (!Array.isArray(result)) {
            return { scenarios: serverScenarios, rejectionNotes: ["enrichedScenarios: expected a JSON array, got " + typeof result] };
        }
        parsed = result;
    }
    catch {
        logger.warning("enrichedScenarios: invalid JSON — using server-side scenarios only");
        return { scenarios: serverScenarios, rejectionNotes: ["enrichedScenarios: invalid JSON — all scenarios skipped"] };
    }
    const agentScenarios = [];
    for (const s of parsed) {
        const name = s?.scenarioName ? String(s.scenarioName) : null;
        const label = name ? `"${name}"` : "(unnamed)";
        if (!name) {
            rejectionNotes.push(`rejected ${label}: missing scenarioName`);
            continue;
        }
        if (!Array.isArray(s?.steps) || s.steps.length === 0) {
            rejectionNotes.push(`rejected ${label}: missing or empty steps array`);
            continue;
        }
        if (!s?.category) {
            rejectionNotes.push(`rejected ${label}: missing category`);
            continue;
        }
        if (!SCENARIO_CATEGORIES.includes(s.category)) {
            rejectionNotes.push(`rejected ${label}: unknown category "${s.category}" — valid: ${SCENARIO_CATEGORIES.join(", ")}`);
            continue;
        }
        agentScenarios.push({
            scenarioName: name,
            description: s.description ?? "",
            category: s.category,
            priority: s.priority ?? "high",
            bugCatchingTarget: s.bugCatchingTarget,
            testType: s.testType,
            steps: s.steps.map((st, idx) => ({
                order: st.order ?? idx + 1,
                method: String(st.method ?? "GET").toUpperCase(),
                path: String(st.path ?? "/"),
                description: st.description ?? `${st.method} ${st.path}`,
                interactionType: st.interactionType ?? "success",
                requestBody: st.requestBody,
                queryParams: st.queryParams,
                responseBody: st.responseBody,
                // Default status code by method if omitted to avoid `statusCode: undefined` in tool calls
                expectedStatusCode: st.expectedStatusCode ?? inferExpectedStatus(String(st.method ?? "GET")),
                expectedResponseFields: st.expectedResponseFields,
                bodyMustInclude: st.bodyMustInclude,
                chainsFrom: st.chainsFrom,
            })),
            chainingKeys: s.chainingKeys ?? [],
            requiresAuth: s.requiresAuth ?? true,
            estimatedComplexity: s.estimatedComplexity ?? "moderate",
            source: ScenarioSource.AgentEnriched,
        });
    }
    if (agentScenarios.length === 0) {
        return { scenarios: serverScenarios, rejectionNotes };
    }
    const merged = new Map(serverScenarios.map(s => [s.scenarioName, s]));
    for (const s of agentScenarios) {
        merged.set(s.scenarioName, s);
    }
    logger.info("Merged agent-enriched scenarios", {
        server: serverScenarios.length,
        agent: agentScenarios.length,
        total: merged.size,
        rejected: rejectionNotes.length,
    });
    return { scenarios: Array.from(merged.values()), rejectionNotes };
}
export function registerRecommendTestsPrompt(server) {
    server.registerPrompt("skyramp_recommend_tests", {
        description: "Given the repository analysis in stateFile, produce ranked test recommendations split into " +
            "GENERATE (call generation tools immediately) and ADDITIONAL (deferred, describe only).\n\n" +
            "**Output contract:** Every GENERATE integration test targeting a business rule or formula " +
            "MUST include a non-empty bugCatchingTarget. Parameters for generation tools must derive " +
            "from the repository analysis, enrichedScenarios, or source code you read — no invented " +
            "field names, no guessed URLs.\n\n" +
            "**Ranking:** Prioritize business_rule and security_boundary over crud. Within a category, " +
            "prefer tests that catch specific formulas, constraints, or state transitions over generic " +
            "happy-path coverage. Provide a stateFile path from skyramp_analyze_changes.",
        argsSchema: {
            stateFile: z
                .string()
                .describe("State file path returned by skyramp_analyze_changes"),
            topN: z
                .number()
                .int()
                .positive()
                .default(10)
                .optional()
                .describe("Maximum number of ranked recommendations to return (default: 10)"),
            enrichedScenarios: z
                .string()
                .optional()
                .refine((val) => {
                if (val === undefined || val === "")
                    return true;
                try {
                    const parsed = JSON.parse(val);
                    return Array.isArray(parsed);
                }
                catch {
                    return false;
                }
            }, { message: "enrichedScenarios must be a valid JSON array string (e.g. '[{\"scenarioName\":\"...\"}]')" })
                .describe("JSON array of agent-drafted scenarios (DraftedScenario[]). Each must have " +
                "scenarioName, category, and steps (array with method, path, order). " +
                "Agent scenarios override server-side ones by scenarioName and are prioritized in ranking."),
        },
    }, async (args) => {
        const stateFile = args.stateFile;
        if (!stateFile) {
            throw new Error("stateFile is required");
        }
        // Try in-memory session store first (fullAnalysis no longer on disk).
        // Extract sessionId from the disk state, then look up the full analysis
        // from process memory — same pattern as analysisResources.ts.
        const mgr = StateManager.fromStatePath(stateFile);
        if (!mgr.exists()) {
            throw new Error(`State file "${stateFile}" not found. Run skyramp_analyze_changes first.`);
        }
        const fullState = await mgr.readFullState();
        const state = fullState ?? null;
        if (!state?.repositoryAnalysis) {
            throw new Error(`State file "${stateFile}" has no analysis data. Re-run skyramp_analyze_changes.`);
        }
        const { sessionId, wsAuthHeader, wsAuthType, wsAuthScheme } = state.repositoryAnalysis;
        const repositoryPath = fullState?.metadata?.repositoryPath ?? "";
        // Resolve fullAnalysis: memory first, disk fallback for backward compat
        let fullAnalysis;
        if (sessionId && hasSessionData(sessionId)) {
            const memData = getSessionData(sessionId);
            if (memData?.analysis) {
                fullAnalysis = memData.analysis;
                logger.debug("Loaded fullAnalysis from process memory", { sessionId });
            }
        }
        if (!fullAnalysis) {
            if (sessionId) {
                logger.warning(`Session not found in memory (sessionId=${sessionId}) — server may have restarted; falling back to state file`);
            }
            fullAnalysis = state.repositoryAnalysis.fullAnalysis;
        }
        if (!fullAnalysis) {
            throw new Error(`Analysis data for session not found in memory or on disk. Re-run skyramp_analyze_changes.`);
        }
        // Hydrate testLocations from the disk-persisted field when fullAnalysis came from disk
        // (after a server restart, fullAnalysis is loaded from state.repositoryAnalysis.fullAnalysis
        // but testLocations was persisted separately under state.repositoryAnalysis.testLocations)
        if (fullAnalysis.existingTests &&
            !fullAnalysis.existingTests.testLocations &&
            state.repositoryAnalysis.testLocations) {
            fullAnalysis = {
                ...fullAnalysis,
                existingTests: {
                    ...fullAnalysis.existingTests,
                    testLocations: state.repositoryAnalysis.testLocations,
                },
            };
            logger.debug("Hydrated existingTests.testLocations from disk-persisted state", { sessionId });
        }
        // Normalize legacy state files: before AnalysisScope enum normalization, state stored
        // the user-facing param value "branch_diff". Map it explicitly so diff-mode detection
        // works correctly on state created before this deployment (2-hour TTL window).
        const rawScope = state.analysisScope;
        const analysisScope = rawScope === "branch_diff" || rawScope === AnalysisScope.CurrentBranchDiff
            ? AnalysisScope.CurrentBranchDiff
            : AnalysisScope.FullRepo;
        const effectiveTopN = args.topN;
        const enrichedRaw = args.enrichedScenarios;
        let mergedAnalysis = fullAnalysis;
        let rejectionWarning = "";
        if (enrichedRaw) {
            const { scenarios, rejectionNotes } = mergeEnrichedScenarios(fullAnalysis.businessContext.draftedScenarios ?? [], enrichedRaw);
            mergedAnalysis = {
                ...fullAnalysis,
                businessContext: {
                    ...fullAnalysis.businessContext,
                    draftedScenarios: scenarios,
                },
            };
            if (rejectionNotes.length > 0) {
                rejectionWarning = `\n⚠️ enrichedScenarios — ${rejectionNotes.length} scenario(s) rejected and not used in recommendations:\n${rejectionNotes.map(n => `  - ${n}`).join("\n")}\nFix and re-call skyramp_recommend_tests to incorporate corrections.\n`;
            }
        }
        const prompt = buildRecommendationPrompt(mergedAnalysis, analysisScope, effectiveTopN, undefined, wsAuthHeader, wsAuthType, wsAuthScheme, undefined, sessionId);
        logger.info("Serving recommendation prompt via MCP Prompt", {
            stateFile,
            analysisScope,
        });
        const resourceLinks = sessionId
            ? `\nAvailable MCP Resources:\n- skyramp://analysis/${sessionId}/summary\n- skyramp://analysis/${sessionId}/endpoints\n- skyramp://analysis/${sessionId}/scenarios\n- skyramp://analysis/${sessionId}/diff\n`
            : "";
        return {
            messages: [
                {
                    role: "user",
                    content: {
                        type: "text",
                        text: `State file: ${stateFile}\nRepository: ${repositoryPath}\nScope: ${analysisScope}\n${resourceLinks}${rejectionWarning}\n${prompt}`,
                    },
                },
            ],
        };
    });
}
