import { createHash } from "node:crypto";
import type { FastifyInstance } from "fastify";
import type { MultipartFile } from "@fastify/multipart";
import { z } from "zod";
import { env } from "../../env.js";
import { requireSession } from "../../lib/auth.js";
import { writeAuditLog } from "../../lib/audit.js";
import { extractDocumentText } from "../../lib/document-reviews.js";
import { createId } from "../../lib/id.js";
import {
  createRepositoryDocumentReprocessJob,
  getRepositoryDocumentReprocessJobDetail,
  listRepositoryDocumentReprocessJobs,
  pauseRepositoryDocumentReprocessJob,
  resumePendingRepositoryDocumentReprocessJobs,
  resumeRepositoryDocumentReprocessJob,
  startRepositoryDocumentReprocessJob,
} from "../../lib/repository-document-reprocess-jobs.js";
import {
  addRepositoryFolderUploadJobFile,
  createRepositoryFolderUploadJob,
  getRepositoryFolderUploadJobDetail,
  listRepositoryFolderUploadJobs,
  resumePendingRepositoryFolderUploadJobs,
  stageRepositoryFolderUploadFile,
  startRepositoryFolderUploadJob,
} from "../../lib/repository-folder-upload-jobs.js";
import {
  attachKommoOutboundDeliveryToRepositoryItem,
  dispatchKommoConversationReply,
  mirrorRepositoryMessageToKommoLead,
} from "../../lib/kommo.js";
import { consolidateCaseFacts, createDocumentAndExtraction } from "../../lib/intelligence.js";
import { createConversation, createRepositoryItem, storeConversationMessage } from "../../lib/repository.js";
import { prisma } from "../../lib/prisma.js";
import { getSessionProfile } from "../../lib/session.js";
import { saveBinaryFile } from "../../lib/storage.js";
import { createAiRun, finishAiRun, runJsonChatCompletion } from "../../lib/tenant-ai.js";

const messageSchema = z.object({
  clientId: z.string().uuid(),
  caseId: z.string().uuid().optional().nullable(),
  channelCode: z.enum(["email", "whatsapp", "internal"]),
  subject: z.string().max(255).optional().or(z.literal("")),
  bodyText: z.string().min(1),
  senderName: z.string().max(255).optional().or(z.literal("")),
  senderAddress: z.string().max(255).optional().or(z.literal("")),
  recipientAddress: z.string().max(255).optional().or(z.literal("")),
  directionCode: z.enum(["inbound", "outbound"]).default("inbound"),
  conversationId: z.string().uuid().optional().nullable(),
});

const callLogSchema = z.object({
  clientId: z.string().uuid(),
  caseId: z.string().uuid().optional().nullable(),
  fromNumber: z.string().max(50).optional().or(z.literal("")),
  toNumber: z.string().max(50).optional().or(z.literal("")),
  directionCode: z.enum(["inbound", "outbound"]).default("inbound"),
  durationSeconds: z.coerce.number().int().nonnegative().default(0),
  transcriptText: z.string().min(1),
});

const querySchema = z.object({
  clientId: z.string().uuid().optional(),
  caseId: z.string().uuid().optional(),
});

const folderUploadJobListQuerySchema = z.object({
  clientId: z.string().uuid().optional(),
  caseId: z.string().uuid().optional(),
  limit: z.coerce.number().int().positive().max(10).optional(),
});

const folderUploadJobParamsSchema = z.object({
  jobId: z.string().uuid(),
});

const repositoryDocumentReprocessCreateSchema = z.object({
  clientId: z.string().uuid(),
  caseId: z.string().uuid().optional().nullable(),
});

const repositoryDocumentReprocessJobListQuerySchema = z.object({
  clientId: z.string().uuid().optional(),
  caseId: z.string().uuid().optional(),
  limit: z.coerce.number().int().positive().max(10).optional(),
});

const repositoryDocumentReprocessJobParamsSchema = z.object({
  jobId: z.string().uuid(),
});

const folderUploadMultipartLimits = {
  files: env.API_MULTIPART_FILE_COUNT_LIMIT,
  parts: env.API_MULTIPART_PARTS_LIMIT,
  fileSize: env.API_MULTIPART_FILE_LIMIT_MB * 1024 * 1024,
} as const;

const knowledgeDocumentIndexingResultSchema = z.object({
  synopsis: z.string().optional(),
  searchableSummary: z.string().optional(),
  documentClass: z.string().optional(),
  keywords: z.array(z.unknown()).optional(),
  legalTopics: z.array(z.unknown()).optional(),
  citedEntities: z.array(z.unknown()).optional(),
  jurisdictions: z.array(z.unknown()).optional(),
  useCases: z.array(z.unknown()).optional(),
  suggestedQuestions: z.array(z.unknown()).optional(),
  confidence: z.union([z.number(), z.string()]).optional(),
});

function getMultipartFieldValue(
  fields: Record<string, MultipartFile["fields"][string]>,
  key: string,
): string {
  const field = fields[key];

  if (!field || Array.isArray(field) || !("value" in field)) {
    return "";
  }

  return String(field.value ?? "");
}

function isKnowledgeDocumentMimeAllowed(input: {
  fileName: string;
  mimeType: string;
}) {
  const lowerFileName = input.fileName.toLowerCase();
  const mimeType = String(input.mimeType ?? "").toLowerCase();

  return (
    mimeType === "application/pdf" ||
    mimeType.startsWith("image/") ||
    lowerFileName.endsWith(".pdf") ||
    lowerFileName.endsWith(".png") ||
    lowerFileName.endsWith(".jpg") ||
    lowerFileName.endsWith(".jpeg") ||
    lowerFileName.endsWith(".webp") ||
    lowerFileName.endsWith(".gif") ||
    lowerFileName.endsWith(".bmp") ||
    lowerFileName.endsWith(".tif") ||
    lowerFileName.endsWith(".tiff")
  );
}

function getKnowledgeRepositoryItemTypeCode(mimeType: string) {
  return String(mimeType ?? "").toLowerCase().startsWith("image/") ? "image" : "document";
}

function buildKnowledgeDocumentRepositoryBody(input: {
  title: string;
  objective: string;
  extractedText?: string | null;
  aiIndex?: {
    synopsis: string;
    searchableSummary: string;
    documentClass: string;
    keywords: string[];
    legalTopics: string[];
    citedEntities: string[];
    jurisdictions: string[];
    useCases: string[];
    suggestedQuestions: string[];
  } | null;
}) {
  const parts = [`Título: ${input.title}.`, `Objetivo: ${input.objective}.`];
  const aiIndex = input.aiIndex;

  if (aiIndex) {
    if (aiIndex.synopsis) {
      parts.push(`Resumo de indexação AI: ${aiIndex.synopsis}`);
    }

    if (aiIndex.searchableSummary) {
      parts.push(`Resumo expandido para busca: ${aiIndex.searchableSummary}`);
    }

    if (aiIndex.documentClass) {
      parts.push(`Classe documental estimada: ${aiIndex.documentClass}`);
    }

    if (aiIndex.keywords.length) {
      parts.push(`Palavras-chave: ${aiIndex.keywords.join(", ")}.`);
    }

    if (aiIndex.legalTopics.length) {
      parts.push(`Tópicos jurídicos relacionados: ${aiIndex.legalTopics.join(", ")}.`);
    }

    if (aiIndex.citedEntities.length) {
      parts.push(`Entidades citadas: ${aiIndex.citedEntities.join(", ")}.`);
    }

    if (aiIndex.jurisdictions.length) {
      parts.push(`Jurisdicionalidade/contexto geográfico: ${aiIndex.jurisdictions.join(", ")}.`);
    }

    if (aiIndex.useCases.length) {
      parts.push(`Cenários ideais de uso: ${aiIndex.useCases.join(" | ")}.`);
    }

    if (aiIndex.suggestedQuestions.length) {
      parts.push(
        `Perguntas que este material ajuda a responder: ${aiIndex.suggestedQuestions.join(" | ")}.`,
      );
    }
  }

  const extractedText = String(input.extractedText ?? "").trim();

  if (extractedText) {
    parts.push(`Conteúdo extraído do documento: ${extractedText}`);
  }

  return parts.join("\n\n");
}

function truncateKnowledgeIndexText(value: string | null | undefined, maxLength = 18000) {
  const normalized = String(value ?? "").replace(/\s+/g, " ").trim();

  if (!normalized) {
    return "";
  }

  return normalized.length > maxLength
    ? `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`
    : normalized;
}

function sanitizeKnowledgeIndexString(value: unknown, maxLength: number) {
  const normalized = String(value ?? "").replace(/\s+/g, " ").trim();

  if (!normalized) {
    return "";
  }

  return normalized.length > maxLength
    ? `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`
    : normalized;
}

function sanitizeKnowledgeIndexList(value: unknown, limit: number, itemMaxLength = 120) {
  if (!Array.isArray(value)) {
    return [];
  }

  return Array.from(
    new Set(
      value
        .map((item) => sanitizeKnowledgeIndexString(item, itemMaxLength))
        .filter(Boolean),
    ),
  ).slice(0, limit);
}

function sanitizeKnowledgeIndexConfidence(value: unknown) {
  const normalized = Number(value);

  if (!Number.isFinite(normalized)) {
    return 0.5;
  }

  return Math.min(1, Math.max(0, normalized));
}

function isMissingAiConfigurationError(error: unknown) {
  const message = error instanceof Error ? error.message : String(error ?? "");

  return (
    message.includes("No active AI credential configured") ||
    message.includes("Active AI credential is missing or inactive")
  );
}

async function indexKnowledgeDocumentWithAi(input: {
  lawFirmId: string;
  repositoryItemId: string;
  title: string;
  objective: string;
  fileName: string;
  mimeType: string;
  extractedText: string;
}) {
  const aiRun = await createAiRun({
    lawFirmId: input.lawFirmId,
    runType: "classification",
    status: "running",
  });

  try {
    const completion = await runJsonChatCompletion({
      lawFirmId: input.lawFirmId,
      systemPrompt: [
        "Você indexa documentos jurídicos para uma base de conhecimento usada por agentes de IA.",
        "Responda sempre em JSON.",
        "Não invente fatos ausentes no material.",
        "Gere metadados úteis para futuras buscas e leituras.",
        'Use exatamente este formato: {"synopsis":"", "searchableSummary":"", "documentClass":"", "keywords":[], "legalTopics":[], "citedEntities":[], "jurisdictions":[], "useCases":[], "suggestedQuestions":[], "confidence":0}.',
      ].join(" "),
      userPrompt: JSON.stringify({
        title: input.title,
        objective: input.objective,
        fileName: input.fileName,
        mimeType: input.mimeType,
        extractedText: truncateKnowledgeIndexText(input.extractedText, 14000),
        instructions: [
          "synopsis: resumo curto do valor do documento",
          "searchableSummary: resumo mais rico para ajudar buscas futuras",
          "documentClass: classe/tipo documental mais provável",
          "keywords: termos curtos de busca",
          "legalTopics: temas jurídicos relacionados",
          "citedEntities: pessoas, empresas, órgãos, locais ou documentos relevantes citados",
          "jurisdictions: países, estados, cidades, órgãos ou contextos geográficos relevantes",
          "useCases: cenários ideais de uso deste documento por agentes",
          "suggestedQuestions: perguntas que esse documento ajuda a responder",
          "confidence: número entre 0 e 1",
        ],
      }),
      maxCompletionTokens: 1400,
    });

    const parsed = knowledgeDocumentIndexingResultSchema.parse(completion.json);
    const normalized = {
      synopsis: sanitizeKnowledgeIndexString(parsed.synopsis, 480),
      searchableSummary: sanitizeKnowledgeIndexString(parsed.searchableSummary, 1800),
      documentClass: sanitizeKnowledgeIndexString(parsed.documentClass, 160),
      keywords: sanitizeKnowledgeIndexList(parsed.keywords, 18, 80),
      legalTopics: sanitizeKnowledgeIndexList(parsed.legalTopics, 14, 120),
      citedEntities: sanitizeKnowledgeIndexList(parsed.citedEntities, 18, 160),
      jurisdictions: sanitizeKnowledgeIndexList(parsed.jurisdictions, 10, 120),
      useCases: sanitizeKnowledgeIndexList(parsed.useCases, 10, 180),
      suggestedQuestions: sanitizeKnowledgeIndexList(parsed.suggestedQuestions, 8, 220),
      confidence: sanitizeKnowledgeIndexConfidence(parsed.confidence),
    };

    await finishAiRun({
      aiRunId: aiRun.id,
      status: "completed",
      inputTokens: completion.usage.inputTokens,
      outputTokens: completion.usage.outputTokens,
    });

    return {
      status: "completed" as const,
      aiRunId: aiRun.id,
      model: completion.model,
      usage: completion.usage,
      index: normalized,
      errorMessage: null,
    };
  } catch (error) {
    await finishAiRun({
      aiRunId: aiRun.id,
      status: "failed",
      errorMessage: error instanceof Error ? error.message : "Knowledge document indexing failed",
    });
    throw error;
  }
}

export async function registerRepositoryRoutes(app: FastifyInstance) {
  app.get("/items", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const query = querySchema.parse(request.query);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const items = await prisma.$queryRaw<
      Array<{
        id: string;
        item_type_code: string;
        channel_code: string | null;
        subject: string | null;
        body_text: string | null;
        occurred_at: Date;
      }>
    >`
      SELECT id, item_type_code, channel_code, subject, body_text, occurred_at
      FROM repository_items
      WHERE law_firm_id = ${profile.lawFirm.id}
        AND (${query.clientId ?? null} IS NULL OR client_id = ${query.clientId ?? null})
        AND (${query.caseId ?? null} IS NULL OR case_id = ${query.caseId ?? null})
      ORDER BY occurred_at DESC
      LIMIT 200
    `;

    return items.map((item) => ({
      id: item.id,
      itemTypeCode: item.item_type_code,
      channelCode: item.channel_code,
      subject: item.subject,
      bodyText: item.body_text,
      occurredAt: item.occurred_at,
    }));
  });

  app.get("/knowledge-documents", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const documents = await prisma.$queryRaw<
      Array<{
        repository_item_id: string;
        title: string;
        objective: string | null;
        body_text: string | null;
        item_type_code: string;
        created_at: Date;
        file_id: string;
        file_name: string;
        mime_type: string;
        size_bytes: number;
      }>
    >`
      SELECT
        ri.id AS repository_item_id,
        ri.subject AS title,
        ri.summary_text AS objective,
        ri.body_text,
        ri.item_type_code,
        ri.created_at,
        f.id AS file_id,
        f.original_file_name AS file_name,
        f.mime_type,
        f.size_bytes
      FROM repository_items ri
      INNER JOIN files f ON f.repository_item_id = ri.id
      WHERE ri.law_firm_id = ${profile.lawFirm.id}
        AND ri.source_entity_type = 'knowledge_base_document'
      ORDER BY ri.created_at DESC
      LIMIT 200
    `;

    return documents.map((document) => ({
      id: document.repository_item_id,
      title: document.title,
      objective: document.objective,
      textContent: document.body_text,
      itemTypeCode: document.item_type_code,
      createdAt: document.created_at,
      file: {
        id: document.file_id,
        fileName: document.file_name,
        mimeType: document.mime_type,
        sizeBytes: Number(document.size_bytes ?? 0),
      },
    }));
  });

  app.post("/messages", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const payload = messageSchema.parse(request.body);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const itemTypeCode =
      payload.channelCode === "email"
        ? "email"
        : payload.channelCode === "whatsapp"
          ? "whatsapp_message"
          : "note";
    const dispatchResult =
      payload.directionCode === "outbound" &&
      payload.channelCode === "whatsapp" &&
      payload.conversationId
        ? await dispatchKommoConversationReply({
            lawFirmId: profile.lawFirm.id,
            conversationId: payload.conversationId,
            bodyText: payload.bodyText,
            auditContext: {
              officeId: profile.user.primaryOfficeId ?? null,
              actorUserId: profile.user.id,
              request,
            },
          })
        : null;

    if (dispatchResult?.isKommoConversation && dispatchResult.messageStatus === "kommo_reply_unavailable") {
      throw reply.conflict(
        "Esta conversa do Kommo ainda não tem return_url do Salesbot configurado para envio de resposta.",
      );
    }

    const storedMessage = await storeConversationMessage({
      lawFirmId: profile.lawFirm.id,
      clientId: dispatchResult?.clientId ?? payload.clientId,
      caseId: dispatchResult?.caseId ?? payload.caseId ?? null,
      conversationId: payload.conversationId ?? null,
      channelCode: payload.channelCode,
      itemTypeCode,
      directionCode: payload.directionCode,
      subject: payload.subject || null,
      bodyText: payload.bodyText,
      senderName: payload.senderName || null,
      senderAddress: payload.senderAddress || null,
      recipientAddress: payload.recipientAddress || null,
      authoredByUserId: profile.user.id,
      createdByUserId: profile.user.id,
      externalMessageId: dispatchResult?.externalMessageId ?? null,
      messageStatus: dispatchResult?.messageStatus ?? "stored",
      conversationSubject: payload.subject || null,
      conversationParticipantsJson: {
        senderName: payload.senderName || null,
        senderAddress: payload.senderAddress || null,
        recipientAddress: payload.recipientAddress || null,
      },
    });

    if (dispatchResult?.deliveryId) {
      await attachKommoOutboundDeliveryToRepositoryItem({
        lawFirmId: profile.lawFirm.id,
        deliveryId: dispatchResult.deliveryId,
        repositoryItemId: storedMessage.repositoryItemId,
        messageStatus: dispatchResult.messageStatus ?? "stored",
      });
    }

    if (payload.directionCode === "outbound" && storedMessage.conversationId) {
      await mirrorRepositoryMessageToKommoLead({
        lawFirmId: profile.lawFirm.id,
        conversationId: storedMessage.conversationId,
        repositoryItemId: storedMessage.repositoryItemId,
        bodyText: payload.bodyText,
        senderName: payload.senderName || null,
        senderAddress: payload.senderAddress || null,
        recipientAddress: payload.recipientAddress || null,
        messageStatus: dispatchResult?.messageStatus ?? "stored",
      });
    }

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "repository_item",
      entityId: storedMessage.repositoryItemId,
      action: "repository.message.create",
      afterJson: {
        channelCode: payload.channelCode,
        conversationId: storedMessage.conversationId,
      },
      request,
    });

    return reply.code(201).send({
      conversationId: storedMessage.conversationId,
      repositoryItemId: storedMessage.repositoryItemId,
      messageStatus: dispatchResult?.messageStatus ?? "stored",
      kommoDelivery: dispatchResult?.isKommoConversation
        ? {
            deliveryId: dispatchResult.deliveryId ?? null,
            nextRetryAt: dispatchResult.nextRetryAt ?? null,
            lastErrorMessage: dispatchResult.lastErrorMessage ?? null,
          }
        : null,
    });
  });

  app.post("/call-logs", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const payload = callLogSchema.parse(request.body);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const conversationId = await createConversation({
      lawFirmId: profile.lawFirm.id,
      clientId: payload.clientId,
      caseId: payload.caseId ?? null,
      channelCode: "phone",
      subject: "Phone call",
    });

    const repositoryItemId = await createRepositoryItem({
      lawFirmId: profile.lawFirm.id,
      clientId: payload.clientId,
      caseId: payload.caseId ?? null,
      itemTypeCode: "phone_call",
      channelCode: "phone",
      directionCode: payload.directionCode,
      subject: "Phone call",
      bodyText: payload.transcriptText,
      authoredByUserId: profile.user.id,
      createdByUserId: profile.user.id,
      sourceEntityType: "conversation",
      sourceEntityId: conversationId,
    });

    await prisma.$executeRaw`
      INSERT INTO call_logs (
        id, law_firm_id, conversation_id, repository_item_id, from_number, to_number,
        direction_code, duration_seconds, started_at, ended_at, transcript_text,
        recording_file_id, external_call_id, created_at
      ) VALUES (
        ${createId()},
        ${profile.lawFirm.id},
        ${conversationId},
        ${repositoryItemId},
        ${payload.fromNumber || null},
        ${payload.toNumber || null},
        ${payload.directionCode},
        ${payload.durationSeconds},
        NOW(),
        NOW(),
        ${payload.transcriptText},
        NULL,
        NULL,
        CURRENT_TIMESTAMP
      )
    `;

    return reply.code(201).send({
      conversationId,
      repositoryItemId,
    });
  });

  app.get("/documents", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const query = querySchema.parse(request.query);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const documents = await prisma.$queryRaw<
      Array<{
        id: string;
        title: string;
        document_type_code: string;
        document_status: string;
        extracted_text: string | null;
        created_at: Date;
      }>
    >`
      SELECT id, title, document_type_code, document_status, extracted_text, created_at
      FROM document_records
      WHERE law_firm_id = ${profile.lawFirm.id}
        AND (${query.clientId ?? null} IS NULL OR client_id = ${query.clientId ?? null})
        AND (${query.caseId ?? null} IS NULL OR case_id = ${query.caseId ?? null})
      ORDER BY created_at DESC
    `;

    return documents.map((document) => ({
      id: document.id,
      title: document.title,
      documentTypeCode: document.document_type_code,
      documentStatus: document.document_status,
      extractedText: document.extracted_text,
      createdAt: document.created_at,
    }));
  });

  app.post("/documents/upload", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const multipartRequest = request as typeof request & {
      file: () => Promise<MultipartFile | undefined>;
    };
    const file = await multipartRequest.file();
    if (!file) {
      throw reply.badRequest("A file upload is required");
    }

    const clientId = getMultipartFieldValue(file.fields, "clientId");
    const caseId = getMultipartFieldValue(file.fields, "caseId");
    const documentTypeCode = getMultipartFieldValue(file.fields, "documentTypeCode") || "other_supporting";
    const title = getMultipartFieldValue(file.fields, "title") || file.filename;
    const providedText = getMultipartFieldValue(file.fields, "textContent");

    if (!clientId) {
      throw reply.badRequest("clientId is required");
    }

    const buffer = await file.toBuffer();
    const textContent = await extractDocumentText({
      fileName: file.filename,
      mimeType: file.mimetype,
      bytes: buffer,
      providedText: providedText || null,
    });

    const created = await createDocumentAndExtraction({
      lawFirmId: profile.lawFirm.id,
      clientId,
      caseId: caseId || null,
      actorUserId: profile.user.id,
      title,
      documentTypeCode,
      originalFileName: file.filename,
      mimeType: file.mimetype,
      fileBuffer: buffer,
      textContent: textContent || null,
    });

    if (caseId) {
      try {
        await consolidateCaseFacts({
          lawFirmId: profile.lawFirm.id,
          caseId,
          actorUserId: profile.user.id,
        });
      } catch {
        // keep upload successful even if consolidation is not ready yet
      }
    }

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "document_record",
      entityId: created.documentRecordId,
      action: "document.upload",
      afterJson: {
        documentTypeCode,
        title,
        checksum: createHash("sha256").update(buffer).digest("hex"),
      },
      request,
    });

    return reply.code(201).send(created);
  });

  app.post("/documents/folder-upload", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    if (!request.isMultipart()) {
      throw reply.badRequest("A multipart folder upload is required");
    }

    let clientId = "";
    let caseId = "";
    let documentTypeCode = "";
    let jobId = "";
    let sortOrder = 0;

    for await (const part of request.parts({
      limits: folderUploadMultipartLimits,
    })) {
      if (part.type === "field") {
        const value = String(part.value ?? "").trim();

        if (part.fieldname === "clientId") {
          clientId = value;
        } else if (part.fieldname === "caseId") {
          caseId = value;
        } else if (part.fieldname === "documentTypeCode" && value) {
          documentTypeCode = value;
        }

        continue;
      }

      const resolvedClientId = clientId || getMultipartFieldValue(part.fields, "clientId");
      const resolvedCaseId = caseId || getMultipartFieldValue(part.fields, "caseId");
      const resolvedDocumentTypeCode =
        documentTypeCode || getMultipartFieldValue(part.fields, "documentTypeCode") || "";

      if (!resolvedClientId) {
        throw reply.badRequest("clientId is required");
      }

      if (!jobId) {
        jobId = await createRepositoryFolderUploadJob({
          lawFirmId: profile.lawFirm.id,
          clientId: resolvedClientId,
          caseId: resolvedCaseId || null,
          createdByUserId: profile.user.id,
          fallbackDocumentTypeCode: resolvedDocumentTypeCode || null,
        });
      }

      const normalizedRelativePath = String(part.filename ?? "").trim() || "document";

      try {
        const buffer = await part.toBuffer();
        const staged = await stageRepositoryFolderUploadFile({
          lawFirmId: profile.lawFirm.id,
          caseId: resolvedCaseId || null,
          originalRelativePath: normalizedRelativePath,
          mimeType: part.mimetype,
          bytes: buffer,
        });

        await addRepositoryFolderUploadJobFile({
          jobId,
          sortOrder,
          fileName: staged.fileName,
          relativePath: staged.relativePath,
          title: staged.fileName,
          mimeType: part.mimetype,
          sizeBytes: staged.sizeBytes,
          stagingStorageProvider: staged.storageProvider,
          stagingStorageBucket: staged.storageBucket,
          stagingObjectKey: staged.objectKey,
          stagingStorageRegion: staged.storageRegion,
          stagingOriginalFileName: staged.originalFileName,
        });
      } catch (error) {
        await addRepositoryFolderUploadJobFile({
          jobId,
          sortOrder,
          fileName: normalizedRelativePath.split(/[/\\]/).pop() || "document",
          relativePath: normalizedRelativePath,
          title: normalizedRelativePath.split(/[/\\]/).pop() || "document",
          mimeType: part.mimetype,
          sizeBytes: 0,
          initialStatusCode: "failed",
          errorMessage: error instanceof Error ? error.message : "Failed to stage the file for processing",
        });
      }

      sortOrder += 1;
    }

    if (!jobId || sortOrder === 0) {
      throw reply.badRequest("At least one file is required");
    }

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "repository_folder_upload_job",
      entityId: jobId,
      action: "repository.folder_upload.queued",
      afterJson: {
        clientId: clientId.trim() || null,
        caseId: caseId.trim() || null,
        fallbackDocumentTypeCode: documentTypeCode || null,
        totalFiles: sortOrder,
      },
      request,
    });

    await startRepositoryFolderUploadJob({ jobId, logger: app.log });
    const detail = await getRepositoryFolderUploadJobDetail({
      lawFirmId: profile.lawFirm.id,
      jobId,
      logger: app.log,
    });

    return reply.code(202).send(detail);
  });

  app.get("/documents/folder-upload-jobs", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const query = folderUploadJobListQuerySchema.parse(request.query);
    const jobs = await listRepositoryFolderUploadJobs({
      lawFirmId: profile.lawFirm.id,
      clientId: query.clientId,
      caseId: query.caseId,
      limit: query.limit,
      logger: app.log,
    });

    return reply.send(jobs);
  });

  app.get("/documents/folder-upload-jobs/:jobId", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const { jobId } = folderUploadJobParamsSchema.parse(request.params);
    const detail = await getRepositoryFolderUploadJobDetail({
      lawFirmId: profile.lawFirm.id,
      jobId,
      logger: app.log,
    });

    if (!detail) {
      throw reply.notFound("Folder upload job not found");
    }

    return reply.send(detail);
  });

  app.post("/documents/reprocess-ai-jobs", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const payload = repositoryDocumentReprocessCreateSchema.parse(request.body);
    const jobId = await createRepositoryDocumentReprocessJob({
      lawFirmId: profile.lawFirm.id,
      clientId: payload.clientId,
      caseId: payload.caseId ?? null,
      actorUserId: profile.user.id,
    });

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "repository_document_reprocess_job",
      entityId: jobId,
      action: "repository.document_reprocess.queued",
      afterJson: {
        clientId: payload.clientId,
        caseId: payload.caseId ?? null,
      },
      request,
    });

    await startRepositoryDocumentReprocessJob({
      jobId,
      logger: app.log,
    });

    const detail = await getRepositoryDocumentReprocessJobDetail({
      lawFirmId: profile.lawFirm.id,
      jobId,
      logger: app.log,
    });

    if (!detail) {
      throw reply.internalServerError("Repository reprocess job could not be created");
    }

    return reply.code(202).send(detail);
  });

  app.get("/documents/reprocess-ai-jobs", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const query = repositoryDocumentReprocessJobListQuerySchema.parse(request.query);
    const jobs = await listRepositoryDocumentReprocessJobs({
      lawFirmId: profile.lawFirm.id,
      clientId: query.clientId,
      caseId: query.caseId,
      limit: query.limit,
      logger: app.log,
    });

    return reply.send(jobs);
  });

  app.get("/documents/reprocess-ai-jobs/:jobId", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const { jobId } = repositoryDocumentReprocessJobParamsSchema.parse(request.params);
    const detail = await getRepositoryDocumentReprocessJobDetail({
      lawFirmId: profile.lawFirm.id,
      jobId,
      logger: app.log,
    });

    if (!detail) {
      throw reply.notFound("Repository reprocess job not found");
    }

    return reply.send(detail);
  });

  app.post("/documents/reprocess-ai-jobs/:jobId/pause", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const { jobId } = repositoryDocumentReprocessJobParamsSchema.parse(request.params);
    const detail = await pauseRepositoryDocumentReprocessJob({
      lawFirmId: profile.lawFirm.id,
      jobId,
    });

    if (!detail) {
      throw reply.notFound("Repository reprocess job not found");
    }

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      actorUserId: profile.user.id,
      officeId: profile.user.primaryOfficeId ?? null,
      entityType: "repository_document_reprocess_job",
      entityId: jobId,
      action: "repository.document_reprocess.paused",
      afterJson: detail,
      request,
    });

    return reply.send(detail);
  });

  app.post("/documents/reprocess-ai-jobs/:jobId/resume", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const { jobId } = repositoryDocumentReprocessJobParamsSchema.parse(request.params);
    const detail = await resumeRepositoryDocumentReprocessJob({
      lawFirmId: profile.lawFirm.id,
      jobId,
      logger: app.log,
    });

    if (!detail) {
      throw reply.notFound("Repository reprocess job not found");
    }

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      actorUserId: profile.user.id,
      officeId: profile.user.primaryOfficeId ?? null,
      entityType: "repository_document_reprocess_job",
      entityId: jobId,
      action: "repository.document_reprocess.resumed",
      afterJson: detail,
      request,
    });

    return reply.send(detail);
  });

  app.post("/knowledge-documents/upload", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const multipartRequest = request as typeof request & {
      file: () => Promise<MultipartFile | undefined>;
    };
    const file = await multipartRequest.file();
    if (!file) {
      throw reply.badRequest("Envie um arquivo PDF ou imagem.");
    }

    const title = getMultipartFieldValue(file.fields, "title").trim();
    const objective = getMultipartFieldValue(file.fields, "objective").trim();

    if (!title) {
      throw reply.badRequest("Informe o título do documento.");
    }

    if (!objective) {
      throw reply.badRequest("Informe a descrição do objetivo do documento.");
    }

    if (!isKnowledgeDocumentMimeAllowed({ fileName: file.filename, mimeType: file.mimetype })) {
      throw reply.badRequest("A base de conhecimento aceita apenas PDF ou imagem.");
    }

    const buffer = await file.toBuffer();
    const extractedText = await extractDocumentText({
      fileName: file.filename,
      mimeType: file.mimetype,
      bytes: buffer,
    });
    const stored = await saveBinaryFile({
      lawFirmId: profile.lawFirm.id,
      caseId: null,
      fileName: file.filename,
      bytes: buffer,
      kind: "uploads",
    });

    const repositoryItemId = await createRepositoryItem({
      lawFirmId: profile.lawFirm.id,
      itemTypeCode: getKnowledgeRepositoryItemTypeCode(file.mimetype),
      channelCode: "internal",
      sourceEntityType: "knowledge_base_document",
      subject: title,
      summaryText: objective,
      bodyText: buildKnowledgeDocumentRepositoryBody({
        title,
        objective,
        extractedText,
      }),
      metadataJson: {
        knowledgeBase: true,
        originalFileName: file.filename,
        mimeType: file.mimetype,
        extractedTextLength: extractedText.length,
        indexing: {
          status: "pending",
          indexedAt: null,
          aiRunId: null,
          model: null,
          errorMessage: null,
        },
      },
      authoredByUserId: profile.user.id,
      createdByUserId: profile.user.id,
    });

    const fileId = createId();
    await prisma.$executeRaw`
      INSERT INTO files (
        id, law_firm_id, client_id, case_id, repository_item_id, storage_provider, storage_bucket,
        object_key, storage_region, original_file_name, stored_file_name, mime_type, size_bytes,
        checksum_sha256, is_encrypted, uploaded_by_user_id, uploaded_at, created_at
      ) VALUES (
        ${fileId},
        ${profile.lawFirm.id},
        NULL,
        NULL,
        ${repositoryItemId},
        'local_dev',
        'workspace',
        ${stored.relativeObjectKey},
        'local',
        ${file.filename},
        ${stored.storedFileName},
        ${file.mimetype},
        ${buffer.length},
        ${stored.checksumSha256},
        0,
        ${profile.user.id},
        NOW(),
        CURRENT_TIMESTAMP
      )
    `;

    let indexingResult:
      | {
          status: "completed";
          aiRunId: string;
          model: string;
          usage: { inputTokens: number; outputTokens: number; totalTokens: number };
          index: {
            synopsis: string;
            searchableSummary: string;
            documentClass: string;
            keywords: string[];
            legalTopics: string[];
            citedEntities: string[];
            jurisdictions: string[];
            useCases: string[];
            suggestedQuestions: string[];
            confidence: number;
          };
          errorMessage: null;
        }
      | {
          status: "skipped" | "failed";
          aiRunId: null;
          model: null;
          usage: { inputTokens: number; outputTokens: number; totalTokens: number };
          index: null;
          errorMessage: string;
        } = {
      status: "skipped",
      aiRunId: null,
      model: null,
      usage: {
        inputTokens: 0,
        outputTokens: 0,
        totalTokens: 0,
      },
      index: null,
      errorMessage: "Indexação não executada.",
    };

    try {
      const aiIndex = await indexKnowledgeDocumentWithAi({
        lawFirmId: profile.lawFirm.id,
        repositoryItemId,
        title,
        objective,
        fileName: file.filename,
        mimeType: file.mimetype,
        extractedText,
      });

      indexingResult = aiIndex;

      await prisma.$executeRaw`
        UPDATE repository_items
        SET
          body_text = ${buildKnowledgeDocumentRepositoryBody({
            title,
            objective,
            extractedText,
            aiIndex: aiIndex.index,
          })},
          metadata_json = ${JSON.stringify({
            knowledgeBase: true,
            originalFileName: file.filename,
            mimeType: file.mimetype,
            extractedTextLength: extractedText.length,
            indexing: {
              status: aiIndex.status,
              indexedAt: new Date().toISOString(),
              aiRunId: aiIndex.aiRunId,
              model: aiIndex.model,
              errorMessage: null,
              usage: aiIndex.usage,
            },
            aiIndex: aiIndex.index,
          })},
          updated_at = CURRENT_TIMESTAMP
        WHERE id = ${repositoryItemId}
      `;
    } catch (error) {
      indexingResult = {
        status: isMissingAiConfigurationError(error) ? "skipped" : "failed",
        aiRunId: null,
        model: null,
        usage: {
          inputTokens: 0,
          outputTokens: 0,
          totalTokens: 0,
        },
        index: null,
        errorMessage:
          error instanceof Error ? error.message : "Falha inesperada na indexação automática.",
      };

      await prisma.$executeRaw`
        UPDATE repository_items
        SET
          metadata_json = ${JSON.stringify({
            knowledgeBase: true,
            originalFileName: file.filename,
            mimeType: file.mimetype,
            extractedTextLength: extractedText.length,
            indexing: {
              status: indexingResult.status,
              indexedAt: null,
              aiRunId: null,
              model: null,
              errorMessage: indexingResult.errorMessage,
            },
          })},
          updated_at = CURRENT_TIMESTAMP
        WHERE id = ${repositoryItemId}
      `;
    }

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "repository_item",
      entityId: repositoryItemId,
      action: "repository.knowledge_document.upload",
      afterJson: {
        title,
        objective,
        fileName: file.filename,
        mimeType: file.mimetype,
        checksum: createHash("sha256").update(buffer).digest("hex"),
        indexingStatus: indexingResult.status,
        indexingAiRunId: indexingResult.aiRunId,
        indexingErrorMessage: indexingResult.errorMessage,
        indexingKeywords:
          indexingResult.status === "completed" ? indexingResult.index.keywords : [],
      },
      request,
    });

    return reply.code(201).send({
      repositoryItemId,
      fileId,
      title,
      objective,
      extractedTextLength: extractedText.length,
      indexing: {
        status: indexingResult.status,
        aiRunId: indexingResult.aiRunId,
        model: indexingResult.model,
        errorMessage: indexingResult.errorMessage,
        keywords:
          indexingResult.status === "completed" ? indexingResult.index.keywords : [],
      },
    });
  });

  void resumePendingRepositoryFolderUploadJobs(app.log).catch((error) => {
    app.log.error({ error }, "Failed to resume pending repository folder upload jobs");
  });

  void resumePendingRepositoryDocumentReprocessJobs(app.log).catch((error) => {
    app.log.error({ error }, "Failed to resume pending repository document reprocess jobs");
  });
}
