import { z } from "zod";
import { registerMcpTool } from "./tool-registry.js";
import { createSandbox } from "./sandbox.js";
import { truncateResult } from "./truncate.js";
import { hasRequiredFeatures } from "./auth.js";
import { getApiEndpoints, getRawOpenApiSpec } from "./api-endpoint-index.js";
import {
  getCachedEntityGraph,
  inferModuleFromEntity
} from "./entity-graph.js";
import {
  lookupSearchCache,
  storeSearchResult,
  buildMemoryContext,
  buildSearchLabel,
  incrementToolCallCount
} from "./session-memory.js";
import { fetchWithTimeout, resolveTimeoutMs } from "@open-mercato/shared/lib/http/fetchWithTimeout";
const DEFAULT_AI_API_REQUEST_TIMEOUT_MS = 3e4;
function resolveAiApiRequestTimeoutMs() {
  const raw = process.env.AI_API_REQUEST_TIMEOUT_MS;
  const parsed = raw ? Number.parseInt(raw, 10) : void 0;
  return resolveTimeoutMs(parsed, DEFAULT_AI_API_REQUEST_TIMEOUT_MS);
}
let cachedCodeModeSpec = null;
let cachedCommonTypes = null;
const CODE_MODE_REQUIRED_FEATURES = ["ai_assistant.view"];
async function getCodeModeSpec() {
  if (cachedCodeModeSpec) return cachedCodeModeSpec;
  const rawSpec = await getRawOpenApiSpec();
  const graph = getCachedEntityGraph();
  const paths = rawSpec?.paths ?? {};
  const entitySchemas = graph ? buildEntitySchemas(graph) : [];
  const spec = {
    paths,
    info: rawSpec?.info,
    components: rawSpec?.components,
    entitySchemas
  };
  spec.findEndpoints = (keyword) => {
    const kw = keyword.toLowerCase();
    return Object.entries(paths).filter(([path]) => path.toLowerCase().includes(kw)).map(([path, methods]) => ({
      path,
      methods: Object.keys(methods).filter((m) => m !== "parameters")
    }));
  };
  spec.describeEndpoint = (path, method) => {
    const pathObj = paths[path];
    if (!pathObj) return null;
    const endpoint = pathObj[method.toLowerCase()];
    if (!endpoint) return null;
    const bodySchema = extractRequestBodySchema(endpoint);
    const bodyProps = bodySchema?.properties ?? {};
    const bodyRequired = bodySchema?.required ?? [];
    const requiredFields = [];
    const optionalFields = [];
    const nestedCollections = [];
    for (const [name, prop] of Object.entries(bodyProps)) {
      const propType = prop.type || "string";
      if (propType === "array" && prop.items && prop.items.type === "object") {
        const itemSchema = prop.items;
        const itemProps = itemSchema.properties ?? {};
        const itemRequired = itemSchema.required ?? [];
        const nestedRequired = itemRequired.map((n) => ({
          name: n,
          type: itemProps[n]?.type || "string"
        }));
        const nestedOptional = Object.keys(itemProps).filter((n) => !itemRequired.includes(n));
        const commonFields = nestedOptional.slice(0, 6);
        nestedCollections.push({
          field: name,
          type: "array",
          requiredFields: nestedRequired,
          commonFields
        });
        continue;
      }
      if (bodyRequired.includes(name)) {
        const field = { name, type: propType };
        if (prop.format) field.format = prop.format;
        requiredFields.push(field);
      } else {
        optionalFields.push(name);
      }
    }
    const example = {};
    for (const field of requiredFields) {
      example[field.name] = generatePlaceholder(field.type, field.format);
    }
    for (const collection of nestedCollections) {
      const itemExample = {};
      for (const field of collection.requiredFields) {
        itemExample[field.name] = generatePlaceholder(field.type);
      }
      for (const name of collection.commonFields.slice(0, 2)) {
        itemExample[name] = "<value>";
      }
      example[collection.field] = [itemExample];
    }
    const segments = path.replace("/api/", "").split("/");
    const moduleSegment = segments[0];
    const resourceName = segments[1] || segments[0];
    const modulePrefix = `/api/${moduleSegment}/`;
    const relatedEndpoints = Object.entries(paths).filter(([p]) => p.startsWith(modulePrefix) && p !== path && !p.includes("{")).map(([p, methods]) => ({
      path: p,
      methods: Object.keys(methods).filter((m) => m !== "parameters")
    })).slice(0, 8);
    const resourceNorm = resourceName.replace(/-/g, "_");
    const resourceSingular = resourceNorm.endsWith("s") ? resourceNorm.slice(0, -1) : resourceNorm;
    const moduleSingular = moduleSegment.endsWith("s") ? moduleSegment.slice(0, -1) : moduleSegment;
    const prefixedTable = `${moduleSingular}_${resourceNorm}`;
    const entity = entitySchemas.find((e) => {
      const table = (e.tableName || "").toLowerCase();
      const cls = (e.className || "").toLowerCase();
      const mod = (e.module || "").toLowerCase();
      if (table === resourceNorm || table === prefixedTable) return true;
      if (cls.includes(moduleSingular) && cls.includes(resourceSingular)) return true;
      if (mod === moduleSegment && cls.includes(resourceSingular)) return true;
      if (cls === resourceSingular || cls.includes(resourceSingular)) return true;
      return false;
    }) || null;
    let relatedEntity = null;
    if (entity) {
      const ent = entity;
      const rels = ent.relationships || [];
      const relSummary = rels.map((r) => `${r.relationship}: ${r.target}`).join(", ");
      relatedEntity = `${ent.className}${relSummary ? ` (${relSummary})` : ""}`;
    }
    const parameters = method.toLowerCase() === "get" ? (endpoint.parameters || []).filter((p) => p.in === "query").map((p) => p.name) : void 0;
    return {
      path,
      method: method.toUpperCase(),
      summary: endpoint.summary || endpoint.description,
      ...parameters && parameters.length > 0 ? { queryParams: parameters } : {},
      ...requiredFields.length > 0 ? { requiredFields } : {},
      ...optionalFields.length > 0 ? { optionalFields } : {},
      ...nestedCollections.length > 0 ? { nestedCollections } : {},
      ...Object.keys(example).length > 0 ? { example } : {},
      ...relatedEndpoints.length > 0 ? { relatedEndpoints } : {},
      relatedEntity
    };
  };
  spec.describeEntity = (keyword) => {
    const kw = keyword.toLowerCase();
    return entitySchemas.find((e) => {
      const cls = (e.className || "").toLowerCase();
      const table = (e.tableName || "").toLowerCase();
      return cls.includes(kw) || table.includes(kw);
    }) || null;
  };
  cachedCodeModeSpec = spec;
  return spec;
}
function extractRequestBodySchema(endpoint) {
  const requestBody = endpoint.requestBody;
  if (!requestBody) return null;
  const content = requestBody.content;
  if (!content) return null;
  const jsonContent = content["application/json"];
  if (!jsonContent) return null;
  return jsonContent.schema || null;
}
function generatePlaceholder(type, format) {
  if (format === "uuid" || format === "objectId") return "<uuid>";
  if (format === "date-time" || format === "date") return "<date>";
  if (format === "email") return "<email>";
  switch (type) {
    case "string":
      return "<string>";
    case "number":
    case "integer":
      return 0;
    case "boolean":
      return false;
    case "array":
      return [];
    default:
      return "<value>";
  }
}
const COMMON_ENDPOINTS = [
  { path: "/api/sales/quotes", method: "post", typeName: "CreateQuote" },
  { path: "/api/sales/orders", method: "post", typeName: "CreateOrder" },
  { path: "/api/sales/invoices", method: "post", typeName: "CreateInvoice" },
  { path: "/api/customers/companies", method: "post", typeName: "CreateCompany" },
  { path: "/api/customers/people", method: "post", typeName: "CreatePerson" },
  { path: "/api/customers/deals", method: "post", typeName: "CreateDeal" },
  { path: "/api/catalog/products", method: "post", typeName: "CreateProduct" },
  { path: "/api/customers/companies", method: "put", typeName: "UpdateCompany" },
  { path: "/api/customers/people", method: "put", typeName: "UpdatePerson" },
  { path: "/api/sales/quotes", method: "put", typeName: "UpdateQuote" }
];
async function generateCommonTypes() {
  if (cachedCommonTypes) return cachedCommonTypes;
  const rawSpec = await getRawOpenApiSpec();
  if (!rawSpec?.paths) {
    cachedCommonTypes = "";
    return "";
  }
  const paths = rawSpec.paths;
  const typeLines = ["Available types for api.request() body:\n"];
  for (const { path, method, typeName } of COMMON_ENDPOINTS) {
    const pathObj = paths[path];
    if (!pathObj) continue;
    const endpoint = pathObj[method];
    if (!endpoint) continue;
    const bodySchema = extractRequestBodySchema(endpoint);
    if (!bodySchema?.properties) continue;
    const typeStr = schemaToTypeString(
      typeName,
      bodySchema,
      `${method.toUpperCase()} ${path}`
    );
    if (typeStr) typeLines.push(typeStr);
  }
  if (typeLines.length <= 1) {
    cachedCommonTypes = "";
    return "";
  }
  cachedCommonTypes = typeLines.join("\n");
  console.error(`[Code Mode] Generated ${typeLines.length - 1} common type stubs`);
  return cachedCommonTypes;
}
function schemaToTypeString(typeName, schema, comment) {
  const props = schema.properties;
  if (!props) return null;
  const required = new Set(schema.required || []);
  const skipFields = /* @__PURE__ */ new Set(["tenantId", "organizationId"]);
  const fields = [];
  const nestedTypes = [];
  for (const [name, prop] of Object.entries(props)) {
    if (skipFields.has(name)) continue;
    if (!prop || typeof prop !== "object") continue;
    const isRequired = required.has(name);
    const optMark = isRequired ? "" : "?";
    if (prop.type === "array" && prop.items && prop.items.type === "object") {
      const itemTypeName = `${typeName}${capitalize(singularize(name))}`;
      const itemSchema = prop.items;
      const nestedType = schemaToTypeString(itemTypeName, itemSchema, "");
      if (nestedType) nestedTypes.push(nestedType);
      fields.push(`${name}${optMark}: ${itemTypeName}[]`);
      continue;
    }
    const propType = resolvePropertyType(prop);
    fields.push(`${name}${optMark}: ${propType}`);
  }
  if (fields.length === 0) return null;
  const commentLine = comment ? `// ${comment}
` : "";
  const nested = nestedTypes.length > 0 ? nestedTypes.join("\n") + "\n" : "";
  return `${nested}${commentLine}type ${typeName} = { ${fields.join("; ")} }`;
}
function resolvePropertyType(prop) {
  if (prop.anyOf && Array.isArray(prop.anyOf)) {
    const variants = prop.anyOf.filter(
      (s) => s != null
    );
    const nonNull = variants.filter((s) => s.type !== "null");
    if (nonNull.length === 1) {
      return resolvePropertyType(nonNull[0]) + " | null";
    }
    if (nonNull.length > 1) {
      return nonNull.map((s) => resolvePropertyType(s)).join(" | ");
    }
  }
  if (prop.enum && Array.isArray(prop.enum)) {
    return prop.enum.map((v) => `'${v}'`).join(" | ");
  }
  const type = prop.type;
  const format = prop.format;
  if (type === "array") {
    const items = prop.items;
    if (items) return `${resolvePropertyType(items)}[]`;
    return "unknown[]";
  }
  if (type === "object") return "object";
  if (format === "uuid") return "string /*uuid*/";
  if (format === "date-time") return "string /*ISO date*/";
  if (format === "date") return "string /*date*/";
  if (format === "email") return "string /*email*/";
  switch (type) {
    case "string":
      return "string";
    case "number":
    case "integer":
      return "number";
    case "boolean":
      return "boolean";
    default:
      return "unknown";
  }
}
function capitalize(s) {
  return s.charAt(0).toUpperCase() + s.slice(1);
}
function singularize(s) {
  if (s.endsWith("ies")) return s.slice(0, -3) + "y";
  if (s.endsWith("ses")) return s.slice(0, -2);
  if (s.endsWith("s") && !s.endsWith("ss")) return s.slice(0, -1);
  return s;
}
function formatValidationError(data) {
  if (!data || typeof data !== "object") {
    return `Validation error: ${JSON.stringify(data)}`;
  }
  if (Array.isArray(data)) {
    const issues = data;
    const parts = issues.slice(0, 5).map((issue) => {
      const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
      const msg = issue.message || `expected ${issue.expected}` || issue.code || "invalid";
      return path ? `${path}: ${msg}` : msg;
    });
    if (parts.length > 0) {
      return `Validation failed \u2014 ${parts.join("; ")}. Fix the listed fields and retry.`;
    }
  }
  const obj = data;
  if (obj.fieldErrors && typeof obj.fieldErrors === "object") {
    const fieldErrors = obj.fieldErrors;
    const parts = [];
    for (const [field, messages] of Object.entries(fieldErrors)) {
      if (Array.isArray(messages) && messages.length > 0) {
        parts.push(`${field}: ${messages[0]}`);
      }
    }
    const formErrors = obj.formErrors;
    if (Array.isArray(formErrors) && formErrors.length > 0) {
      parts.push(formErrors[0]);
    }
    if (parts.length > 0) {
      return `Validation failed \u2014 ${parts.join("; ")}. Fix the listed fields and retry.`;
    }
  }
  if (obj.issues && Array.isArray(obj.issues)) {
    const issues = obj.issues;
    const parts = issues.slice(0, 5).map((issue) => {
      const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
      const msg = issue.message || issue.code || "invalid";
      return path ? `${path}: ${msg}` : msg;
    });
    return `Validation failed \u2014 ${parts.join("; ")}. Fix the listed fields and retry.`;
  }
  if (obj.error && typeof obj.error === "string") {
    const details = obj.details;
    if (details && typeof details === "object") {
      return formatValidationError(details);
    }
    return obj.error;
  }
  if (obj.message && typeof obj.message === "string") {
    return obj.message;
  }
  const json = JSON.stringify(data);
  if (json.length > 500) {
    return `Validation error (truncated): ${json.slice(0, 500)}...`;
  }
  return `Validation error: ${json}`;
}
function buildEntitySchemas(graph) {
  return graph.nodes.map((node) => {
    const relationships = graph.edges.filter((edge) => edge.source === node.className).map((edge) => ({
      relationship: edge.relationship,
      target: edge.target,
      property: edge.property,
      nullable: edge.nullable
    }));
    return {
      className: node.className,
      tableName: node.tableName,
      module: inferModuleFromEntity(node.className, node.tableName),
      fields: node.properties,
      relationships
    };
  });
}
function detectMutationInCode(code) {
  const methods = [];
  const pattern = /method:\s*['"](\w+)['"]/gi;
  let match;
  while ((match = pattern.exec(code)) !== null) {
    const method = match[1].toUpperCase();
    if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
      methods.push(method);
    }
  }
  return { hasMutation: methods.length > 0, methods };
}
async function loadCodeModeTools() {
  const commonTypes = await generateCommonTypes();
  registerSearchTool();
  registerExecuteTool(commonTypes);
  return 2;
}
function registerSearchTool() {
  registerMcpTool(
    {
      name: "search",
      description: `Query the OpenAPI spec and entity schemas. READ-ONLY, no side effects.
Globals: spec.findEndpoints(keyword), spec.describeEndpoint(path, method), spec.describeEntity(keyword), spec.paths, spec.entitySchemas.
Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common paths (companies, people, orders, quotes, products).`,
      inputSchema: z.object({
        code: z.string().describe(
          'An async arrow function that queries spec, e.g. async () => spec.paths["/api/customers/companies"]'
        )
      }),
      requiredFeatures: [...CODE_MODE_REQUIRED_FEATURES],
      handler: async (input, ctx) => {
        const codePreview = input.code.slice(0, 120).replace(/\n/g, " ");
        console.error(`[AI Usage] search: code="${codePreview}${input.code.length > 120 ? "..." : ""}"`);
        if (ctx.sessionId) {
          const cached = lookupSearchCache(ctx.sessionId, input.code);
          if (cached) {
            console.error(`[AI Usage] search: CACHE HIT (label="${cached.label}")`);
            const memoryContext2 = buildMemoryContext(ctx.sessionId);
            return {
              success: true,
              result: cached.result,
              fromCache: true,
              _memoryContext: memoryContext2
            };
          }
          const { count, exceeded } = incrementToolCallCount(ctx.sessionId);
          if (exceeded) {
            console.error(`[AI Usage] search: TOOL CALL LIMIT EXCEEDED (count=${count})`);
            return {
              success: false,
              error: "Tool call limit exceeded. Summarize what you know and respond to the user."
            };
          }
        }
        const spec = await getCodeModeSpec();
        const sandbox = createSandbox({ spec });
        const result = await sandbox.execute(input.code);
        if (result.error) {
          console.error(`[AI Usage] search: ERROR in ${result.durationMs}ms \u2014 ${result.error}`);
          return {
            success: false,
            error: result.error,
            logs: result.logs,
            durationMs: result.durationMs
          };
        }
        const truncated = truncateResult(result.result);
        console.error(`[AI Usage] search: OK in ${result.durationMs}ms \u2014 ${truncated.length} chars`);
        if (ctx.sessionId) {
          const label = buildSearchLabel(input.code);
          storeSearchResult(ctx.sessionId, input.code, truncated, label);
        }
        const memoryContext = ctx.sessionId ? buildMemoryContext(ctx.sessionId) : void 0;
        return {
          success: true,
          result: truncated,
          logs: result.logs,
          durationMs: result.durationMs,
          _memoryContext: memoryContext
        };
      }
    },
    { moduleId: "codemode" }
  );
}
function registerExecuteTool(commonTypes) {
  const typesBlock = commonTypes ? `

${commonTypes}` : "";
  registerMcpTool(
    {
      name: "execute",
      description: `Make API calls. Returns JSON.
Globals: api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }, context { tenantId, organizationId, userId }.
RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collection path with id in BODY. NEVER PUT/POST/DELETE unless user explicitly asked to change data. Before ANY write operation (POST/PUT/DELETE), you MUST use the AskUserQuestion tool to get explicit user confirmation. Do NOT just ask in text \u2014 use the tool so execution pauses until the user responds.${typesBlock}`,
      inputSchema: z.object({
        code: z.string().describe(
          'Async arrow function. For reads: async () => api.request({ method: "GET", path: "/api/customers/companies" }). For updates: async () => api.request({ method: "PUT", path: "/api/customers/companies", body: { id: "<uuid>", name: "New Name" } }). id goes in BODY not URL.'
        )
      }),
      requiredFeatures: [...CODE_MODE_REQUIRED_FEATURES],
      handler: async (input, ctx) => {
        const codePreview = input.code.slice(0, 120).replace(/\n/g, " ");
        console.error(`[AI Usage] execute: code="${codePreview}${input.code.length > 120 ? "..." : ""}" user=${ctx.userId || "unknown"}`);
        if (ctx.sessionId) {
          const { count, exceeded } = incrementToolCallCount(ctx.sessionId);
          if (exceeded) {
            console.error(`[AI Usage] execute: TOOL CALL LIMIT EXCEEDED (count=${count})`);
            return {
              success: false,
              error: "Tool call limit exceeded. Summarize what you know and respond to the user."
            };
          }
        }
        const mutationInfo = detectMutationInCode(input.code);
        const maxApiCalls = mutationInfo.hasMutation ? 20 : 50;
        if (mutationInfo.hasMutation) {
          console.error(`[AI Usage] execute: MUTATION DETECTED (${mutationInfo.methods.join(",")}) \u2014 capping API calls to ${maxApiCalls}`);
        }
        let apiCallCount = 0;
        const apiRequestFn = createApiRequestFn(ctx, () => {
          apiCallCount++;
          if (apiCallCount > maxApiCalls) {
            throw new Error(`API call limit exceeded (max ${maxApiCalls})`);
          }
        });
        const context = {
          tenantId: ctx.tenantId,
          organizationId: ctx.organizationId,
          userId: ctx.userId
        };
        const sandbox = createSandbox(
          { api: { request: apiRequestFn }, context },
          { maxApiCalls }
        );
        const result = await sandbox.execute(input.code);
        if (result.error) {
          console.error(`[AI Usage] execute: ERROR in ${result.durationMs}ms \u2014 apiCalls=${apiCallCount} \u2014 ${result.error}`);
          return {
            success: false,
            error: result.error,
            logs: result.logs,
            durationMs: result.durationMs,
            apiCallCount
          };
        }
        const truncated = truncateResult(result.result);
        console.error(`[AI Usage] execute: OK in ${result.durationMs}ms \u2014 apiCalls=${apiCallCount} \u2014 ${truncated.length} chars`);
        const memoryContext = ctx.sessionId ? buildMemoryContext(ctx.sessionId) : void 0;
        return {
          success: true,
          result: truncated,
          logs: result.logs,
          durationMs: result.durationMs,
          apiCallCount,
          _memoryContext: memoryContext
        };
      }
    },
    { moduleId: "codemode" }
  );
}
function createApiRequestFn(ctx, onCall) {
  const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:3000";
  return async (params) => {
    onCall();
    const { method, path, query, body } = params;
    const callStart = Date.now();
    const normalizedMethod = method.toUpperCase();
    const apiPath = normalizeApiRequestPath(path);
    const authorization = await authorizeCodeModeApiRequest(ctx, normalizedMethod, apiPath);
    if (!authorization.allowed) {
      const callDuration2 = Date.now() - callStart;
      console.error(
        `[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${authorization.statusCode} in ${callDuration2}ms (blocked by Code Mode RBAC)`
      );
      return {
        success: false,
        statusCode: authorization.statusCode,
        error: authorization.error,
        details: authorization.details
      };
    }
    let url = `${baseUrl}${apiPath}`;
    const queryParams = { ...query };
    if (normalizedMethod === "GET") {
      if (ctx.tenantId) queryParams.tenantId = ctx.tenantId;
      if (ctx.organizationId) queryParams.organizationId = ctx.organizationId;
    }
    if (Object.keys(queryParams).length > 0) {
      const separator = url.includes("?") ? "&" : "?";
      url += separator + new URLSearchParams(queryParams).toString();
    }
    let requestBody;
    if (["POST", "PUT", "PATCH"].includes(normalizedMethod)) {
      requestBody = { ...body };
      if (ctx.tenantId) requestBody.tenantId = ctx.tenantId;
      if (ctx.organizationId) requestBody.organizationId = ctx.organizationId;
    }
    const headers = {
      "Content-Type": "application/json"
    };
    if (ctx.apiKeySecret) headers["X-API-Key"] = ctx.apiKeySecret;
    if (ctx.tenantId) headers["X-Tenant-Id"] = ctx.tenantId;
    if (ctx.organizationId) headers["X-Organization-Id"] = ctx.organizationId;
    const response = await fetchWithTimeout(url, {
      method: normalizedMethod,
      headers,
      body: requestBody ? JSON.stringify(requestBody) : void 0,
      timeoutMs: resolveAiApiRequestTimeoutMs()
    });
    const responseText = await response.text();
    const data = tryParseJson(responseText);
    const callDuration = Date.now() - callStart;
    if (!response.ok) {
      console.error(`[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${response.status} in ${callDuration}ms`);
      if (response.status === 400) {
        return {
          success: false,
          statusCode: 400,
          error: formatValidationError(data)
        };
      }
      return {
        success: false,
        statusCode: response.status,
        error: `API error ${response.status}`,
        details: data
      };
    }
    console.error(`[AI Usage] api.request: ${normalizedMethod} ${apiPath} \u2192 ${response.status} in ${callDuration}ms (${responseText.length} bytes)`);
    if (!["GET", "HEAD", "OPTIONS"].includes(normalizedMethod)) {
      return {
        success: true,
        statusCode: response.status,
        data,
        _note: "WRITE operation performed. Only do writes when user explicitly requested data modification."
      };
    }
    return {
      success: true,
      statusCode: response.status,
      data
    };
  };
}
async function authorizeCodeModeApiRequest(ctx, method, path) {
  const normalizedMethod = method.toUpperCase();
  const normalizedPath = normalizeApiRequestPath(path);
  const endpoint = await findCodeModeApiEndpoint(normalizedMethod, normalizedPath);
  if (!endpoint) {
    return {
      allowed: false,
      statusCode: 403,
      error: `Code Mode cannot call undocumented API endpoint ${normalizedMethod} ${normalizedPath}`
    };
  }
  const rbacService = resolveRbacService(ctx);
  const requiredFeatures = endpoint.requiredFeatures ?? [];
  if (requiredFeatures.length > 0) {
    if (hasRequiredFeatures(requiredFeatures, ctx.userFeatures, ctx.isSuperAdmin, rbacService)) {
      return { allowed: true, endpoint };
    }
    return {
      allowed: false,
      statusCode: 403,
      error: `Insufficient permissions for ${normalizedMethod} ${normalizedPath}`,
      details: { requiredFeatures, operationId: endpoint.operationId }
    };
  }
  if (isUnsafeHttpMethod(normalizedMethod)) {
    return {
      allowed: false,
      statusCode: 403,
      error: `Code Mode cannot call mutation endpoint without declared required features: ${normalizedMethod} ${normalizedPath}`,
      details: { operationId: endpoint.operationId }
    };
  }
  return { allowed: true, endpoint };
}
function resolveRbacService(ctx) {
  try {
    return ctx.container.resolve("rbacService");
  } catch {
    return void 0;
  }
}
async function findCodeModeApiEndpoint(method, path) {
  const endpoints = await getApiEndpoints();
  const exactMatch = endpoints.find((endpoint) => endpoint.method === method && endpoint.path === path);
  if (exactMatch) {
    return exactMatch;
  }
  return endpoints.find((endpoint) => endpoint.method === method && matchApiEndpointPath(endpoint.path, path)) ?? null;
}
function matchApiEndpointPath(endpointPath, requestPath) {
  const normalizedEndpointPath = normalizeApiRequestPath(endpointPath);
  const normalizedRequestPath = normalizeApiRequestPath(requestPath);
  if (normalizedEndpointPath === normalizedRequestPath) {
    return true;
  }
  const endpointSegments = normalizedEndpointPath.split("/").filter(Boolean);
  const requestSegments = normalizedRequestPath.split("/").filter(Boolean);
  if (endpointSegments.length !== requestSegments.length) {
    return false;
  }
  return endpointSegments.every((segment, index) => {
    if (isPathParameterSegment(segment)) {
      return requestSegments[index].length > 0;
    }
    return segment === requestSegments[index];
  });
}
function normalizeApiRequestPath(path) {
  const [rawPath] = path.split("?");
  const normalizedPath = rawPath.startsWith("/api") ? rawPath : `/api${rawPath.startsWith("/") ? rawPath : `/${rawPath}`}`;
  if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
    return normalizedPath.slice(0, -1);
  }
  return normalizedPath;
}
function isPathParameterSegment(segment) {
  return segment.startsWith("{") && segment.endsWith("}") || segment.startsWith("[") && segment.endsWith("]") || segment.startsWith(":");
}
function isUnsafeHttpMethod(method) {
  return !["GET", "HEAD", "OPTIONS"].includes(method.toUpperCase());
}
function tryParseJson(text) {
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}
export {
  CODE_MODE_REQUIRED_FEATURES,
  authorizeCodeModeApiRequest,
  loadCodeModeTools,
  matchApiEndpointPath
};
//# sourceMappingURL=codemode-tools.js.map
