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 * 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;
}
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 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\`.

**EXECUTING ACTIONS:**
- UPDATE: Reads each test file and emits targeted per-file edit instructions driven by \`updateInstructions\` and \`renamedEndpoints\`
- REGENERATE: Reads the existing file for context (endpoint, auth, test type), then instructs the LLM to call the appropriate generation tool (e.g. \`skyramp_integration_test_generation\`) with \`outputDir\` + \`output\` matching the existing file to overwrite it
- IGNORE / VERIFY / DELETE: Passed through and summarised — no file reads, no automated edits

**OUTPUT:**
Per-file instructions for UPDATE and REGENERATE actions, plus a structured \`LLM_INSTRUCTIONS\` block for automated execution.
`,
        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;
            }
            // Resolve repo root for path normalization and security checks.
            const repoRoot = repositoryPath ? path.resolve(repositoryPath) : "";
            // Relevant external (user-written) tests: UPDATE is permitted; REGENERATE/DELETE
            // are report-only (the LLM may recommend them but this tool will not apply them).
            // Paths are stored relative to repositoryPath in the state file — re-absolutize.
            const relevantExternalPaths = new Set((stateData.repositoryAnalysis?.relevantExternalTestPaths ?? []).map((p) => path.isAbsolute(p) ? p : path.resolve(repoRoot, p)));
            // Allowlist: Skyramp-generated tests + relevant external tests.
            // Using an allowlist (not a blocklist) catches hallucinated paths the LLM
            // may supply that are not in the scanned catalog at all.
            const testAnalysisResults = (stateData.existingTests || []);
            const skyrampTestFiles = new Set(testAnalysisResults
                .filter((t) => (t.source ?? TestSource.Skyramp) !== TestSource.External)
                .map((t) => t.testFile));
            const externalTestFiles = new Set(testAnalysisResults
                .filter((t) => (t.source ?? TestSource.Skyramp) === TestSource.External)
                .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.
                // Normalize via path.resolve to collapse any `..` segments before the
                // traversal guard — otherwise "/repo/../etc/passwd" would pass startsWith.
                const rawFile = path.isAbsolute(rec.testFile)
                    ? rec.testFile
                    : repoRoot
                        ? path.resolve(repoRoot, rec.testFile)
                        : rec.testFile;
                const resolvedFile = path.resolve(rawFile);
                // Reject files outside the repo root (path-traversal guard).
                // Exception: files already in the scanned test catalog (externalTestFiles / skyrampTestFiles)
                // may legitimately live in a separate testsRepoDir outside repositoryPath — catalog
                // membership is a sufficient provenance check for those paths.
                const isInCatalog = skyrampTestFiles.has(resolvedFile) || skyrampTestFiles.has(rec.testFile)
                    || externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.testFile);
                if (repoRoot && !isInCatalog && !resolvedFile.startsWith(repoRoot + path.sep) && resolvedFile !== repoRoot) {
                    logger.warning(`Skipping recommendation for path outside repo root: ${rec.testFile}`);
                    return;
                }
                // Guard: only files present in the scanned test catalog may receive any
                // recommendation. Hallucinated paths (not in either set) are rejected for
                // all actions, including VERIFY and IGNORE, to keep the report consistent
                // with what was actually discovered.
                const isSkyramp = skyrampTestFiles.has(resolvedFile) || skyrampTestFiles.has(rec.testFile);
                const isRelevantExternal = (externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.testFile)) &&
                    (relevantExternalPaths.has(resolvedFile) || relevantExternalPaths.has(rec.testFile));
                const isInAnyKnownCatalog = isSkyramp || isRelevantExternal
                    || externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.testFile);
                if (!isInAnyKnownCatalog) {
                    logger.warning(`Skipping ${rec.action} for unknown test (not in scanned catalog): ${rec.testFile}`);
                    return;
                }
                const isActionable = [DriftAction.Update, DriftAction.Regenerate, DriftAction.Delete].includes(rec.action);
                if (isActionable && !isSkyramp && !isRelevantExternal) {
                    logger.warning(`Skipping ${rec.action} for irrelevant external test: ${rec.testFile}`);
                    return;
                }
                // REGENERATE and DELETE on external tests are report-only — convert to VERIFY so
                // the finding surfaces to the developer without touching the file.
                if (isRelevantExternal && !isSkyramp &&
                    (rec.action === DriftAction.Regenerate || rec.action === DriftAction.Delete)) {
                    recommendations.push({
                        testFile: resolvedFile,
                        action: DriftAction.Verify,
                        priority: rec.priority ?? RecommendationPriority.Medium,
                        rationale: `[external test — needs manual review] ${rec.rationale ?? ""}`.trimEnd(),
                        estimatedWork: rec.estimatedWork ?? EstimatedWork.Small,
                        updateInstructions: "",
                        renamedEndpoints: [],
                    });
                    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 and REGENERATE 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,
            };
            // Build per-file winner maps. REGENERATE beats UPDATE for the same file —
            // if the LLM emits both, keep REGENERATE (higher severity) and drop UPDATE.
            const updateByFile = new Map();
            const regenerateByFile = new Map();
            for (const rec of recommendations) {
                if (rec.action === DriftAction.Regenerate) {
                    const existing = regenerateByFile.get(rec.testFile);
                    if (!existing || priorityRank[rec.priority] > priorityRank[existing.priority]) {
                        regenerateByFile.set(rec.testFile, rec);
                    }
                }
                else if (rec.action === DriftAction.Update) {
                    // Only add to updateByFile if no REGENERATE exists for this file.
                    if (!regenerateByFile.has(rec.testFile)) {
                        const existing = updateByFile.get(rec.testFile);
                        if (!existing || priorityRank[rec.priority] > priorityRank[existing.priority]) {
                            updateByFile.set(rec.testFile, rec);
                        }
                    }
                }
            }
            // Second pass: drop any UPDATE entries for files that ended up with REGENERATE
            // (handles ordering where UPDATE was inserted before REGENERATE was seen).
            for (const file of regenerateByFile.keys()) {
                updateByFile.delete(file);
            }
            const updateRecommendations = Array.from(updateByFile.values());
            const regenerateRecommendations = Array.from(regenerateByFile.values());
            const fileInstructions = [];
            const testFilesToUpdate = [];
            const testFileContentMap = new Map();
            // ── UPDATE: read file, emit targeted edit instructions ──
            for (const rec of updateRecommendations) {
                if (!rec.testFile) {
                    logger.warning("Recommendation missing testFile", rec);
                    continue;
                }
                testFilesToUpdate.push(rec.testFile);
                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`;
                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) {
                    const fallbackRationale = rec.rationale ?? "";
                    if (fallbackRationale) {
                        instruction += `**Why:** ${fallbackRationale}\n\n`;
                    }
                    instruction += `**Action:** Update this test file based on 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`;
                }
                fileInstructions.push(instruction);
            }
            // ── REGENERATE: read file for context, emit overwrite instructions ──
            const regenerateInstructions = [];
            const testFilesToRegenerate = [];
            const regenerateContentMap = new Map();
            for (const rec of regenerateRecommendations) {
                if (!rec.testFile) {
                    logger.warning("Recommendation missing testFile", rec);
                    continue;
                }
                testFilesToRegenerate.push(rec.testFile);
                let existingContent = "";
                try {
                    existingContent = fs.readFileSync(rec.testFile, "utf-8");
                    regenerateContentMap.set(rec.testFile, existingContent);
                }
                catch (error) {
                    logger.warning(`Could not read file for REGENERATE context ${rec.testFile}: ${error.message}`);
                }
                let instruction = `\n### ${rec.testFile}\n\n`;
                instruction += `**Action: REGENERATE** — the response shape changed too drastically for targeted edits.\n\n`;
                if (rec.updateInstructions) {
                    instruction += `**What changed:**\n\n${rec.updateInstructions}\n\n`;
                }
                instruction += `Call the appropriate generation tool (e.g. \`skyramp_integration_test_generation\`, \`skyramp_contract_test_generation\`) with \`outputDir: "${path.dirname(rec.testFile)}"\` and \`output: "${path.basename(rec.testFile)}"\` to overwrite this file from scratch. Use the existing file content in \`LLM_INSTRUCTIONS.regenerate_context\` for context on the endpoint, auth pattern, and test structure — replicate the test type and language.\n\n`;
                regenerateInstructions.push(instruction);
            }
            // ── 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 (regenerateRecommendations.length > 0) {
                responseText += `\n## Tests Requiring Regeneration (${regenerateRecommendations.length})\n\n`;
                testFilesToRegenerate.forEach((file, idx) => {
                    responseText += `${idx + 1}. \`${file}\`\n`;
                });
                responseText += `\n---\n`;
                responseText += regenerateInstructions.join("\n---\n");
            }
            const otherRecs = recommendations.filter((rec) => rec.action !== DriftAction.Update && rec.action !== DriftAction.Regenerate);
            if (otherRecs.length > 0) {
                responseText += `\n## Other Findings (${otherRecs.length})\n\n`;
                otherRecs.forEach((rec) => {
                    responseText += `- **${rec.testFile}** — Action: ${rec.action}, Priority: ${rec.priority}`;
                    if (rec.rationale)
                        responseText += ` — ${rec.rationale}`;
                    responseText += `\n`;
                });
            }
            else if (updateRecommendations.length === 0 && regenerateRecommendations.length === 0) {
                responseText += `No action required. All existing tests appear healthy.\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,
                files_to_regenerate: testFilesToRegenerate,
                regenerate_count: regenerateRecommendations.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.";
            }
            // REGENERATE context: existing file content gives the generation tool the
            // endpoint URL, auth pattern, test type, and language to replicate.
            const regenerateContextFiles = [];
            for (const rec of regenerateRecommendations) {
                const existing_content = regenerateContentMap.get(rec.testFile);
                regenerateContextFiles.push({
                    file: rec.testFile,
                    rationale: rec.updateInstructions || rec.rationale,
                    ...(existing_content !== undefined && { existing_content }),
                });
            }
            if (regenerateContextFiles.length > 0) {
                llmInstructionsObj.regenerate_context = regenerateContextFiles;
                llmInstructionsObj.regenerate_strategy =
                    "For each file in regenerate_context, call the appropriate generation tool (skyramp_integration_test_generation or skyramp_contract_test_generation) with outputDir set to the file's directory and output set to the filename. Use existing_content to determine the test type, endpoint, auth pattern, and language. The generation tool will overwrite the file. Do NOT use skyramp_ui_test_generation here — UI test regeneration requires a recorded trace (playwrightInput) and must be handled separately.";
            }
            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"],
                    },
                },
            ];
            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,
            });
        }
    });
}
