import { NextResponse } from "next/server";
import { z } from "zod";
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
import { loadAgentRegistry } from "../../../lib/agent-registry.js";
import { checkAgentPolicy } from "../../../lib/agent-policy.js";
import {
  runAiAgentText,
  resolveLoopBudgetPreset
} from "../../../lib/agent-runtime.js";
import { AgentPolicyError } from "../../../lib/agent-tools.js";
import { readBaseurlAllowlist, isBaseurlAllowlisted } from "../../../lib/baseurl-allowlist.js";
import {
  canonicalProviderId,
  hasAllowlistSnapshotRestrictions,
  intersectEffectiveAllowlistWithSnapshot,
  intersectAllowlists,
  isModelAllowedForProviderInEffective,
  isProviderAllowedInEffective,
  modelAllowlistEnvVarName,
  readAgentRuntimeOverrideAllowlist
} from "../../../lib/model-allowlist.js";
import { AiTenantModelAllowlistRepository } from "../../../data/repositories/AiTenantModelAllowlistRepository.js";
import { AiAgentRuntimeOverrideRepository } from "../../../data/repositories/AiAgentRuntimeOverrideRepository.js";
import { createConversationStorage } from "../../../lib/conversation-storage.js";
const MAX_MESSAGES = 100;
const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/;
const chatMessageSchema = z.object({
  id: z.string().min(1).max(128).optional(),
  role: z.enum(["user", "assistant", "system"]),
  content: z.string(),
  uiParts: z.array(z.unknown()).optional(),
  files: z.array(
    z.object({
      id: z.string().optional(),
      name: z.string().optional(),
      type: z.string().optional(),
      mimeType: z.string().optional(),
      size: z.number().optional()
    }).passthrough()
  ).optional()
});
const pageContextSchema = z.object({
  pageId: z.string().nullable().optional(),
  entityType: z.string().nullable().optional(),
  recordId: z.string().nullable().optional()
}).passthrough();
const chatRequestSchema = z.object({
  messages: z.array(chatMessageSchema).min(1, "messages must contain at least one message").max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),
  attachmentIds: z.array(z.string()).optional(),
  debug: z.boolean().optional(),
  pageContext: pageContextSchema.optional(),
  /**
   * Stable per-conversation id (Phase 6.2). Wins over `conversationId` when
   * both are provided. The server echoes the resolved id on the SSE
   * `loop-finish` event so clients can persist it for the next turn.
   */
  sessionId: z.string().uuid().optional(),
  /**
   * @deprecated Use `sessionId` instead.
   */
  conversationId: z.string().min(1).max(128).optional()
});
const agentQuerySchema = z.object({
  agent: z.string().regex(agentIdPattern, 'agent must match "<module>.<agent>" (lowercase, digits, underscores only)'),
  /**
   * Per-request provider override. Must match a registered + configured
   * provider id. Validated against `llmProviderRegistry` at dispatch time.
   * Rejected when the agent has `allowRuntimeOverride: false`.
   *
   * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
   */
  provider: z.string().optional(),
  /**
   * Per-request model id override. Free-form string. Logged (not rejected)
   * when not in the provider's curated `defaultModels` catalog.
   *
   * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
   */
  model: z.string().optional(),
  /**
   * Per-request base URL override. Must parse as a URL and match
   * `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated host patterns). When the
   * env var is unset or empty, any non-empty value is rejected.
   *
   * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
   */
  baseUrl: z.string().optional(),
  /**
   * Named loop-budget preset. Maps to a fixed `loop.budget` triple:
   *   tight   → maxSteps: 3,  maxWallClockMs: 10_000,  maxTokens:  50_000
   *   default → no override (agent default applies)
   *   loose   → maxSteps: 20, maxWallClockMs: 120_000, maxTokens: 500_000
   *
   * Rejected when the agent has `allowRuntimeOverride: false` or
   * `loop.allowRuntimeOverride: false`.
   *
   * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
   */
  loopBudget: z.enum(["tight", "default", "loose"]).optional()
});
const openApi = {
  tag: "AI Assistant",
  summary: "AI agent dispatcher",
  methods: {
    POST: {
      operationId: "aiAssistantChatAgent",
      summary: "Stream a chat turn for a registered AI agent",
      description: "Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, execution-mode compatibility, and attachment media-type policy. The streaming response body uses an AI SDK-compatible `text/event-stream` transport. Optional `?provider=`, `?model=`, and `?baseUrl=` query params let callers override the resolved provider/model/base-URL for this turn (Phase 4a). Provider must be registered and configured; baseUrl must match `AI_RUNTIME_BASEURL_ALLOWLIST` when set. Both are suppressed when the agent declares `allowRuntimeOverride: false`.",
      query: agentQuerySchema,
      requestBody: {
        contentType: "application/json",
        description: "Chat turn payload. `messages` is required; `attachmentIds`, `debug`, and `pageContext` are optional.",
        schema: chatRequestSchema
      },
      responses: [
        { status: 200, description: "Streaming text/event-stream response compatible with AI SDK chat transports.", mediaType: "text/event-stream" }
      ],
      errors: [
        {
          status: 400,
          description: "Invalid query param, malformed payload, or message count above the cap. Typed codes: `runtime_override_disabled` (agent has allowRuntimeOverride:false), `provider_unknown` (provider id not registered), `provider_not_configured` (provider registered but no API key in env), `baseurl_not_allowlisted` (baseUrl not in AI_RUNTIME_BASEURL_ALLOWLIST)."
        },
        { status: 401, description: "Unauthenticated caller." },
        { status: 403, description: "Caller lacks agent-level or tool-level required features." },
        { status: 404, description: "Unknown agent id." },
        { status: 409, description: "Agent/tool/execution-mode policy violation." },
        { status: 500, description: "Internal runtime failure." }
      ]
    }
  }
};
const metadata = {
  POST: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
};
function jsonError(status, message, code, extra) {
  return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
}
function statusForDenyCode(code) {
  switch (code) {
    case "agent_unknown":
      return 404;
    case "agent_features_denied":
    case "tool_features_denied":
      return 403;
    case "tool_not_whitelisted":
    case "tool_unknown":
    case "mutation_blocked_by_readonly":
    case "mutation_blocked_by_policy":
    case "execution_mode_not_supported":
      return 409;
    case "attachment_type_not_accepted":
      return 400;
    default:
      return 409;
  }
}
function extractDataPayload(eventBlock) {
  const dataLines = eventBlock.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5));
  if (dataLines.length === 0) return null;
  return dataLines.join("\n");
}
function extractUiPartsFromToolOutput(output) {
  let parsed = output;
  if (typeof output === "string") {
    const trimmed = output.trim();
    if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return [];
    try {
      parsed = JSON.parse(trimmed);
    } catch {
      return [];
    }
  }
  if (!parsed || typeof parsed !== "object") return [];
  const value = parsed;
  const parts = [];
  if (value.status === "pending-confirmation" || value.status === "awaiting-confirmation") {
    const pendingActionId = typeof value.pendingActionId === "string" && value.pendingActionId.length > 0 ? value.pendingActionId : null;
    if (pendingActionId) {
      parts.push({
        componentId: "mutation-preview-card",
        pendingActionId,
        payload: {
          pendingActionId,
          expiresAt: typeof value.expiresAt === "string" ? value.expiresAt : void 0,
          agentId: typeof value.agentId === "string" ? value.agentId : typeof value.agent === "string" ? value.agent : void 0,
          toolName: typeof value.toolName === "string" ? value.toolName : void 0
        }
      });
    }
  }
  if (value.uiPart && typeof value.uiPart === "object") parts.push(value.uiPart);
  if (Array.isArray(value.uiParts)) parts.push(...value.uiParts);
  return parts;
}
function extractAssistantSnapshot(raw, contentType) {
  if (!contentType?.includes("event-stream")) {
    return { content: raw, uiParts: [] };
  }
  let content = "";
  const uiParts = [];
  for (const block of raw.split("\n\n")) {
    const data = extractDataPayload(block);
    if (!data || data === "[DONE]") continue;
    try {
      const parsed = JSON.parse(data);
      if (parsed.type === "text-delta" && typeof parsed.delta === "string") {
        content += parsed.delta;
      } else if (parsed.type === "text" && typeof parsed.content === "string") {
        content += parsed.content;
      } else if (parsed.type === "tool-output-available") {
        uiParts.push(...extractUiPartsFromToolOutput(parsed.output));
      }
    } catch {
    }
  }
  return { content, uiParts };
}
async function persistChatTurnStart(input) {
  if (!input.tenantId || !input.conversationId) return null;
  const repo = createConversationStorage(input.container);
  const ctx = {
    tenantId: input.tenantId,
    organizationId: input.organizationId ?? null,
    userId: input.userId
  };
  await repo.createOrGet(
    {
      conversationId: input.conversationId,
      agentId: input.agentId,
      pageContext: input.pageContext ?? null
    },
    ctx
  );
  const userMessage = [...input.messages].reverse().find((message) => message.role === "user");
  if (!userMessage) return { conversationId: input.conversationId, userClientMessageId: null };
  await repo.appendMessage(
    input.conversationId,
    {
      clientMessageId: userMessage.id,
      role: "user",
      content: userMessage.content,
      uiParts: userMessage.uiParts,
      attachmentIds: input.attachmentIds,
      files: userMessage.files?.map((file, index) => {
        const id = file.id ?? input.attachmentIds?.[index];
        const mimeType = file.mimeType ?? file.type;
        return {
          ...id ? { id } : {},
          ...file.name ? { name: file.name } : {},
          ...mimeType ? { mimeType } : {},
          ...typeof file.size === "number" ? { size: file.size } : {}
        };
      })
    },
    ctx
  );
  return {
    conversationId: input.conversationId,
    userClientMessageId: userMessage.id ?? null
  };
}
function persistAssistantOnStreamCompletion(input) {
  if (!input.response.body || !input.tenantId) return input.response;
  const tenantId = input.tenantId;
  const { readable, writable } = new TransformStream();
  const writer = writable.getWriter();
  const decoder = new TextDecoder();
  const contentType = input.response.headers.get("content-type");
  async function pump() {
    const reader = input.response.body.getReader();
    let raw = "";
    try {
      for (; ; ) {
        const { value, done } = await reader.read();
        if (done) break;
        if (!value) continue;
        raw += decoder.decode(value, { stream: true });
        await writer.write(value);
      }
      raw += decoder.decode();
      const assistant = extractAssistantSnapshot(raw, contentType);
      if (assistant.content.trim() || assistant.uiParts.length > 0) {
        const repo = createConversationStorage(input.container);
        await repo.appendMessage(
          input.conversationId,
          {
            clientMessageId: input.userClientMessageId ? `${input.userClientMessageId}:assistant` : void 0,
            role: "assistant",
            content: assistant.content,
            uiParts: assistant.uiParts
          },
          {
            tenantId,
            organizationId: input.organizationId ?? null,
            userId: input.userId
          }
        );
      }
    } catch (error) {
      console.error("[AI Chat Agent] Conversation persistence failure:", error);
    } finally {
      reader.releaseLock();
      await writer.close().catch(() => void 0);
    }
  }
  void pump();
  return new Response(readable, {
    status: input.response.status,
    statusText: input.response.statusText,
    headers: input.response.headers
  });
}
async function POST(req) {
  const auth = await getAuthFromRequest(req);
  if (!auth) {
    return jsonError(401, "Unauthorized", "unauthenticated");
  }
  const requestUrl = new URL(req.url);
  const queryResult = agentQuerySchema.safeParse({
    agent: requestUrl.searchParams.get("agent") ?? void 0,
    provider: requestUrl.searchParams.get("provider") ?? void 0,
    model: requestUrl.searchParams.get("model") ?? void 0,
    baseUrl: requestUrl.searchParams.get("baseUrl") ?? void 0,
    loopBudget: requestUrl.searchParams.get("loopBudget") ?? void 0
  });
  if (!queryResult.success) {
    return jsonError(400, 'Invalid or missing "agent" query parameter.', "validation_error", {
      issues: queryResult.error.issues
    });
  }
  const agentId = queryResult.data.agent;
  const rawProvider = queryResult.data.provider;
  const rawModel = queryResult.data.model;
  const rawBaseUrl = queryResult.data.baseUrl;
  const rawLoopBudget = queryResult.data.loopBudget;
  let parsedBody;
  try {
    parsedBody = await req.json();
  } catch {
    return jsonError(400, "Request body must be valid JSON.", "validation_error");
  }
  const bodyResult = chatRequestSchema.safeParse(parsedBody);
  if (!bodyResult.success) {
    return jsonError(400, "Invalid request body.", "validation_error", {
      issues: bodyResult.error.issues
    });
  }
  try {
    await loadAgentRegistry();
    const container = await createRequestContainer();
    const rbacService = container.resolve("rbacService");
    const acl = await rbacService.loadAcl(auth.sub, {
      tenantId: auth.tenantId,
      organizationId: auth.orgId
    });
    const decision = checkAgentPolicy({
      agentId,
      authContext: {
        userFeatures: acl.features,
        isSuperAdmin: acl.isSuperAdmin
      },
      requestedExecutionMode: "chat",
      // TODO(step-3.7): resolve attachmentIds -> media types via attachment-bridge
      // once the attachment-to-model conversion bridge lands. Until then the
      // policy gate skips attachment-type validation because media types are
      // not known at dispatch time.
      attachmentMediaTypes: void 0
    });
    if (!decision.ok) {
      return jsonError(statusForDenyCode(decision.code), decision.message, decision.code);
    }
    const agentDef = decision.agent;
    const hasRuntimeOverride = rawProvider && rawProvider.trim().length > 0 || rawModel && rawModel.trim().length > 0 || rawBaseUrl && rawBaseUrl.trim().length > 0 || rawLoopBudget !== void 0 && rawLoopBudget !== "default";
    const runtimeOverrideAllowed = agentDef.allowRuntimeOverride !== false && agentDef.allowRuntimeModelOverride !== false;
    if (hasRuntimeOverride && !runtimeOverrideAllowed) {
      return jsonError(
        400,
        `Agent "${agentId}" has runtime override disabled (allowRuntimeOverride: false).`,
        "runtime_override_disabled"
      );
    }
    let tenantAllowlistSnapshot = null;
    let agentRuntimeOverrideAllowlist = null;
    if (auth.tenantId) {
      try {
        const em = container.resolve("em");
        const allowlistRepo = new AiTenantModelAllowlistRepository(em);
        tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({
          tenantId: auth.tenantId,
          organizationId: auth.orgId ?? null
        });
        const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em);
        const agentRuntimeOverrideRow = await runtimeOverrideRepo.getExact({
          tenantId: auth.tenantId,
          organizationId: auth.orgId ?? null,
          agentId
        });
        const tenantAgentAllowlist = agentRuntimeOverrideRow ? {
          allowedProviders: agentRuntimeOverrideRow.allowedOverrideProviders ?? null,
          allowedModelsByProvider: agentRuntimeOverrideRow.allowedOverrideModelsByProvider ?? {}
        } : null;
        agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist) ? tenantAgentAllowlist : null;
      } catch (snapshotError) {
        console.error(
          "[AI Chat Agent] Tenant allowlist lookup failed; refusing to dispatch:",
          snapshotError
        );
        return jsonError(
          503,
          "Tenant allowlist is temporarily unavailable. Try again shortly.",
          "tenant_allowlist_unavailable"
        );
      }
    }
    const knownProviderIds = llmProviderRegistry.list().map((p) => p.id);
    const baseEffectiveAllowlist = intersectAllowlists(
      process.env,
      knownProviderIds,
      tenantAllowlistSnapshot
    );
    const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(
      process.env,
      agentId,
      knownProviderIds
    );
    const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(
      intersectEffectiveAllowlistWithSnapshot(
        baseEffectiveAllowlist,
        knownProviderIds,
        envAgentAllowlist
      ),
      knownProviderIds,
      agentRuntimeOverrideAllowlist
    );
    const normalizedProvider = rawProvider && rawProvider.trim().length > 0 ? canonicalProviderId(rawProvider.trim(), llmProviderRegistry.list().map((p) => p.id)) : null;
    if (rawProvider && rawProvider.trim().length > 0) {
      const providerEntry = normalizedProvider ? llmProviderRegistry.get(normalizedProvider) : null;
      if (!providerEntry) {
        return jsonError(
          400,
          `Provider "${rawProvider}" is not registered. Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(", ")}.`,
          "provider_unknown"
        );
      }
      if (!providerEntry.isConfigured()) {
        return jsonError(
          400,
          `Provider "${rawProvider}" is registered but not configured in this environment (missing API key).`,
          "provider_not_configured"
        );
      }
      if (!isProviderAllowedInEffective(effectiveAllowlist, normalizedProvider)) {
        const source = effectiveAllowlist.tenantOverridesActive ? "the effective allowlist (env \u2229 tenant)" : "OM_AI_AVAILABLE_PROVIDERS";
        return jsonError(
          400,
          `Provider "${rawProvider}" is not in ${source}.`,
          "provider_not_allowlisted"
        );
      }
      if (rawModel && rawModel.trim().length > 0 && !isModelAllowedForProviderInEffective(
        effectiveAllowlist,
        normalizedProvider,
        rawModel.trim()
      )) {
        const source = effectiveAllowlist.tenantOverridesActive ? `the effective allowlist (env \u2229 tenant) for "${normalizedProvider}"` : modelAllowlistEnvVarName(normalizedProvider);
        return jsonError(
          400,
          `Model "${rawModel}" is not in ${source}.`,
          "model_not_allowlisted"
        );
      }
    }
    if (rawBaseUrl && rawBaseUrl.trim().length > 0) {
      const allowlist = readBaseurlAllowlist();
      if (!isBaseurlAllowlisted(rawBaseUrl.trim(), allowlist)) {
        return jsonError(
          400,
          `baseUrl "${rawBaseUrl}" is not in the AI_RUNTIME_BASEURL_ALLOWLIST. Set that env var to a comma-separated list of allowed host patterns to enable per-request baseUrl overrides.`,
          "baseurl_not_allowlisted"
        );
      }
    }
    const requestOverride = hasRuntimeOverride ? {
      providerId: normalizedProvider,
      modelId: rawModel && rawModel.trim().length > 0 ? rawModel.trim() : null,
      baseURL: rawBaseUrl && rawBaseUrl.trim().length > 0 ? rawBaseUrl.trim() : null
    } : void 0;
    const loopFromPreset = rawLoopBudget !== void 0 && rawLoopBudget !== "default" ? resolveLoopBudgetPreset(rawLoopBudget) : void 0;
    const effectiveConversationId = bodyResult.data.sessionId ?? bodyResult.data.conversationId ?? null;
    let persistedTurn = null;
    try {
      persistedTurn = await persistChatTurnStart({
        container,
        tenantId: auth.tenantId ?? null,
        organizationId: auth.orgId ?? null,
        userId: auth.sub,
        agentId,
        conversationId: effectiveConversationId,
        pageContext: bodyResult.data.pageContext,
        messages: bodyResult.data.messages,
        attachmentIds: bodyResult.data.attachmentIds
      });
    } catch (error) {
      if (error instanceof Error && error.name === "AiChatConversationOrgNotFoundError") {
        return jsonError(400, error.message, "organization_not_found");
      }
      console.error("[AI Chat Agent] Failed to persist user message:", error);
    }
    const response = await runAiAgentText({
      agentId,
      messages: bodyResult.data.messages,
      attachmentIds: bodyResult.data.attachmentIds,
      pageContext: bodyResult.data.pageContext,
      debug: bodyResult.data.debug,
      sessionId: bodyResult.data.sessionId ?? null,
      conversationId: bodyResult.data.conversationId ?? null,
      authContext: {
        tenantId: auth.tenantId ?? null,
        organizationId: auth.orgId ?? null,
        userId: auth.sub,
        features: acl.features,
        isSuperAdmin: acl.isSuperAdmin
      },
      container,
      requestOverride,
      loop: loopFromPreset,
      emitLoopTrace: true
    });
    if (!persistedTurn) return response;
    return persistAssistantOnStreamCompletion({
      response,
      container,
      tenantId: auth.tenantId ?? null,
      organizationId: auth.orgId ?? null,
      userId: auth.sub,
      conversationId: persistedTurn.conversationId,
      userClientMessageId: persistedTurn.userClientMessageId
    });
  } catch (error) {
    if (error instanceof AgentPolicyError) {
      return jsonError(statusForDenyCode(error.code), error.message, error.code);
    }
    console.error("[AI Chat Agent] Dispatch failure:", error);
    return jsonError(
      500,
      error instanceof Error ? error.message : "Agent dispatch failed.",
      "internal_error"
    );
  }
}
export {
  POST,
  metadata,
  openApi
};
//# sourceMappingURL=route.js.map
