import { z } from "zod";
import path from "path";
import { ScenarioGenerationService } from "../../services/ScenarioGenerationService.js";
import fs from "fs";
import { baseSchema, AUTH_PLACEHOLDER_TOKEN, HttpMethod } from "../../types/TestTypes.js";
import { AnalyticsService } from "../../services/AnalyticsService.js";
import { getWorkspaceAuthConfig, WorkspaceAuthType, getDefaultAuthHeader, isAuthorizationHeaderName, getAuthScheme } from "../../utils/workspaceAuth.js";
import yaml from "js-yaml";
import { logger } from "../../utils/logger.js";
function isJsonValue(v) {
    if (v === undefined || v === null)
        return true;
    try {
        JSON.parse(v);
        return true;
    }
    catch {
        return false;
    }
}
function isJsonObject(v) {
    if (v === undefined || v === null)
        return true;
    try {
        const p = JSON.parse(v);
        return typeof p === "object" && !Array.isArray(p) && p !== null;
    }
    catch {
        return false;
    }
}
export const stepSchema = z.object({
    method: z
        .nativeEnum(HttpMethod)
        .describe("HTTP method for this step."),
    path: z
        .string()
        .startsWith("/", { message: "path must begin with '/' (e.g. '/api/v1/products/123')" })
        .describe("API path for this step, must start with '/'. For steps that reference an ID from a prior step, use the same concrete value in the path (e.g. '/api/v1/orders/1') that appears in the prior step's response body (e.g. {\"id\": 1}). The backend auto-detects chaining by matching these values across steps."),
    requestBody: z
        .string()
        .optional()
        .refine(isJsonValue, { message: "requestBody must be valid JSON (e.g. '{\"name\":\"product\"}')." })
        .describe("JSON string of the request body parsed by AI from the scenario. "
        + "IMPORTANT: For GET and DELETE requests, do NOT put query/filter/search parameters here — use queryParams instead. "
        + "requestBody should only contain data sent in the HTTP body (typically for POST, PUT, PATCH)."),
    queryParams: z
        .string()
        .optional()
        .refine(isJsonObject, { message: "queryParams must be a JSON object string (e.g. '{\"limit\":\"10\"}')." })
        .describe(`JSON string of URL query parameters. Provide a FLAT object where each key is the exact URL parameter name and each value is a string or number (single value per key).

<examples>
  <example label="simple params">{"q": "bear", "limit": 10}</example>
  <example label="bracket-notation keys">{"filter[title][_neq]": "not-a-number"}</example>
  <example label="multi-value as comma-separated">{"tags": "sale,new,featured"}</example>
  <example label="JSON-encoded string value">{"filter": "{\\\"status\\\":\\\"active\\\"}"}</example>
</examples>

For multi-value params (e.g. ?tags=sale,new), provide a comma-separated string: "tags": "sale,new". Arrays are accepted but will be comma-joined into one value — note this produces a single ?tags=sale,new param, not repeated ?tags=sale&tags=new keys.

If you provide nested objects, the service JSON-stringifies them as a fallback, but you should inspect the target API source to determine the correct key format and use that directly.

Use this for GET request filters, search terms, pagination, sorting — any parameter that belongs in the URL query string.

CRITICAL: For search/filter/list endpoints (e.g., GET /products/search?q=bear&limit=10), parameters MUST go here, NOT in requestBody. GET request bodies are non-standard and may be ignored or rejected by servers and frameworks.`),
    responseBody: z
        .string()
        .optional()
        .describe("JSON string of the expected response body"),
    statusCode: z
        .number()
        .optional()
        .describe("Expected HTTP status code. Defaults: POST→201, DELETE→204, GET/PUT/PATCH→200."),
    responseHeaders: z
        .record(z.array(z.string()))
        .optional()
        .describe("Response headers as a JSON object. Defaults to Content-Type: application/json."),
}).superRefine((step, ctx) => {
    const isBodyMethod = ["POST", "PUT", "PATCH"].includes(step.method);
    if (isBodyMethod && step.requestBody) {
        let parsed;
        try {
            parsed = JSON.parse(step.requestBody);
        }
        catch {
            // JSON parse errors are already caught by the isJsonValue refine above
            return;
        }
        if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && Object.keys(parsed).length === 0) {
            ctx.addIssue({
                code: z.ZodIssueCode.custom,
                path: ["requestBody"],
                message: `requestBody for ${step.method} ${step.path} is an empty object {}. `
                    + `Provide actual field values from the source code schema (Pydantic model, Zod type, DTO) — `
                    + `do not send an empty body for ${step.method} requests.`,
            });
        }
    }
});
const batchScenarioSchema = {
    scenarioName: z
        .string()
        .describe("Short name for the multi-step workflow scenario being tested. Use underscores, not hyphens (e.g., 'product_lifecycle', 'user_checkout_flow'). Used as the output file name: scenario_{scenarioName}.json."),
    destination: z
        .string()
        .describe("Destination hostname or IP address for the test (e.g., api.example.com, localhost, 192.168.1.1). Do NOT include port numbers."),
    baseURL: z
        .string()
        .optional()
        .describe("Base URL for the API endpoints (e.g., http://localhost:8000 or http://localhost:8000/api/v1). When baseURL includes a pathname, each step's path is appended to it (e.g., baseURL=/api/v1 + path=/orders → /api/v1/orders)."),
    apiSchema: z
        .string()
        .default("")
        .describe("Optional. Absolute path (/path/to/openapi.json) to the OpenAPI/Swagger schema file or a URL (e.g., https://api.example.com/openapi.json). DO NOT TRY TO ASSUME THE OPENAPI SCHEMA IF NOT PROVIDED. NOTE TO AI ASSISTANTS: You do not need to read the contents of this file - simply pass the file path as the backend will handle reading and processing it."),
    prompt: z
        .string()
        .optional()
        .describe("Description of the overall multi-step scenario being tested"),
    steps: z
        .array(stepSchema)
        .min(1)
        .describe("Ordered array of API request steps. Each step is generated as a TraceRequest and appended to the scenario file."),
    outputDir: baseSchema.shape.outputDir.describe("MUST be absolute path to the directory where the synthetic trace output file will be generated. Usually the same directory as the integration test."),
    authHeader: z
        .string()
        .optional()
        .describe("Which HTTP header carries the auth credential (e.g., 'Authorization', 'X-Api-Key', 'Cookie'). Omit or pass empty string to auto-resolve from workspace config. To force an unauthenticated request, omit AND ensure no workspace auth is configured."),
    authScheme: z
        .string()
        .optional()
        .describe("Only when authHeader is 'Authorization'. The prefix before the token "
        + "(e.g., 'Bearer' produces 'Authorization: Bearer <token>'). "
        + "Common values: 'Bearer', 'Basic', 'Token'. "
        + "Leave empty if the API uses raw tokens with no prefix. "
        + "Ignored for non-Authorization headers like X-Api-Key or Cookie."),
    authToken: z
        .string()
        .optional()
        .describe("Optional explicit auth token value. Leave empty — the service auto-generates "
        + `${AUTH_PLACEHOLDER_TOKEN} for all auth headers (Authorization, X-API-Key, Cookie, etc.). `
        + `Only provide this when you need a custom value (e.g., 'session=${AUTH_PLACEHOLDER_TOKEN}' for Cookie).`),
};
const TOOL_NAME = "skyramp_batch_scenario_test_generation";
export function registerBatchScenarioTestTool(server) {
    server.registerTool(TOOL_NAME, {
        description: `Generate a synthetic scenario trace file for a multi-step API workflow scenario — use this before skyramp_integration_test_generation.

### When to use this tool
- When the user asks to generate a scenario trace for a multi-step API workflow (example: create → read → update → delete).
- When you need a synthetic trace file before calling \`skyramp_integration_test_generation\`.

### Pre-call Verification (REQUIRED — verify all before populating parameters)
This tool accepts AI-parsed structured parameters from a natural language scenario description. Analyze the scenario and provide structured parameters for all steps.
1. **Dynamic context**: If \`skyramp_analyze_changes\` has already run and returned a \`sessionId\`, fetch endpoint detail before building each step: \`skyramp://analysis/{sessionId}/endpoints/{path}/{method}\`. This gives you exact request body fields, types, and required vs optional — use it instead of guessing from field names.
2. **Endpoints**: Confirm each step's method + path exists as a real endpoint (from OpenAPI spec, source code routes, or skyramp_analyze_changes output). Do NOT invent paths.
3. **Status codes**: Confirm expected status code per step (defaults: POST→201, DELETE→204, GET/PUT/PATCH→200 — note if non-standard).
4. **Request bodies**: Identify each field and its source (schema / prior step response / user input). For GET/DELETE steps, confirm filters go in queryParams — NEVER in requestBody.
5. **Chaining**: For steps that use an ID from a prior step, use the same concrete value in the path (e.g. \`/api/v1/orders/1\`) that will appear in the prior step's response body (e.g. \`{"id": 1}\`). The backend auto-detects chaining by matching values across step responses.
6. **Echo-back fields**: Identify which request body fields will be returned unchanged in the response — these will need exact-value assertions in the generated test.
7. **Missing info**: If any required path parameter or request body field cannot be determined without guessing, STOP and ask the user before calling the tool.

### Error Recovery
If a step fails, the error shows which step (N/total), method+path, and reason. Fix that step and resubmit the full \`steps\` array — do NOT split into separate calls.

### After this tool
Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\` path. Do NOT also pass \`apiSchema\` or \`endpointURL\` — the scenario file already contains all endpoint information and passing both will cause test generation to fail.`,
        inputSchema: batchScenarioSchema,
    }, async (params) => {
        if (params.authHeader === undefined) {
            try {
                const repoPath = params.outputDir || process.cwd();
                const wsAuth = await getWorkspaceAuthConfig(repoPath);
                // Resolve the auth header: prefer explicit authHeader, fall back to inferred default.
                let resolvedHeader = wsAuth.authHeader ?? getDefaultAuthHeader(wsAuth.authType);
                // authScheme is set but getDefaultAuthHeader returned "" (e.g. unrecognized authType or
                // custom scheme like "Ghost"/"Hawk"). authScheme only applies to Authorization headers.
                if (!resolvedHeader && wsAuth.authScheme !== undefined) {
                    resolvedHeader = "Authorization";
                }
                if (resolvedHeader && wsAuth.authType !== WorkspaceAuthType.None) {
                    logger.info("Auth header was empty — resolved from workspace config", {
                        authHeader: resolvedHeader,
                        authType: wsAuth.authType,
                    });
                    params.authHeader = resolvedHeader;
                    // Scheme only applies to Authorization headers; use header name (not authType) to detect.
                    if (isAuthorizationHeaderName(resolvedHeader)) {
                        // Prefer explicit authScheme from workspace (for custom schemes like "Ghost", "Hawk").
                        // Fall back to getAuthScheme() for standard types (bearer→Bearer, token→Token).
                        // Use ?? to preserve intentionally empty authScheme ("" = raw token, no prefix).
                        params.authScheme = params.authScheme ?? wsAuth.authScheme ?? getAuthScheme(wsAuth.authType);
                    }
                }
            }
            catch {
                logger.warning("Could not resolve auth from workspace config");
            }
        }
        // Separate try/catch so auth errors don't silently swallow schema population.
        // Walk up from outputDir with a sync existsSync check — one stat per level,
        // one read+parse only when found — instead of an async readWorkspaceConfigRaw
        // call (WorkspaceConfigManager instantiation + 2 async I/O ops) per level.
        if (!params.apiSchema) {
            try {
                const WS_SUBPATH = path.join(".skyramp", "workspace.yml");
                // Assumption: outputDir lives somewhere inside the repo tree so the walk-up
                // eventually reaches .skyramp/workspace.yml. If outputDir is outside the repo
                // (e.g. /tmp/skyramp-test), the loop exits at the filesystem root without finding
                // the config — the surrounding try/catch handles this gracefully (non-critical).
                let searchDir = path.resolve(params.outputDir);
                let wsConfigPath = null;
                while (searchDir !== path.dirname(searchDir)) {
                    const candidate = path.join(searchDir, WS_SUBPATH);
                    if (fs.existsSync(candidate)) {
                        wsConfigPath = candidate;
                        break;
                    }
                    searchDir = path.dirname(searchDir);
                }
                if (wsConfigPath) {
                    const wsRaw = yaml.load(fs.readFileSync(wsConfigPath, "utf-8"));
                    // Best-effort: picks the first service. Multi-service workspaces may have
                    // the wrong schema if outputDir belongs to a later service — acceptable
                    // limitation for now; the user can always pass apiSchema explicitly.
                    const rawSchemaPath = wsRaw?.services?.[0]?.api?.schemaPath;
                    if (rawSchemaPath && typeof rawSchemaPath === "string") {
                        const isUrl = rawSchemaPath.startsWith("http://") || rawSchemaPath.startsWith("https://");
                        // searchDir is the directory where workspace.yml was found — use it as
                        // the resolution base so relative paths like "../openapi.yml" resolve
                        // correctly regardless of outputDir depth.
                        const schemaPath = isUrl
                            ? rawSchemaPath
                            : path.resolve(searchDir, rawSchemaPath);
                        params = { ...params, apiSchema: schemaPath };
                        logger.info("Auto-populated apiSchema from workspace config", { schemaPath });
                    }
                }
            }
            catch {
                // non-critical
            }
        }
        // ── Change 10b: Reject GraphQL endpoint steps ──
        // Skyramp supports REST testing only; /graphql* requires introspection not implemented.
        {
            const graphqlSteps = params.steps.filter((s) => {
                if (typeof s.path !== "string")
                    return false;
                const normalized = s.path.replace(/\/+$/, "").toLowerCase();
                return normalized.split("/").some((seg) => seg === "graphql");
            });
            if (graphqlSteps.length > 0) {
                return {
                    isError: true,
                    content: [{ type: "text", text: `GraphQL endpoints are not supported by Skyramp's test generation.\n` +
                                `Affected steps: ${graphqlSteps.map((s) => `${s.method} ${s.path}`).join(", ")}\n\n` +
                                `Skyramp supports REST API testing only. Remove GraphQL steps and use ` +
                                `the REST endpoints for the same resource instead.`,
                        }],
                };
            }
        }
        // ── Change 3: Soft spec path validation ──
        // Warn when step paths are missing from the spec — proceed with generation regardless.
        // Hard rejection was removed: specs frequently lag code (new endpoints, undocumented
        // internal routes, stale auto-gen) so a missing path != a phantom path.
        let specValidationWarning = "";
        if (params.apiSchema) {
            try {
                const isUrl = params.apiSchema.startsWith("http://") || params.apiSchema.startsWith("https://");
                let specText;
                if (isUrl) {
                    const specRes = await fetch(params.apiSchema, { signal: AbortSignal.timeout(10_000) });
                    if (!specRes.ok) {
                        throw new Error(`HTTP ${specRes.status} ${specRes.statusText} fetching spec at ${params.apiSchema}`);
                    }
                    specText = await specRes.text();
                }
                else {
                    // Note: relative apiSchema paths resolve against outputDir, not the workspace root.
                    // In practice apiSchema is always absolute (Gap Fix 2 / schema guidance), so this is safe.
                    specText = fs.readFileSync(path.resolve(params.outputDir, params.apiSchema), "utf-8");
                }
                // js-yaml handles both JSON and YAML specs
                const specLoaded = yaml.load(specText);
                const specPaths = new Set(Object.keys((specLoaded && typeof specLoaded === "object" ? specLoaded.paths : null) ?? {}));
                if (specPaths.size === 0) {
                    logger.warning("Spec loaded but contains no paths — skipping path check", {
                        apiSchema: params.apiSchema,
                    });
                }
                else {
                    const unverifiedSteps = params.steps.filter((s) => {
                        if (typeof s.path !== "string")
                            return false;
                        const norm = s.path.replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, (m) => `{${m.slice(1)}}`);
                        return !specPaths.has(s.path) && !specPaths.has(norm);
                    });
                    if (unverifiedSteps.length > 0) {
                        specValidationWarning =
                            `\n\n⚠️ **Spec warning** — the following paths were not found in \`${params.apiSchema}\` ` +
                                `(spec may be stale or incomplete — verify paths against source before running tests):\n` +
                                unverifiedSteps.map((s) => `  ${s.method} ${s.path}`).join("\n") +
                                `\n\nKnown spec paths (first 20): ${[...specPaths].slice(0, 20).join(", ")}`;
                        logger.warning("Step paths not found in spec — proceeding (spec may lag code)", {
                            unverifiedPaths: unverifiedSteps.map((s) => s.path),
                            apiSchema: params.apiSchema,
                        });
                    }
                }
            }
            catch (err) {
                logger.warning("Spec check skipped — could not load apiSchema", {
                    apiSchema: params.apiSchema,
                    error: err instanceof Error ? err.message : String(err),
                });
            }
        }
        const service = new ScenarioGenerationService();
        const steps = params.steps;
        const scenarioSlug = params.scenarioName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "scenario";
        const fileName = `scenario_${scenarioSlug}.json`;
        const filePath = path.join(params.outputDir, fileName);
        const resolvedOut = path.resolve(params.outputDir);
        if (!path.resolve(filePath).startsWith(resolvedOut + path.sep) && path.resolve(filePath) !== resolvedOut) {
            return {
                content: [{ type: "text", text: `Validation Error: scenarioName '${params.scenarioName}' produced a file path outside outputDir.\n`
                            + `Expected: File path within '${params.outputDir}'.\n`
                            + `Fix: Use a simpler scenarioName without path separators or special characters.` }],
                isError: true,
            };
        }
        const traceRequests = [];
        for (let i = 0; i < steps.length; i++) {
            const step = steps[i];
            const stepParams = {
                scenarioName: params.scenarioName,
                destination: params.destination,
                baseURL: params.baseURL,
                apiSchema: params.apiSchema,
                method: step.method,
                path: step.path,
                requestBody: step.requestBody,
                queryParams: step.queryParams,
                responseBody: step.responseBody,
                statusCode: step.statusCode,
                responseHeaders: step.responseHeaders,
                outputDir: params.outputDir,
                authHeader: params.authHeader ?? "",
                authScheme: params.authScheme ?? "",
                authToken: params.authToken,
            };
            const traceRequest = service.generateTraceRequestFromInput(stepParams);
            if (!traceRequest) {
                const failResult = {
                    content: [
                        {
                            type: "text",
                            text: `**Batch Scenario Generation Failed at Step ${i + 1}/${steps.length}**\n\n`
                                + `${i}/${steps.length} steps succeeded before failure.\n\n`
                                + `**Failed step:** ${step.method} ${step.path}\n`
                                + `**Error:** Could not generate a trace request from the provided parameters.\n\n`
                                + `Fix the failing step and retry with the full batch.`,
                        },
                    ],
                    isError: true,
                };
                AnalyticsService.pushMCPToolEvent(TOOL_NAME, failResult, {
                    scenarioName: params.scenarioName,
                    stepCount: String(i),
                    failedStep: String(i + 1),
                }).catch(() => { });
                return failResult;
            }
            traceRequests.push(traceRequest);
        }
        try {
            fs.writeFileSync(filePath, JSON.stringify(traceRequests, null, 2), "utf8");
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            return {
                content: [{ type: "text", text: `Error writing scenario file: ${errorMessage}` }],
                isError: true,
            };
        }
        const stepCount = traceRequests.length;
        const result = {
            content: [
                {
                    type: "text",
                    text: `**Batch Scenario Generated — ${stepCount} steps**\n\n`
                        + `**Scenario:** ${params.scenarioName}\n`
                        + `**Steps:**\n${steps.map((s, i) => `  ${i + 1}. ${s.method} ${s.path} → ${s.statusCode ?? "default"}`).join("\n")}\n\n`
                        + `**File:** ${filePath}\n\n`
                        + `**Next:** Call \`skyramp_integration_test_generation\` with \`scenarioFile: "${filePath}"\``
                        + specValidationWarning,
                },
            ],
            isError: false,
        };
        AnalyticsService.pushMCPToolEvent(TOOL_NAME, result, {
            scenarioName: params.scenarioName,
            stepCount: String(stepCount),
        }).catch(() => { });
        return result;
    });
}
