import { z } from "zod";
import { logger } from "../../utils/logger.js";
import { StateManager, } from "../../utils/AnalysisStateManager.js";
import { TestSource, DriftAction, RecommendationPriority, EstimatedWork } from "../../types/TestAnalysis.js";
import { TestType } from "../../types/TestTypes.js";
import * as fs from "fs";
import * as path from "path";
import { AnalyticsService } from "../../services/AnalyticsService.js";
import { toolError } from "../../utils/utils.js";
/**
 * Compute a suggested new filename when an endpoint is renamed.
 */
export function computeRenamedTestFile(testFile, renames) {
    const basename = path.basename(testFile);
    let newBasename = basename;
    for (const rename of renames) {
        const oldSegments = rename.oldPath.split("/").filter((s) => s.length > 0);
        const newSegments = rename.newPath.split("/").filter((s) => s.length > 0);
        if (oldSegments.length !== newSegments.length)
            continue;
        const paramPattern = /^\{[^}]+\}$/;
        for (let i = 0; i < oldSegments.length; i++) {
            if (paramPattern.test(oldSegments[i]))
                continue;
            if (oldSegments[i] !== newSegments[i]) {
                const oldName = oldSegments[i].toLowerCase();
                const newName = newSegments[i].toLowerCase();
                if (newBasename.toLowerCase().includes(oldName)) {
                    const idx = newBasename.toLowerCase().indexOf(oldName);
                    newBasename =
                        newBasename.substring(0, idx) +
                            newName +
                            newBasename.substring(idx + oldName.length);
                }
            }
        }
    }
    if (newBasename === basename)
        return null;
    const newFilePath = path.join(path.dirname(testFile), newBasename);
    if (fs.existsSync(newFilePath)) {
        logger.info(`Skipping file rename suggestion: ${newFilePath} already exists`);
        return null;
    }
    return newFilePath;
}
/**
 * Select test types to generate based on HTTP method.
 */
function selectTestTypesForEndpoint(method) {
    switch (method.toUpperCase()) {
        case "POST":
        case "PUT":
        case "PATCH":
            return [TestType.INTEGRATION, TestType.CONTRACT];
        case "DELETE":
            return [TestType.INTEGRATION, TestType.SMOKE];
        case "GET":
        default:
            return [TestType.CONTRACT, TestType.SMOKE];
    }
}
const recommendationSchema = z.object({
    testFile: z
        .string()
        .refine((p) => path.isAbsolute(p), { message: "testFile must be an absolute path" })
        .describe("Absolute path to the test file — use the path as reported by skyramp_analyze_changes or skyramp_analyze_test_health"),
    action: z
        .nativeEnum(DriftAction)
        .describe("Drift action assigned by the LLM health assessment"),
    priority: z
        .nativeEnum(RecommendationPriority)
        .optional()
        .describe("Update priority"),
    rationale: z
        .string()
        .optional()
        .describe("1-2 sentence explanation of why this action is needed"),
    estimatedWork: z
        .nativeEnum(EstimatedWork)
        .optional()
        .describe("Estimated effort to apply the update"),
    updateInstructions: z
        .string()
        .optional()
        .describe("Free-form summary of what this test must change — written for the downstream LLM that will edit the file. " +
        "Specificity prevents incomplete or mismatched edits. Include diff-specific details: " +
        "new response fields to assert, constraint details (types, ranges, defaults), " +
        "auth changes, new request params, removed fields, or other drift-related changes. " +
        "Example: 'Added stock_count: int (ge=0, default=0) to ProductBase. " +
        "Test hits GET /products — assert stock_count is present and non-negative.'"),
    renamedEndpoints: z
        .array(z.object({
        oldPath: z.string().describe("Previous endpoint path"),
        newPath: z.string().describe("New endpoint path after rename"),
        method: z.string().describe("HTTP method, e.g. GET"),
    }))
        .optional()
        .describe("Renamed endpoints — supply this array when action is UPDATE and the endpoint path has changed. Omit if action is not UPDATE."),
});
const actionsSchema = {
    stateFile: z
        .string()
        .refine((p) => path.isAbsolute(p), { message: "stateFile must be an absolute path" })
        .describe("Path to state file from skyramp_analyze_changes"),
    recommendations: z
        .array(recommendationSchema)
        .optional()
        .describe("LLM drift assessment — one entry per test assessed. Required for UPDATE instructions to be emitted; omitting results in no maintenance actions."),
};
const TOOL_NAME = "skyramp_actions";
export function registerActionsTool(server) {
    server.registerTool(TOOL_NAME, {
        annotations: {
            destructiveHint: true,
            readOnlyHint: false,
            idempotentHint: false,
            openWorldHint: true,
        },
        description: `Execute test maintenance and generation actions — final step of the unified Test Health Analysis Flow.

**PREREQUISITE:** Call \`skyramp_analyze_changes\` (produces the stateFile), then \`skyramp_analyze_test_health\` (runs the drift assessment). This tool reads the stateFile from \`skyramp_analyze_changes\`.

Call this tool after completing the drift assessment. It executes maintenance actions automatically from the stateFile — no user confirmation required.

**EXECUTING ACTIONS:**
- UPDATE: Tests with drift — emits targeted per-file edit instructions driven by updateInstructions and renamedEndpoints
- REGENERATE: Emits file-level summary; follow up by calling the appropriate generation tool (e.g. skyramp_integration_test_generation) with the same filename to overwrite
- VERIFY: Emits file-level summary for human review — no automated edits
- ADD: Auto-generates tests for new endpoints via LLM instructions

**OUTPUT:**
Comprehensive report with executed actions, summary, and instructions for ADD recommendations
`,
        inputSchema: actionsSchema,
    }, async (args) => {
        let errorResult;
        try {
            logger.info("Performing Actions (unified test-management)");
            // Load UnifiedAnalysisState from state file
            const stateManager = StateManager.fromStatePath(args.stateFile);
            const stateData = await stateManager.readData();
            const fullState = await stateManager.readFullState();
            const repositoryPath = fullState?.metadata.repositoryPath || "";
            if (!stateData) {
                errorResult = toolError(`State file is empty or invalid: ${args.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
                return errorResult;
            }
            // External tests must not be candidates for UPDATE/REGENERATE/DELETE actions.
            // Default source to Skyramp for backwards compat with state files created before the source field existed.
            const testAnalysisResults = (stateData.existingTests || []).filter((t) => (t.source ?? TestSource.Skyramp) !== TestSource.External);
            const newEndpoints = stateData.newEndpoints || [];
            // Resolve repo root for path normalization and security checks.
            const repoRoot = repositoryPath ? path.resolve(repositoryPath) : "";
            // Set of non-external (Skyramp-generated) test file paths — the only files
            // that may receive UPDATE/REGENERATE/DELETE actions. Using the allowlist rather
            // than a blocklist catches both external tests AND hallucinated paths the LLM
            // may supply that are not present in the scanned catalog at all.
            const skyrampTestFiles = new Set(testAnalysisResults.map((t) => t.testFile));
            // ── Build recommendations from LLM-supplied drift assessment ──
            // The LLM performs the drift assessment in context after skyramp_analyze_test_health
            // and passes results here directly — analyzeTestHealthTool never writes assessment
            // data back to the state file.
            const recommendations = [];
            (args.recommendations ?? []).forEach((rec) => {
                // Schema requires absolute paths; resolve any relative paths defensively
                // against repoRoot in case the LLM sends a relative path despite the schema.
                const resolvedFile = path.isAbsolute(rec.testFile)
                    ? rec.testFile
                    : repoRoot
                        ? path.resolve(repoRoot, rec.testFile)
                        : rec.testFile;
                // Reject files outside the repo root (path-traversal guard).
                if (repoRoot && !resolvedFile.startsWith(repoRoot + path.sep) && resolvedFile !== repoRoot) {
                    logger.warning(`Skipping recommendation for path outside repo root: ${rec.testFile}`);
                    return;
                }
                // Guard: only Skyramp-generated tests may receive UPDATE/REGENERATE/DELETE.
                // Using an allowlist (skyrampTestFiles) rather than a blocklist catches both
                // external tests and hallucinated paths the LLM may supply that are not in
                // the scanned catalog. IGNORE/VERIFY are informational and pass through.
                const isActionable = [DriftAction.Update, DriftAction.Regenerate, DriftAction.Delete].includes(rec.action);
                if (isActionable && !skyrampTestFiles.has(resolvedFile) && !skyrampTestFiles.has(rec.testFile)) {
                    logger.warning(`Skipping ${rec.action} for non-Skyramp or unknown test: ${rec.testFile}`);
                    return;
                }
                recommendations.push({
                    testFile: resolvedFile,
                    action: rec.action,
                    priority: rec.priority ?? RecommendationPriority.Medium,
                    rationale: rec.rationale ?? "",
                    estimatedWork: rec.estimatedWork ?? EstimatedWork.Small,
                    updateInstructions: rec.updateInstructions ?? "",
                    renamedEndpoints: rec.renamedEndpoints ?? [],
                });
            });
            // ── Process UPDATE recommendations ──
            // Deduplicate by testFile — keep the highest-priority entry when the LLM
            // repeats a file. Priority order: high > medium > low.
            const priorityRank = {
                [RecommendationPriority.High]: 2,
                [RecommendationPriority.Medium]: 1,
                [RecommendationPriority.Low]: 0,
            };
            const updateByFile = new Map();
            for (const rec of recommendations) {
                if (rec.action !== DriftAction.Update)
                    continue;
                const existing = updateByFile.get(rec.testFile);
                if (!existing || priorityRank[rec.priority] > priorityRank[existing.priority]) {
                    updateByFile.set(rec.testFile, rec);
                }
            }
            const updateRecommendations = Array.from(updateByFile.values());
            const fileInstructions = [];
            const testFilesToUpdate = [];
            const testFileContentMap = new Map();
            for (const rec of updateRecommendations) {
                if (!rec.testFile) {
                    logger.warning("Recommendation missing testFile", rec);
                    continue;
                }
                testFilesToUpdate.push(rec.testFile);
                const testData = testAnalysisResults.find((t) => t.testFile === rec.testFile);
                const driftData = testData?.drift;
                const driftChanges = driftData?.changes || [];
                let testFileContent = "";
                try {
                    testFileContent = fs.readFileSync(rec.testFile, "utf-8");
                    testFileContentMap.set(rec.testFile, testFileContent);
                }
                catch (error) {
                    logger.error(`Failed to read test file ${rec.testFile}: ${error.message}`);
                    continue;
                }
                const renames = rec.renamedEndpoints || [];
                const isRenameUpdate = renames.length > 0;
                let instruction = `\n### ${rec.testFile}\n\n`;
                instruction += `**Priority:** ${rec.priority} | `;
                instruction += `**Estimated Effort:** ${rec.estimatedWork || EstimatedWork.Small}\n\n`;
                instruction += `**Why Update Needed:** ${rec.rationale}\n\n`;
                if (isRenameUpdate) {
                    instruction += `**Endpoint Rename Detected — Path Substitution Required:**\n\n`;
                    instruction += `| Old Path | New Path | Method |\n`;
                    instruction += `|----------|----------|--------|\n`;
                    for (const rename of renames) {
                        instruction += `| \`${rename.oldPath}\` | \`${rename.newPath}\` | ${rename.method} |\n`;
                    }
                    instruction += `\n`;
                    instruction += `**Action:** Find-and-replace all occurrences of the old path with the new path in this test file. `;
                    instruction += `Do NOT change any test logic, assertions, or structure — only update the URL paths.\n\n`;
                    const suggestedNewFile = computeRenamedTestFile(rec.testFile, renames);
                    if (suggestedNewFile) {
                        instruction += `**File Rename:** After updating the paths, rename this file:\n`;
                        instruction += `- From: \`${path.basename(rec.testFile)}\`\n`;
                        instruction += `- To: \`${path.basename(suggestedNewFile)}\`\n\n`;
                        rec._suggestedNewFile = suggestedNewFile;
                    }
                }
                const recUpdateInstructions = rec.updateInstructions ?? "";
                if (recUpdateInstructions) {
                    instruction += `**What to change:**\n\n${recUpdateInstructions}\n\n`;
                    instruction += `Match the assertion style already used in the file. `;
                    instruction += `Preserve all existing test logic — only add or adjust what is described above.\n\n`;
                }
                else if (!isRenameUpdate) {
                    instruction += `**Action:** Update this test file per the rationale above. `;
                    instruction += `Match the assertion style already used in the file. `;
                    instruction += `Preserve all existing test logic — only add or adjust the minimum required assertions.\n\n`;
                }
                if (driftData) {
                    instruction += `**Analysis:**\n`;
                    instruction += `- Changes Detected: ${driftData.changes?.length || 0}\n`;
                    instruction += `- Affected Files: ${driftData.affectedFiles.files?.length || 0}\n\n`;
                }
                if (driftChanges.length > 0) {
                    instruction += `**Changes Detected:**\n`;
                    driftChanges.forEach((change) => {
                        instruction += `**${change.type}** (Severity: ${change.severity}): ${change.description}\n`;
                        if (change.details) {
                            instruction += `   └─ ${change.details}\n`;
                        }
                        if (change.file) {
                            instruction += `   └─ In: \`${change.file}\`\n`;
                        }
                    });
                    instruction += `\n`;
                }
                // File content is provided in LLM_INSTRUCTIONS.update_context.current_content — omit here to avoid duplication.
                fileInstructions.push(instruction);
            }
            // ── Build ADD section for new endpoints ──
            const wsBaseUrl = stateData.repositoryAnalysis?.wsBaseUrl || "";
            const wsSchemaPath = stateData.repositoryAnalysis?.wsSchemaPath || "";
            const primaryLanguage = stateData.repositoryAnalysis?.projectMeta?.primaryLanguage ||
                "python";
            const primaryFramework = stateData.repositoryAnalysis?.projectMeta?.primaryFramework ||
                "pytest";
            // Determine output directory from workspace config or repo path
            const outputDir = repositoryPath
                ? path.join(repositoryPath, "tests", "skyramp")
                : "./tests/skyramp";
            const addSummaryLines = [];
            const llmToolCalls = [];
            for (const ep of newEndpoints) {
                const testTypes = selectTestTypesForEndpoint(ep.method);
                const endpointURL = wsBaseUrl
                    ? wsBaseUrl.replace(/\/$/, "") + ep.path
                    : ep.path;
                addSummaryLines.push(`- ${ep.method} ${ep.path} → ${testTypes.join(", ")} tests`);
                for (const testType of testTypes) {
                    let toolName = "";
                    switch (testType) {
                        case TestType.CONTRACT:
                            toolName = "skyramp_contract_test_generation";
                            break;
                        case TestType.INTEGRATION:
                            toolName = "skyramp_integration_test_generation";
                            break;
                        case TestType.SMOKE:
                            toolName = "skyramp_smoke_test_generation";
                            break;
                        default:
                            toolName = "skyramp_contract_test_generation";
                    }
                    llmToolCalls.push({
                        tool: toolName,
                        params: {
                            endpointURL,
                            method: ep.method,
                            language: primaryLanguage,
                            framework: primaryFramework,
                            outputDir,
                            ...(wsSchemaPath ? { apiSchema: wsSchemaPath } : {}),
                        },
                        endpoint: `${ep.method} ${ep.path}`,
                        testType: testType,
                    });
                }
            }
            // ── Build response text ──
            let responseText = `# Test Actions Report\n\n`;
            if (updateRecommendations.length > 0) {
                responseText += `## Tests Requiring Updates (${updateRecommendations.length})\n\n`;
                testFilesToUpdate.forEach((file, idx) => {
                    responseText += `${idx + 1}. \`${file}\`\n`;
                });
                responseText += `\n---\n`;
                responseText += fileInstructions.join("\n---\n");
            }
            if (newEndpoints.length > 0) {
                responseText += `\n## New Endpoint Tests to Generate (${newEndpoints.length} endpoints)\n\n`;
                addSummaryLines.forEach((line) => {
                    responseText += `${line}\n`;
                });
                responseText += `\nThe following tests will be generated automatically.\n`;
            }
            if (updateRecommendations.length === 0 && newEndpoints.length === 0) {
                const otherRecs = recommendations.filter((rec) => rec.action !== DriftAction.Update);
                if (otherRecs.length > 0) {
                    responseText += `## Recommendations (${otherRecs.length})\n\n`;
                    otherRecs.forEach((rec) => {
                        responseText += `- **${rec.testFile}** — Action: ${rec.action}, Priority: ${rec.priority}\n`;
                        responseText += `  ${rec.rationale}\n`;
                    });
                }
                else {
                    responseText += `No action required. All existing tests appear healthy.\n`;
                }
            }
            responseText += `\n\n## Next Steps\n\n`;
            responseText += `The AI assistant will:\n`;
            let stepNumber = 1;
            if (updateRecommendations.length > 0) {
                responseText += `${stepNumber++}. Review the changes and issues for each test\n`;
                responseText += `${stepNumber++}. Update test files to fix compatibility issues\n`;
                responseText += `${stepNumber++}. Preserve original test logic and structure\n`;
                responseText += `${stepNumber++}. Show you the changes made\n`;
            }
            if (newEndpoints.length > 0) {
                responseText += `${stepNumber++}. Generate new tests for new endpoints\n`;
            }
            responseText += `\n**This tool is currently in Early Preview stage. Please verify the results.**\n`;
            // ── Build LLM instructions for UPDATE ──
            const allRenames = [];
            for (const rec of updateRecommendations) {
                if (rec.renamedEndpoints && rec.renamedEndpoints.length > 0) {
                    allRenames.push(...rec.renamedEndpoints);
                }
            }
            const uniqueRenames = allRenames.filter((r, i, arr) => arr.findIndex((x) => x.oldPath === r.oldPath &&
                x.newPath === r.newPath &&
                x.method === r.method) === i);
            const llmInstructionsObj = {
                workflow: "test_maintenance",
                action: "execute_updates",
                auto_proceed: true,
                files_to_update: testFilesToUpdate,
                update_count: updateRecommendations.length,
            };
            if (uniqueRenames.length > 0) {
                llmInstructionsObj.endpoint_renames = uniqueRenames;
                llmInstructionsObj.rename_strategy =
                    "For each file, find-and-replace all occurrences of oldPath with newPath. Do NOT regenerate or restructure the test — only update the URL paths.";
                const fileRenames = [];
                for (const rec of updateRecommendations) {
                    if (rec._suggestedNewFile) {
                        fileRenames.push({ from: rec.testFile, to: rec._suggestedNewFile });
                    }
                }
                if (fileRenames.length > 0) {
                    llmInstructionsObj.file_renames = fileRenames;
                    llmInstructionsObj.file_rename_strategy =
                        "After updating path content in each file, rename the file using 'mv' or equivalent. Use git mv if the repo tracks the file.";
                }
            }
            // Update context: per-file guidance + current content for the downstream LLM.
            // Including file content avoids re-reads on each Edit turn, reducing token usage.
            const updateInstructionsFiles = [];
            for (const rec of updateRecommendations) {
                if (rec.updateInstructions) {
                    const current_content = testFileContentMap.get(rec.testFile);
                    updateInstructionsFiles.push({ file: rec.testFile, context: rec.updateInstructions, ...(current_content !== undefined && { current_content }) });
                }
            }
            if (updateInstructionsFiles.length > 0) {
                llmInstructionsObj.update_context = updateInstructionsFiles;
                llmInstructionsObj.update_strategy =
                    "For each file in update_context, apply the changes described in context to the provided current_content. Write the result using the Edit tool. Do NOT re-read the file first. Match the assertion style already used in the file. Preserve all existing test logic. After applying all edits, call skyramp_enhance_assertions with each updated file path to strengthen the assertions.";
            }
            const llmInstructions = `<!-- LLM_INSTRUCTIONS:\n${JSON.stringify(llmInstructionsObj, null, 2)}\n-->\n`;
            const contentBlocks = [
                {
                    type: "text",
                    text: responseText,
                    annotations: {
                        audience: ["user"],
                    },
                },
                {
                    type: "text",
                    text: llmInstructions,
                    annotations: {
                        audience: ["assistant"],
                    },
                },
            ];
            // ── Build ADD instructions for new endpoints ──
            if (newEndpoints.length > 0 && llmToolCalls.length > 0) {
                const addInstructionsObj = {
                    workflow: "add_tests_for_new_endpoints",
                    auto_proceed: true,
                    instruction: "Call each tool in tool_calls immediately and in order. Do NOT ask for confirmation. If endpointURL is a bare path, read .skyramp/workspace.yml for baseUrl and prepend it.",
                    tool_calls: llmToolCalls,
                };
                const addInstructions = `<!-- LLM_INSTRUCTIONS:\n${JSON.stringify(addInstructionsObj, null, 2)}\n-->\n`;
                contentBlocks.push({
                    type: "text",
                    text: addInstructions,
                    annotations: {
                        audience: ["assistant"],
                    },
                });
            }
            return {
                content: contentBlocks,
            };
        }
        catch (error) {
            logger.error(`Actions tool failed: ${error.message}`, error);
            errorResult = toolError(`Actions tool failed: ${error.message}`);
            return errorResult;
        }
        finally {
            let repositoryPath = "";
            try {
                const fullState = await StateManager.fromStatePath(args.stateFile).readFullState();
                repositoryPath = fullState?.metadata.repositoryPath || "";
            }
            catch (analyticsError) {
                logger.error(`Failed to read state for analytics in actions tool: ${analyticsError?.message ?? analyticsError}`, analyticsError);
            }
            AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
                repositoryPath,
            });
        }
    });
}
