import { NextResponse } from "next/server";
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
import {
  handleOpenCodeMessageStreaming
} from "../../lib/opencode-handlers.js";
import { createOpenCodeClient } from "../../lib/opencode-client.js";
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
import {
  generateSessionToken,
  createSessionApiKey
} from "@open-mercato/core/modules/api_keys/services/apiKeyService";
import { UserRole } from "@open-mercato/core/modules/auth/data/entities";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
const CHAT_SYSTEM_INSTRUCTIONS = `
You are a helpful business assistant for Open Mercato.

\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
ABSOLUTE RULES \u2014 FOLLOW THESE OR BE CUT OFF
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1. READ = GET only. If the user says find/list/show/search/get \u2192 use only GET. NEVER call PUT/POST/DELETE for a read query.
2. PUT path = collection path. id goes in the BODY, not the URL. Example: PUT /api/customers/companies with { id: '...', name: 'New' }. There are NO /{id} path segments.
3. Confirm before ANY write. Before POST/PUT/DELETE: present your plan in business language, then STOP and wait for user to say "yes". Do NOT execute the write in the same turn.
4. Maximum 4 tool calls per message. Hard limit is 10.

You have 2 tools \u2014 both accept a "code" parameter with an async JavaScript arrow function.

TOOL: search \u2014 discover endpoints and schemas (READ-ONLY, fast)
  - spec.findEndpoints(keyword) \u2192 [{ path, methods }]
  - spec.describeEndpoint(path, method) \u2192 COMPACT: { requiredFields, optionalFields, nestedCollections, example, relatedEndpoints, relatedEntity }
  - spec.describeEntity(keyword) \u2192 { className, fields, relationships }

TOOL: execute \u2014 make API calls (reads and writes)
  - api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }

COMMON API PATHS (use directly \u2014 do NOT call findEndpoints for these):
  /api/customers/companies      \u2014 companies (GET list, POST create, PUT update)
  /api/customers/people         \u2014 contacts/people
  /api/customers/deals          \u2014 deals/opportunities
  /api/customers/activities     \u2014 activities/tasks
  /api/sales/orders             \u2014 sales orders
  /api/sales/quotes             \u2014 quotes
  /api/sales/invoices           \u2014 invoices
  /api/catalog/products         \u2014 products
  /api/catalog/categories       \u2014 categories

\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
RECIPES \u2014 follow EXACTLY for each task type
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550

FIND/LIST records (1 call):
  For COMMON PATHS: skip describeEndpoint, go straight to execute.
  1. execute: api.request({ method: 'GET', path: '/api/<module>/<resource>' })
  The "search" query param only matches indexed text fields \u2014 it will NOT match concepts like "Polish" or "large".
  For conceptual/subjective queries, fetch ALL records and use YOUR reasoning to identify matches from the returned data.
  For unknown paths: 1 search + 1 execute.

UPDATE a record (3-4 calls):
  1. search: spec.describeEndpoint('/api/<module>/<resource>', 'PUT')  \u2192 learn requestBody fields AND relatedEntity
  2. execute: GET the record \u2192 find it, get its ID
  3. execute: PUT to the COLLECTION path with id IN THE BODY:
     api.request({ method: 'PUT', path: '/api/<module>/<resource>', body: { id: '<uuid>', ...changes } })
  NOTE: All CRUD endpoints use the COLLECTION path. The id goes in the request BODY, not the URL. There are NO /{id} path segments.

CREATE a record (2-3 calls):
  1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 gives requiredFields, optionalFields, nestedCollections, and a working example
  2. Ask user for confirmation with the field values
  3. execute: api.request({ method: 'POST', ...body })
  If the endpoint has nestedCollections (like lines), include them INLINE in the body \u2014 do NOT create them separately.
  Use the "example" from describeEndpoint as your template \u2014 fill in real values.
  Example \u2014 create a quote with line items:
    api.request({ method: 'POST', path: '/api/sales/quotes', body: {
      currencyCode: 'EUR', customerEntityId: '<company-uuid>',
      lines: [{ currencyCode: 'EUR', quantity: 1, productId: '<product-uuid>', name: 'Product Name', kind: 'product' }]
    }})
  Do NOT create lines separately. Do NOT include id, quoteId, or total fields \u2014 the server generates them.

CREATE MULTIPLE records (2-3 calls):
  1. search: spec.describeEndpoint('/api/<module>/<resource>', 'POST') \u2192 learn fields + example
  2. execute: loop in one call:
     async () => {
       const results = [];
       for (const item of items) {
         results.push(await api.request({ method: 'POST', path: '...', body: item }));
       }
       return results;
     }

DISCOVER (what endpoints/entities exist) (1 call):
  1. search: spec.findEndpoints('<keyword>') or spec.describeEntity('<keyword>')

\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
HARD RULES
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
- MAXIMUM 4 tool calls per user message. You WILL be cut off after 10.
- NEVER call findEndpoints or describeEndpoint for COMMON PATHS listed above \u2014 use them directly with execute.
- NEVER call describeEntity if describeEndpoint already returned relatedEntity.
- NEVER repeat a search from earlier in the conversation \u2014 reuse previous results.
- NEVER make N+1 API calls (1 call per record). Fetch a list and reason about the results yourself.
- When you already have the data you need from a previous call, use it \u2014 do NOT fetch more data to "enrich" it.
- Do NOT write JavaScript filters/regex to match records. Fetch data with a simple api.request() call and use YOUR knowledge to interpret the results.
- The "search" query param is fulltext only \u2014 it won't match nationalities, categories, or subjective criteria. For those, fetch all and reason.
- describeEndpoint returns a COMPACT summary with requiredFields, optionalFields, and an example. Use the example as your template \u2014 fill in real values and send it.
- For fields you don't know, OMIT them \u2014 the API uses defaults for optional fields.
- NEVER try to set computed/total fields (amounts, totals, counts) \u2014 the server calculates them.
- For updates: describeEndpoint gives you the field names. Go straight to GET + PUT. PUT path is the COLLECTION path, id in BODY.
- For creates with children (e.g. quote + lines): include children INLINE in the body using the nestedCollections field name.

RESPONSE RULES:
- Be proactive \u2014 fetch data and present results, don't ask what the user wants to see.
- Never show technical terms, IDs, JSON, or internal reasoning.
- Present results in clean business language with **bold names** and bullet points.
- Only ask for confirmation before create/update/delete operations.
`.trim();
const openApi = {
  tag: "AI Assistant",
  summary: "AI chat",
  methods: {
    POST: { summary: "Send message to AI agent via SSE stream" }
  }
};
const metadata = {
  POST: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
};
async function getUserRoleIds(em, userId, tenantId) {
  if (!tenantId) return [];
  const links = await findWithDecryption(
    em,
    UserRole,
    { user: userId, role: { tenantId } },
    { populate: ["role"] },
    { tenantId, organizationId: null }
  );
  const linkList = Array.isArray(links) ? links : [];
  return linkList.map((l) => l.role?.id).filter((id) => typeof id === "string" && id.length > 0);
}
async function POST(req) {
  const auth = await getAuthFromRequest(req);
  if (!auth) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  try {
    const body = await req.json();
    const { messages, sessionId, answerQuestion } = body;
    const encoder = new TextEncoder();
    const stream = new TransformStream();
    const writer = stream.writable.getWriter();
    let writerClosed = false;
    const writeSSE = async (event) => {
      if (writerClosed) return;
      try {
        const jsonStr = JSON.stringify(event);
        await writer.write(encoder.encode(`data: ${jsonStr}

`));
      } catch (err) {
        console.warn("[AI Chat] Failed to write SSE event:", event.type);
      }
    };
    const closeWriter = async () => {
      if (writerClosed) return;
      writerClosed = true;
      try {
        await writer.close();
      } catch {
      }
    };
    if (answerQuestion) {
      try {
        const client = createOpenCodeClient();
        await client.answerQuestion(answerQuestion.questionId, answerQuestion.answer);
        return NextResponse.json({ success: true });
      } catch (error) {
        console.error("[AI Chat] Answer error:", error);
        return NextResponse.json(
          { error: error instanceof Error ? error.message : "Failed to answer question" },
          { status: 500 }
        );
      }
    }
    if (!messages || !Array.isArray(messages)) {
      return NextResponse.json({ error: "messages array is required" }, { status: 400 });
    }
    const lastUserMessage = messages.filter((m) => m.role === "user").pop()?.content;
    if (!lastUserMessage) {
      return NextResponse.json({ error: "No user message found" }, { status: 400 });
    }
    const chatStartTime = Date.now();
    const messagePreview = lastUserMessage.slice(0, 80).replace(/\n/g, " ");
    console.error(`[AI Usage] Chat request: user=${auth.sub} session=${sessionId ? sessionId.slice(0, 16) + "..." : "new"} message="${messagePreview}${lastUserMessage.length > 80 ? "..." : ""}"`);
    let sessionToken = null;
    if (!sessionId) {
      try {
        const container = await createRequestContainer();
        const em = container.resolve("em");
        const userRoleIds = await getUserRoleIds(em, auth.sub, auth.tenantId);
        sessionToken = generateSessionToken();
        await createSessionApiKey(em, {
          sessionToken,
          userId: auth.sub,
          userRoles: userRoleIds,
          tenantId: auth.tenantId,
          organizationId: auth.orgId,
          ttlMinutes: 120
        });
        console.log("[AI Chat] Created session token:", sessionToken.slice(0, 12) + "...");
      } catch (error) {
        console.error("[AI Chat] Failed to create session key:", error);
      }
    }
    let messageToSend = "";
    if (!sessionId) {
      messageToSend = `${CHAT_SYSTEM_INSTRUCTIONS}

`;
    }
    if (sessionToken) {
      messageToSend += `[Session Authorization: ${sessionToken}. Include "_sessionToken": "${sessionToken}" in EVERY tool call.]

`;
    }
    messageToSend += lastUserMessage;
    (async () => {
      let toolCallCount = 0;
      let lastTokens;
      let resultSessionId;
      try {
        if (sessionToken) {
          console.log("[AI Chat] Emitting session-authorized event");
          await writeSSE({
            type: "session-authorized",
            sessionToken: sessionToken.slice(0, 12) + "..."
          });
        }
        await writeSSE({ type: "thinking" });
        await handleOpenCodeMessageStreaming(
          {
            message: messageToSend,
            sessionId
          },
          async (event) => {
            if (event.type === "tool-call") toolCallCount++;
            if (event.type === "metadata" && "tokens" in event) lastTokens = event.tokens;
            if (event.type === "done" && "sessionId" in event) resultSessionId = event.sessionId;
            await writeSSE(event);
          }
        );
      } catch (error) {
        console.error("[AI Chat] OpenCode error:", error);
        await writeSSE({
          type: "error",
          error: error instanceof Error ? error.message : "OpenCode request failed"
        });
      } finally {
        const durationMs = Date.now() - chatStartTime;
        console.error(`[AI Usage] Chat complete: user=${auth.sub} session=${(resultSessionId || sessionId || "unknown").slice(0, 16)}... duration=${durationMs}ms toolCalls=${toolCallCount}${lastTokens ? ` tokens={in:${lastTokens.input || 0},out:${lastTokens.output || 0}}` : ""}`);
        await closeWriter();
      }
    })();
    return new Response(stream.readable, {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        Connection: "keep-alive"
      }
    });
  } catch (error) {
    console.error("[AI Chat] Error:", error);
    return NextResponse.json({ error: "Chat request failed" }, { status: 500 });
  }
}
export {
  POST,
  metadata,
  openApi
};
//# sourceMappingURL=route.js.map
