import { createHash } from "node:crypto";
import { extname, resolve } from "node:path";
import type { FastifyRequest } from "fastify";
import { Prisma } from "@prisma/client";
import mammoth from "mammoth";
import { PDFParse } from "pdf-parse";
import { createWorker } from "tesseract.js";
import { writeAuditLog } from "./audit.js";
import { createId } from "./id.js";
import { prisma } from "./prisma.js";
import { saveBinaryFile } from "./storage.js";
import { createAiRun, finishAiRun, runJsonChatCompletion } from "./tenant-ai.js";

function isZipArchiveBuffer(bytes: Buffer) {
  return bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b;
}

type AnalysisType = "pre_submission" | "submission_check" | "review_support" | "final_validation";
type DecisionCode = "approved" | "rejected";
type RiskSeverity = "low" | "medium" | "high" | "critical";
type ReviewVerdict = "valid" | "invalid" | "needs_review";

type ReviewRisk = {
  title: string;
  severity: RiskSeverity;
  description: string;
  recommendation: string | null;
};

type ReviewAnalysis = {
  id: string | null;
  aiRunId: string | null;
  analysisType: AnalysisType;
  summary: string;
  strengths: string[];
  weaknesses: string[];
  suggestions: string[];
  risks: ReviewRisk[];
  possibleRejectionReasons: string[];
  confidence: number;
  confidenceLabel: "low" | "medium" | "high";
  riskLevel: RiskSeverity;
  verdict: ReviewVerdict;
  verdictReason: string;
  alertPoints: string[];
  extractedText: string;
  structure: Record<string, unknown>;
  identifiedPatterns: Record<string, unknown>;
  historicalContext: Record<string, unknown>;
  comparison: Record<string, unknown> | null;
  sourcePayload: Record<string, unknown> | null;
  rawResult: Record<string, unknown>;
  triggeredManually: boolean;
  requiresConfirmation: boolean;
};

type PersistedAnalysis = ReviewAnalysis & {
  id: string;
};

type AnchoredCommentKnowledge = {
  id: string;
  versionId: string;
  selectedText: string;
  commentText: string;
  assistantResponse: string;
  knowledgeSummary: string | null;
  createdAt: Date;
  createdBy: string;
};

type AnchoredSelectionRect = {
  x: number;
  y: number;
  width: number;
  height: number;
};

type ResolvedAnchoredSelection = {
  selectionStart: number | null;
  selectionEnd: number | null;
  selectedText: string;
  contextBefore: string;
  contextAfter: string;
};

type HistoricalDecisionRow = {
  item_id: string;
  document_name: string;
  current_status: string;
  case_number: string;
  case_title: string;
  case_type_code: string;
  decision_code: string | null;
  justification: string | null;
  rejection_reason: string | null;
  guidance_for_new_version: string | null;
  decision_created_at: Date | null;
};

type VersionContext = {
  itemId: string;
  lawFirmId: string;
  caseId: string;
  caseNumber: string;
  caseTitle: string;
  caseTypeCode: string;
  clientId: string;
  clientName: string;
  documentName: string;
  objective: string;
  dueAt: Date;
  currentStatus: string;
  versionId: string;
  versionNumber: number;
  fileId: string;
  originalFileName: string;
  mimeType: string;
  objectKey: string;
  storageProvider: string;
  extractedText: string | null;
  structureJson: unknown;
  identifiedPatternsJson: unknown;
  comparisonJson: unknown;
};

type ReviewerAgentContext = {
  teamId: string;
  reviewerUserId: string;
  reviewerDisplayName: string;
  reviewerEmail: string;
  membershipRole: string;
  agentId: string;
  agentName: string;
  systemPrompt: string | null;
  learningSummary: string | null;
  recentMemories: string[];
};

type ItemAssignmentContext = {
  teamId: string | null;
  assignedReviewerUserId: string | null;
  assignedReviewerDisplayName: string | null;
  assignedReviewerEmail: string | null;
  assignedReviewerAgentId: string | null;
  assignedReviewerAgentName: string | null;
  assignedByUserId: string | null;
  assignedByDisplayName: string | null;
  assignedAt: Date | null;
};

type ItemContext = {
  itemId: string;
  lawFirmId: string;
  caseId: string;
  caseNumber: string;
  caseTitle: string;
  caseTypeCode: string;
  clientId: string;
  clientName: string;
  documentName: string;
  objective: string;
  dueAt: Date;
  currentStatus: string;
  latestVersionNumber: number;
};

type ActorContext = {
  actorUserId: string;
  officeId?: string | null;
  request?: FastifyRequest;
};

type DocumentReviewDependencies = {
  prisma: typeof prisma;
  saveBinaryFile: typeof saveBinaryFile;
  writeAuditLog: typeof writeAuditLog;
  createAiRun: typeof createAiRun;
  finishAiRun: typeof finishAiRun;
  runJsonChatCompletion: typeof runJsonChatCompletion;
};

const defaultDependencies: DocumentReviewDependencies = {
  prisma,
  saveBinaryFile,
  writeAuditLog,
  createAiRun,
  finishAiRun,
  runJsonChatCompletion,
};

const severityRank: Record<RiskSeverity, number> = {
  low: 1,
  medium: 2,
  high: 3,
  critical: 4,
};

const analysisRunTypeByType: Record<AnalysisType, string> = {
  pre_submission: "pre_submission_analysis",
  submission_check: "submission_analysis",
  review_support: "review_assist",
  final_validation: "final_review_validation",
};

const maxPromptCharacters = 18_000;
const ocrCachePath = resolve(process.cwd(), "../../storage/tesseract-cache");

const textPlaceholderPatterns = [
  /\bTODO\b/gi,
  /\bTBD\b/gi,
  /\blorem ipsum\b/gi,
  /\bxxx+\b/gi,
  /_{3,}/g,
  /\[\s*\]/g,
];

export class DocumentReviewServiceError extends Error {
  statusCode: number;

  constructor(statusCode: number, message: string) {
    super(message);
    this.statusCode = statusCode;
  }
}

function normalizeText(value: string) {
  return value.replace(/\u0000/g, "").replace(/\r\n/g, "\n").trim();
}

function truncateText(value: string, limit = maxPromptCharacters) {
  if (value.length <= limit) {
    return value;
  }

  return `${value.slice(0, limit)}\n\n[truncated]`;
}

function parseJsonValue<T>(value: unknown, fallback: T): T {
  if (value === null || value === undefined) {
    return fallback;
  }

  if (typeof value === "string") {
    try {
      return JSON.parse(value) as T;
    } catch {
      return fallback;
    }
  }

  return value as T;
}

function toNumber(value: unknown, fallback = 0) {
  if (value === null || value === undefined || value === "") {
    return fallback;
  }

  if (typeof value === "number") {
    return Number.isFinite(value) ? value : fallback;
  }

  if (typeof value === "bigint") {
    return Number(value);
  }

  if (typeof value === "string") {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : fallback;
  }

  if (value instanceof Prisma.Decimal) {
    return value.toNumber();
  }

  return fallback;
}

function clampNumber(value: number, min: number, max: number) {
  return Math.min(max, Math.max(min, value));
}

function normalizeSelectionText(value: string) {
  return value.replace(/\s+/g, " ").trim();
}

function buildNormalizedTextIndex(value: string) {
  let text = "";
  const indexMap: number[] = [];
  let previousWasWhitespace = true;

  for (let index = 0; index < value.length; index += 1) {
    const character = value[index];

    if (/\s/.test(character)) {
      if (!previousWasWhitespace && text.length > 0) {
        text += " ";
        indexMap.push(index);
      }
      previousWasWhitespace = true;
      continue;
    }

    text += character;
    indexMap.push(index);
    previousWasWhitespace = false;
  }

  if (text.endsWith(" ")) {
    text = text.slice(0, -1);
    indexMap.pop();
  }

  return {
    text,
    indexMap,
  };
}

function scoreSelectionContext(
  sourceSnippet: string,
  hint: string | null | undefined,
  mode: "before" | "after",
) {
  const normalizedSource = normalizeSelectionText(sourceSnippet);
  const normalizedHint = normalizeSelectionText(String(hint ?? ""));

  if (!normalizedSource || !normalizedHint) {
    return 0;
  }

  const excerpt =
    mode === "before"
      ? normalizedHint.slice(Math.max(0, normalizedHint.length - 140))
      : normalizedHint.slice(0, 140);

  if (!excerpt) {
    return 0;
  }

  if (mode === "before") {
    if (normalizedSource.endsWith(excerpt)) {
      return 6;
    }

    if (normalizedSource.includes(excerpt)) {
      return 3;
    }

    return 0;
  }

  if (normalizedSource.startsWith(excerpt)) {
    return 6;
  }

  if (normalizedSource.includes(excerpt)) {
    return 3;
  }

  return 0;
}

function findBestAnchoredSelectionMatch(input: {
  sourceText: string;
  selectedText: string;
  contextBefore?: string | null;
  contextAfter?: string | null;
}) {
  const selectedText = String(input.selectedText ?? "").trim();
  if (!selectedText) {
    return null;
  }

  const candidates: Array<{ start: number; end: number; score: number }> = [];
  let searchFrom = 0;
  let exactMatches = 0;

  while (exactMatches < 40) {
    const foundAt = input.sourceText.indexOf(selectedText, searchFrom);
    if (foundAt < 0) {
      break;
    }

    const end = foundAt + selectedText.length;
    const before = input.sourceText.slice(Math.max(0, foundAt - 220), foundAt);
    const after = input.sourceText.slice(end, Math.min(input.sourceText.length, end + 220));

    candidates.push({
      start: foundAt,
      end,
      score:
        scoreSelectionContext(before, input.contextBefore, "before") +
        scoreSelectionContext(after, input.contextAfter, "after"),
    });
    exactMatches += 1;
    searchFrom = foundAt + 1;
  }

  if (candidates.length > 0) {
    return candidates.sort((left, right) => right.score - left.score || left.start - right.start)[0];
  }

  const normalizedSource = buildNormalizedTextIndex(input.sourceText);
  const normalizedSelected = normalizeSelectionText(selectedText);
  if (!normalizedSource.text || !normalizedSelected) {
    return null;
  }

  let normalizedSearchFrom = 0;
  let normalizedMatches = 0;
  const normalizedCandidates: Array<{ start: number; end: number; score: number }> = [];

  while (normalizedMatches < 40) {
    const foundAt = normalizedSource.text.indexOf(normalizedSelected, normalizedSearchFrom);
    if (foundAt < 0) {
      break;
    }

    const normalizedEnd = foundAt + normalizedSelected.length;
    const originalStart = normalizedSource.indexMap[foundAt];
    const originalEnd = normalizedSource.indexMap[normalizedEnd - 1];
    if (originalStart === undefined || originalEnd === undefined) {
      break;
    }

    const end = originalEnd + 1;
    const before = input.sourceText.slice(Math.max(0, originalStart - 220), originalStart);
    const after = input.sourceText.slice(end, Math.min(input.sourceText.length, end + 220));

    normalizedCandidates.push({
      start: originalStart,
      end,
      score:
        scoreSelectionContext(before, input.contextBefore, "before") +
        scoreSelectionContext(after, input.contextAfter, "after"),
    });
    normalizedMatches += 1;
    normalizedSearchFrom = foundAt + 1;
  }

  return normalizedCandidates.sort(
    (left, right) => right.score - left.score || left.start - right.start,
  )[0] ?? null;
}

function normalizeAnchoredSelectionRects(value: unknown) {
  const rawRects = Array.isArray(value) ? value : [];

  return rawRects
    .map((entry) => {
      const rect = entry as Record<string, unknown>;
      const x = clampNumber(toNumber(rect.x, 0), 0, 1);
      const y = clampNumber(toNumber(rect.y, 0), 0, 1);
      const width = clampNumber(toNumber(rect.width, 0), 0, 1 - x);
      const height = clampNumber(toNumber(rect.height, 0), 0, 1 - y);

      if (width <= 0 || height <= 0) {
        return null;
      }

      return {
        x,
        y,
        width,
        height,
      } satisfies AnchoredSelectionRect;
    })
    .filter((rect): rect is AnchoredSelectionRect => Boolean(rect))
    .slice(0, 24);
}

function resolveAnchoredSelection(input: {
  extractedText: string;
  selectedText: string;
  selectionStart?: number | null;
  selectionEnd?: number | null;
  selectionContextBefore?: string | null;
  selectionContextAfter?: string | null;
}) {
  const selectedText = String(input.selectedText ?? "").trim();
  if (!selectedText) {
    throw new DocumentReviewServiceError(400, "Selecione um trecho com conteúdo visível.");
  }

  if (selectedText.length > 1_500) {
    throw new DocumentReviewServiceError(400, "Selecione no máximo 1500 caracteres por comentário.");
  }

  const extractedText = String(input.extractedText ?? "");
  const fallbackContextBefore = String(input.selectionContextBefore ?? "").slice(-320);
  const fallbackContextAfter = String(input.selectionContextAfter ?? "").slice(0, 320);

  if (!extractedText.trim()) {
    return {
      selectionStart: null,
      selectionEnd: null,
      selectedText,
      contextBefore: fallbackContextBefore,
      contextAfter: fallbackContextAfter,
    } satisfies ResolvedAnchoredSelection;
  }

  const requestedStart =
    input.selectionStart === null || input.selectionStart === undefined
      ? null
      : Math.max(0, Math.floor(Number(input.selectionStart)));
  const requestedEnd =
    input.selectionEnd === null || input.selectionEnd === undefined
      ? null
      : Math.max(0, Math.floor(Number(input.selectionEnd)));

  if (
    requestedStart !== null &&
    requestedEnd !== null &&
    requestedStart < requestedEnd &&
    requestedEnd <= extractedText.length
  ) {
    const exactSelectedText = extractedText.slice(requestedStart, requestedEnd);
    if (normalizeSelectionText(exactSelectedText) === normalizeSelectionText(selectedText)) {
      return {
        selectionStart: requestedStart,
        selectionEnd: requestedEnd,
        selectedText,
        contextBefore: extractedText.slice(Math.max(0, requestedStart - 320), requestedStart),
        contextAfter: extractedText.slice(
          requestedEnd,
          Math.min(extractedText.length, requestedEnd + 320),
        ),
      } satisfies ResolvedAnchoredSelection;
    }
  }

  const matchedExcerpt = findBestAnchoredSelectionMatch({
    sourceText: extractedText,
    selectedText,
    contextBefore: input.selectionContextBefore,
    contextAfter: input.selectionContextAfter,
  });

  if (matchedExcerpt) {
    return {
      selectionStart: matchedExcerpt.start,
      selectionEnd: matchedExcerpt.end,
      selectedText,
      contextBefore: extractedText.slice(
        Math.max(0, matchedExcerpt.start - 320),
        matchedExcerpt.start,
      ),
      contextAfter: extractedText.slice(
        matchedExcerpt.end,
        Math.min(extractedText.length, matchedExcerpt.end + 320),
      ),
    } satisfies ResolvedAnchoredSelection;
  }

  if (fallbackContextBefore || fallbackContextAfter) {
    return {
      selectionStart: null,
      selectionEnd: null,
      selectedText,
      contextBefore: fallbackContextBefore,
      contextAfter: fallbackContextAfter,
    } satisfies ResolvedAnchoredSelection;
  }

  throw new DocumentReviewServiceError(
    400,
    "O trecho enviado não corresponde ao texto extraído desta versão.",
  );
}

function toIsoMonth(date: Date) {
  return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
}

function formatDueDateForAnalysis(date: Date) {
  return date.toISOString().slice(0, 10);
}

function dedupeStrings(values: Array<string | null | undefined>) {
  return Array.from(
    new Set(
      values
        .map((value) => String(value ?? "").trim())
        .filter(Boolean),
    ),
  );
}

function maxRiskLevel(values: RiskSeverity[]) {
  return values.reduce<RiskSeverity>(
    (current, value) => (severityRank[value] > severityRank[current] ? value : current),
    "low",
  );
}

function confidenceLabelFromScore(score: number): "low" | "medium" | "high" {
  if (score >= 0.85) {
    return "high";
  }

  if (score >= 0.65) {
    return "medium";
  }

  return "low";
}

function truncateInsight(value: string, limit = 180) {
  const normalized = String(value ?? "").replace(/\s+/g, " ").trim();
  if (normalized.length <= limit) {
    return normalized;
  }

  return `${normalized.slice(0, Math.max(0, limit - 1)).trimEnd()}...`;
}

function buildTextPatterns<Key extends string>(
  values: Array<string | null | undefined>,
  key: Key,
  limit = 6,
): Array<Record<Key, string> & { count: number }> {
  const counts = values.reduce<Map<string, number>>((acc, item) => {
    const normalized = truncateInsight(String(item ?? ""));
    if (!normalized) {
      return acc;
    }

    acc.set(normalized, (acc.get(normalized) ?? 0) + 1);
    return acc;
  }, new Map());

  const patterns = Array.from(counts.entries()).map(
    ([text, count]) =>
      ({
        [key]: text,
        count,
      }) as Record<Key, string> & { count: number },
  );

  return patterns
    .sort((left, right) => right.count - left.count)
    .slice(0, limit);
}

function formatPatternWithCount(text: string, count: number) {
  return count > 1 ? `${text} (${count}x)` : text;
}

function buildChecksum(bytes: Buffer) {
  return createHash("sha256").update(bytes).digest("hex");
}

function isImageFile(input: { mimeType: string; extension: string }) {
  return (
    input.mimeType.startsWith("image/") ||
    [".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff", ".gif"].includes(input.extension)
  );
}

async function extractImageTextWithLocalOcr(bytes: Buffer) {
  const worker = await createWorker("eng", 1, {
    cachePath: ocrCachePath,
    logger: () => undefined,
  });

  try {
    const result = await worker.recognize(bytes);
    return normalizeText(result.data.text ?? "");
  } finally {
    await worker.terminate().catch(() => undefined);
  }
}

function getUserDisplayName(row: {
  display_name: string | null;
  first_name: string | null;
  last_name: string | null;
  email?: string | null;
}) {
  const displayName = String(row.display_name ?? "").trim();
  if (displayName) {
    return displayName;
  }

  const fullName = `${String(row.first_name ?? "").trim()} ${String(row.last_name ?? "").trim()}`.trim();
  if (fullName) {
    return fullName;
  }

  return String(row.email ?? "").trim() || "Usuário";
}

function normalizeEmail(value: string | null | undefined) {
  return String(value ?? "").trim().toLowerCase();
}

export async function extractDocumentText(input: {
  fileName: string;
  mimeType: string;
  bytes: Buffer;
  providedText?: string | null;
}) {
  const providedText = normalizeText(String(input.providedText ?? ""));
  if (providedText) {
    return providedText;
  }

  const lowerFileName = input.fileName.toLowerCase();
  const extension = extname(lowerFileName);

  if (
    input.mimeType.startsWith("text/") ||
    [".txt", ".md", ".csv", ".json", ".xml", ".html", ".htm"].includes(extension)
  ) {
    return normalizeText(input.bytes.toString("utf8"));
  }

  if (input.mimeType.includes("pdf") || extension === ".pdf") {
    const parser = new PDFParse({ data: input.bytes });
    try {
      const parsed = await parser.getText();
      return normalizeText(parsed.text ?? "");
    } finally {
      await parser.destroy().catch(() => undefined);
    }
  }

  if (
    input.mimeType.includes("wordprocessingml") ||
    extension === ".docx"
  ) {
    if (!isZipArchiveBuffer(input.bytes)) {
      return "";
    }

    try {
      const parsed = await mammoth.extractRawText({ buffer: input.bytes });
      return normalizeText(parsed.value ?? "");
    } catch {
      return "";
    }
  }

  if (isImageFile({ mimeType: input.mimeType, extension })) {
    try {
      return await extractImageTextWithLocalOcr(input.bytes);
    } catch {
      return "";
    }
  }

  const utf8Text = normalizeText(input.bytes.toString("utf8"));
  const printableRatio =
    utf8Text.length === 0
      ? 0
      : utf8Text.replace(/[^\x09\x0A\x0D\x20-\x7E\u00A0-\u024F]/g, "").length / utf8Text.length;

  return printableRatio >= 0.9 ? utf8Text : "";
}

function splitParagraphs(text: string) {
  return text
    .split(/\n{2,}/)
    .map((paragraph) => paragraph.replace(/\s+/g, " ").trim())
    .filter(Boolean);
}

function buildDocumentStructure(text: string) {
  const normalized = normalizeText(text);
  const paragraphs = splitParagraphs(normalized);
  const lines = normalized
    .split(/\n/)
    .map((line) => line.trim())
    .filter(Boolean);

  const headings = lines
    .map((line, index) => ({ line, index }))
    .filter(({ line }) => {
      const isUpperHeading = line.length > 3 && line === line.toUpperCase() && line.length < 120;
      const isColonHeading = /:$/.test(line) && line.length < 120;
      const isNumberedHeading = /^\d+(\.\d+)*[\s.-]+/.test(line);
      return isUpperHeading || isColonHeading || isNumberedHeading;
    })
    .slice(0, 20)
    .map(({ line, index }) => ({
      title: line,
      lineIndex: index,
    }));

  const dates = Array.from(
    new Set(
      Array.from(
        normalized.matchAll(
          /\b(?:\d{1,2}\/\d{1,2}\/\d{2,4}|\d{4}-\d{2}-\d{2}|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \d{1,2}, \d{4})\b/gi,
        ),
      ).map((match) => match[0]),
    ),
  ).slice(0, 12);

  const currencies = Array.from(
    new Set(
      Array.from(normalized.matchAll(/\$\s?\d[\d,]*(?:\.\d{2})?/g)).map((match) => match[0]),
    ),
  ).slice(0, 12);

  const emails = Array.from(
    new Set(
      Array.from(normalized.matchAll(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi)).map(
        (match) => match[0],
      ),
    ),
  ).slice(0, 12);

  const phoneNumbers = Array.from(
    new Set(
      Array.from(normalized.matchAll(/\+?\d[\d\s().-]{7,}\d/g)).map((match) => match[0]),
    ),
  ).slice(0, 12);

  return {
    wordCount: normalized ? normalized.split(/\s+/).length : 0,
    characterCount: normalized.length,
    paragraphCount: paragraphs.length,
    lineCount: lines.length,
    headingCount: headings.length,
    headings,
    bulletCount: lines.filter((line) => /^[-*•]\s+/.test(line)).length,
    numberedClauseCount: lines.filter((line) => /^\d+(\.\d+)*[\s.-]+/.test(line)).length,
    dates,
    currencies,
    emails,
    phoneNumbers,
    sampleParagraphs: paragraphs.slice(0, 4),
  };
}

function detectDocumentPatterns(text: string, structure: ReturnType<typeof buildDocumentStructure>) {
  const placeholderMatches = textPlaceholderPatterns
    .map((pattern) => Array.from(text.matchAll(pattern)).map((match) => match[0]))
    .flat();

  const missingInformationSignals = dedupeStrings([
    structure.wordCount < 80 ? "Documento com pouco conteúdo textual." : null,
    structure.headingCount === 0 ? "Documento sem seções ou cabeçalhos claros." : null,
    structure.dates.length === 0 ? "Nenhuma data identificada no conteúdo." : null,
    structure.paragraphCount < 3 ? "Estrutura com poucas seções ou parágrafos." : null,
  ]);

  return {
    placeholders: dedupeStrings(placeholderMatches).slice(0, 12),
    missingInformationSignals,
    entities: {
      dates: structure.dates,
      currencies: structure.currencies,
      emails: structure.emails,
      phoneNumbers: structure.phoneNumbers,
    },
  };
}

function buildComparison(input: {
  currentText: string;
  previousText: string | null;
  previousVersionNumber: number | null;
}) {
  if (!input.previousText?.trim() || input.previousVersionNumber === null) {
    return null;
  }

  const currentParagraphs = splitParagraphs(input.currentText);
  const previousParagraphs = splitParagraphs(input.previousText);
  const previousSet = new Set(previousParagraphs);
  const currentSet = new Set(currentParagraphs);
  const addedParagraphs = currentParagraphs.filter((paragraph) => !previousSet.has(paragraph));
  const removedParagraphs = previousParagraphs.filter((paragraph) => !currentSet.has(paragraph));
  const sharedParagraphs = currentParagraphs.filter((paragraph) => previousSet.has(paragraph));
  const similarityScore =
    currentParagraphs.length + previousParagraphs.length === 0
      ? 1
      : (sharedParagraphs.length * 2) / (currentParagraphs.length + previousParagraphs.length);

  return {
    previousVersionNumber: input.previousVersionNumber,
    previousWordCount: input.previousText.trim().split(/\s+/).length,
    currentWordCount: input.currentText.trim().split(/\s+/).length,
    addedParagraphCount: addedParagraphs.length,
    removedParagraphCount: removedParagraphs.length,
    sharedParagraphCount: sharedParagraphs.length,
    similarityScore: Number(similarityScore.toFixed(4)),
    addedParagraphs: addedParagraphs.slice(0, 4),
    removedParagraphs: removedParagraphs.slice(0, 4),
  };
}

function buildObjectiveSummary(input: {
  analysisType: AnalysisType;
  verdict: ReviewVerdict;
  reason: string;
}) {
  const verdictLabelByType: Record<AnalysisType, Record<ReviewVerdict, string>> = {
    pre_submission: {
      valid: "pronto para enviar à advogada",
      invalid: "não pronto para enviar à advogada",
      needs_review: "revisar antes de enviar à advogada",
    },
    submission_check: {
      valid: "pronto para seguir para revisão da advogada",
      invalid: "não pronto para seguir para revisão da advogada",
      needs_review: "ajustes necessários antes da revisão da advogada",
    },
    review_support: {
      valid: "em linha com o padrão de revisão da advogada",
      invalid: "alto risco de devolução ou reprovação pela advogada",
      needs_review: "atenção da advogada necessária",
    },
    final_validation: {
      valid: "apto para aprovação final",
      invalid: "não apto para aprovação final",
      needs_review: "revisão final necessária antes da aprovação",
    },
  };

  return `Veredito: ${verdictLabelByType[input.analysisType][input.verdict]}. ${input.reason}`;
}

function determineVerdict(input: {
  analysisType: AnalysisType;
  extractedText: string;
  risks: ReviewRisk[];
  riskLevel: RiskSeverity;
  confidence: number;
}) {
  const severeRisk = input.risks.find(
    (risk) => severityRank[risk.severity] >= severityRank.high,
  );
  const mediumRisk = input.risks.find(
    (risk) => severityRank[risk.severity] >= severityRank.medium,
  );

  if (!input.extractedText.trim()) {
    const emptyTextReasons: Record<AnalysisType, string> = {
      pre_submission:
        "Não há texto suficiente para a assistente validar a peça antes de enviar à advogada.",
      submission_check:
        "Não há texto suficiente para liberar a peça para revisão da advogada.",
      review_support:
        "A advogada não recebeu extração suficiente para revisar a peça com segurança.",
      final_validation:
        "Não há base textual suficiente para sustentar a aprovação final.",
    };

    return {
      verdict: "invalid" as const,
      reason: emptyTextReasons[input.analysisType],
    };
  }

  if (
    input.analysisType === "pre_submission" ||
    input.analysisType === "submission_check"
  ) {
    if (severeRisk || severityRank[input.riskLevel] >= severityRank.high) {
      return {
        verdict: "invalid" as const,
        reason:
          severeRisk?.description ||
          "A peça ainda apresenta falhas materiais e tende a voltar da advogada sem correção prévia.",
      };
    }

    if (
      mediumRisk ||
      severityRank[input.riskLevel] === severityRank.medium ||
      input.confidence < 0.72
    ) {
      return {
        verdict: "needs_review" as const,
        reason:
          mediumRisk?.description ||
          "A peça precisa de ajustes antes do envio para a advogada.",
      };
    }

    return {
      verdict: "valid" as const,
      reason: "A peça parece consistente o suficiente para seguir para revisão da advogada.",
    };
  }

  if (input.analysisType === "review_support") {
    if (severeRisk || input.riskLevel === "critical") {
      return {
        verdict: "invalid" as const,
        reason:
          severeRisk?.description ||
          "Há sinais fortes de que a advogada tenderá a devolver a peça na revisão.",
      };
    }

    if (
      mediumRisk ||
      severityRank[input.riskLevel] >= severityRank.high ||
      input.confidence < 0.78
    ) {
      return {
        verdict: "needs_review" as const,
        reason:
          mediumRisk?.description ||
          "Existem alertas relevantes para a advogada avaliar antes de decidir.",
      };
    }

    return {
      verdict: "valid" as const,
      reason: "A peça está alinhada com o padrão histórico esperado para revisão da advogada.",
    };
  }

  if (severeRisk || severityRank[input.riskLevel] >= severityRank.high) {
    return {
      verdict: "invalid" as const,
      reason:
        severeRisk?.description ||
        "Persistem riscos materiais que impedem aprovar a peça com segurança.",
    };
  }

  if (
    mediumRisk ||
    severityRank[input.riskLevel] === severityRank.medium ||
    input.confidence < 0.7
  ) {
    return {
      verdict: "needs_review" as const,
      reason:
        mediumRisk?.description ||
        "A aprovação final depende de revisão humana sobre pontos ainda sensíveis.",
    };
  }

  return {
    verdict: "valid" as const,
    reason: "A peça está consistente e sem sinais materiais que desaconselhem a aprovação final.",
  };
}

function promptCharacterLimitForAnalysisType(analysisType: AnalysisType) {
  if (analysisType === "review_support" || analysisType === "final_validation") {
    return 10_000;
  }

  return 6_000;
}

function buildLocalAnalysis(input: {
  analysisType: AnalysisType;
  documentName: string;
  objective: string;
  extractedText: string;
  structure: ReturnType<typeof buildDocumentStructure>;
  patterns: ReturnType<typeof detectDocumentPatterns>;
  comparison: Record<string, unknown> | null;
  historicalContext: Record<string, unknown>;
}) {
  const strengths: string[] = [];
  const weaknesses: string[] = [];
  const suggestions: string[] = [];
  const possibleRejectionReasons: string[] = [];
  const risks: ReviewRisk[] = [];
  const seededAlertPoints: string[] = [];

  const wordCount = toNumber((input.structure as { wordCount?: unknown }).wordCount, 0);
  const headingCount = toNumber((input.structure as { headingCount?: unknown }).headingCount, 0);
  const placeholderCount = (input.patterns.placeholders as string[] | undefined)?.length ?? 0;
  const historicalApprovalRate = toNumber(
    (input.historicalContext as { approvalRate?: unknown }).approvalRate,
    0,
  );
  const rejectionPatterns =
    parseJsonValue<Array<{ reason: string; count: number }>>(
      (input.historicalContext as { rejectionReasonPatterns?: unknown }).rejectionReasonPatterns,
      [],
    ) ?? [];
  const guidancePatterns =
    parseJsonValue<Array<{ guidance: string; count: number }>>(
      (input.historicalContext as { guidancePatterns?: unknown }).guidancePatterns,
      [],
    ) ?? [];
  const approvalPatterns =
    parseJsonValue<Array<{ signal: string; count: number }>>(
      (input.historicalContext as { approvalJustificationPatterns?: unknown })
        .approvalJustificationPatterns,
      [],
    ) ?? [];
  const reviewerPreferenceSignals =
    parseJsonValue<string[]>(
      (input.historicalContext as { reviewerPreferenceSignals?: unknown }).reviewerPreferenceSignals,
      [],
    ) ?? [];
  const primaryRejectionPattern = rejectionPatterns[0] ?? null;
  const primaryGuidancePattern = guidancePatterns[0] ?? null;
  const primaryApprovalPattern = approvalPatterns[0] ?? null;
  const isAssistantFacing =
    input.analysisType === "pre_submission" || input.analysisType === "submission_check";

  if (wordCount >= 250) {
    strengths.push("Conteúdo textual robusto para a revisão da advogada.");
  }

  if (headingCount >= 2) {
    strengths.push("Peça com seções identificáveis e estrutura legível.");
  }

  if (((input.patterns.entities.dates as string[] | undefined)?.length ?? 0) > 0) {
    strengths.push("O conteúdo apresenta cronologia e datas relevantes identificáveis.");
  }

  if (!input.extractedText.trim()) {
    weaknesses.push("A extração textual retornou conteúdo vazio.");
    suggestions.push(
      "Envie uma versão com texto pesquisável para que a assistente e a advogada consigam revisar a peça com segurança.",
    );
    possibleRejectionReasons.push(
      "A advogada não conseguirá revisar a peça se o conteúdo não puder ser lido pelo sistema.",
    );
    risks.push({
      title: "Texto não extraído",
      severity: "critical",
      description:
        "O sistema não conseguiu obter conteúdo textual suficiente para orientar a assistente nem apoiar a advogada.",
      recommendation: "Envie uma versão com texto pesquisável ou uma transcrição do conteúdo.",
    });
  }

  if (wordCount > 0 && wordCount < 120) {
    weaknesses.push("A minuta está curta para uma revisão segura da advogada.");
    suggestions.push("Complemente a peça com contexto, fundamentos, pedidos e dados-chave.");
    possibleRejectionReasons.push("Conteúdo insuficiente para a advogada revisar com confiança.");
    risks.push({
      title: "Baixa completude",
      severity: "high",
      description: "A peça tem pouco conteúdo e pode voltar para complementação.",
      recommendation: "Amplie o conteúdo e valide seções ou anexos ausentes.",
    });
  }

  if (headingCount === 0) {
    weaknesses.push("A peça não está organizada em seções claras.");
    suggestions.push("Estruture a minuta em seções nomeadas para facilitar a leitura da advogada.");
    risks.push({
      title: "Estrutura pouco clara",
      severity: "medium",
      description:
        "A ausência de seções torna a revisão menos confiável e aumenta a chance de a advogada pedir reorganização.",
      recommendation: "Divida o texto em blocos com títulos objetivos.",
    });
  }

  if (placeholderCount > 0) {
    const placeholders = (input.patterns.placeholders as string[]).join(", ");
    weaknesses.push("Existem placeholders ou marcadores de conteúdo incompleto na peça.");
    suggestions.push(
      "Substitua placeholders, campos em branco e marcadores temporários antes de enviar para a advogada.",
    );
    possibleRejectionReasons.push("Há campos em aberto que tendem a levar a devolução da peça.");
    risks.push({
      title: "Placeholders detectados",
      severity: "critical",
      description: `Foram encontrados marcadores temporários: ${placeholders}.`,
      recommendation: "Remova todos os placeholders e valide o texto final antes do envio.",
    });
  }

  const comparison = parseJsonValue<{
    addedParagraphCount?: number;
    removedParagraphCount?: number;
    similarityScore?: number;
    previousVersionNumber?: number;
  }>(input.comparison, {});
  if (
    input.analysisType !== "pre_submission" &&
    comparison.previousVersionNumber &&
    toNumber(comparison.removedParagraphCount, 0) > toNumber(comparison.addedParagraphCount, 0)
  ) {
    weaknesses.push("A nova versão removeu mais conteúdo do que adicionou.");
    suggestions.push("Revise o diff para garantir que a assistente não retirou fundamentos importantes.");
    risks.push({
      title: "Possível regressão",
      severity: "high",
      description: "A comparação entre versões sugere regressão de conteúdo.",
      recommendation: "Confirme se todas as correções e fundamentos esperados foram preservados.",
    });
  }

  if (
    rejectionPatterns.length > 0 ||
    guidancePatterns.length > 0 ||
    approvalPatterns.length > 0 ||
    reviewerPreferenceSignals.length > 0
  ) {
    strengths.push("Há histórico de decisões da advogada para orientar esta revisão.");
  }

  if (primaryApprovalPattern && wordCount >= 180) {
    strengths.push(
      `A peça atende a um critério que aparece em aprovações anteriores: ${formatPatternWithCount(
        primaryApprovalPattern.signal,
        primaryApprovalPattern.count,
      )}.`,
    );
  }

  if (rejectionPatterns.length > 0) {
    possibleRejectionReasons.push(
      ...rejectionPatterns
        .slice(0, 3)
        .map((item) => formatPatternWithCount(item.reason, item.count)),
    );
  }

  if (guidancePatterns.length > 0) {
    suggestions.push(
      ...guidancePatterns.slice(0, 2).map(
        (item) =>
          `Aplique a orientação recorrente da advogada: ${formatPatternWithCount(
            item.guidance,
            item.count,
          )}.`,
      ),
    );
  }

  if (reviewerPreferenceSignals.length > 0) {
    seededAlertPoints.push(...reviewerPreferenceSignals.slice(0, 3));
  }

  if (historicalApprovalRate > 0) {
    strengths.push(
      `A análise conta com base histórica da advogada, com ${Math.round(
        historicalApprovalRate * 100,
      )}% de aprovação em documentos comparáveis.`,
    );
  }

  const hasCurrentConcern =
    weaknesses.length > 0 || risks.some((risk) => severityRank[risk.severity] >= severityRank.medium);

  if (hasCurrentConcern && (primaryRejectionPattern || primaryGuidancePattern)) {
    const historicalAttentionText = primaryRejectionPattern
      ? `Histórico da advogada: ${formatPatternWithCount(
          primaryRejectionPattern.reason,
          primaryRejectionPattern.count,
        )}.`
      : `Orientação recorrente: ${formatPatternWithCount(
          primaryGuidancePattern!.guidance,
          primaryGuidancePattern!.count,
        )}.`;

    const recommendation = primaryGuidancePattern
      ? `Ajuste a peça conforme a orientação recorrente: ${primaryGuidancePattern.guidance}`
      : primaryRejectionPattern
        ? `Revise a minuta para evitar a objeção histórica: ${primaryRejectionPattern.reason}`
        : "Revise a peça antes de enviar para revisão.";

    risks.push({
      title: isAssistantFacing
        ? "Provável objeção da advogada"
        : "Ponto histórico de atenção da advogada",
      severity: input.analysisType === "final_validation" ? "high" : "medium",
      description: historicalAttentionText,
      recommendation,
    });

    seededAlertPoints.push(historicalAttentionText);
  }

  if (isAssistantFacing && risks.length === 0) {
    suggestions.push(
      "A peça parece pronta para seguir para a advogada, mas mantenha conferência final dos pedidos e anexos.",
    );
  } else if (!isAssistantFacing && risks.length === 0) {
    seededAlertPoints.push("Sem alertas materiais da IA neste momento.");
  }

  const riskLevel = maxRiskLevel(risks.map((risk) => risk.severity));
  const alertPoints = dedupeStrings(
    [
      ...seededAlertPoints,
      ...risks
        .filter((risk) => severityRank[risk.severity] >= severityRank.medium)
        .map((risk) => `${risk.title}: ${risk.description}`),
    ],
  );

  let confidence = 0.74;

  if (!input.extractedText.trim()) {
    confidence = 0.1;
  } else if (riskLevel === "critical") {
    confidence = 0.34;
  } else if (riskLevel === "high") {
    confidence = 0.52;
  } else if (riskLevel === "medium") {
    confidence = 0.68;
  } else if (wordCount >= 250 && headingCount >= 2) {
    confidence = 0.88;
  }

  const verdictDecision = determineVerdict({
    analysisType: input.analysisType,
    extractedText: input.extractedText,
    risks,
    riskLevel,
    confidence,
  });

  return {
    summary: buildObjectiveSummary({
      analysisType: input.analysisType,
      verdict: verdictDecision.verdict,
      reason: verdictDecision.reason,
    }),
    strengths: dedupeStrings(strengths),
    weaknesses: dedupeStrings(weaknesses),
    suggestions: dedupeStrings(suggestions),
    risks,
    possibleRejectionReasons: dedupeStrings(possibleRejectionReasons),
    confidence,
    confidenceLabel: confidenceLabelFromScore(confidence),
    riskLevel,
    verdict: verdictDecision.verdict,
    verdictReason: verdictDecision.reason,
    alertPoints,
  };
}

function buildAnalysisSystemPrompt(input: {
  analysisType: AnalysisType;
  reviewerName: string;
  reviewerAgentPrompt?: string | null;
}) {
  const modeInstructions: Record<AnalysisType, string> = {
    pre_submission:
      "A resposta é para a assistente jurídica antes de enviar a peça para a advogada. Antecipe o que a advogada corrigiria e priorize melhorias objetivas antes da submissão.",
    submission_check:
      "A análise está ocorrendo no envio da peça para revisão. Diga se a minuta está pronta para chegar à advogada ou se deve voltar para ajustes antes disso.",
    review_support:
      "A resposta é para a advogada revisora. Destaque alertas prioritários, inconsistências e pontos que tendem a gerar devolução ou pedido de ajustes.",
    final_validation:
      "A análise é uma validação final obrigatória antes da aprovação. Só use invalid quando houver risco material para aprovar; se houver dúvida razoável ou pendência sanável, prefira needs_review.",
  };

  return [
    `Você é a copiloto de revisão da pessoa revisora ${input.reviewerName}.`,
    "Responda apenas em JSON válido, sem markdown.",
    "Use português do Brasil.",
    "Seja objetivo e conclusivo.",
    "Não reproduza o conteúdo do documento nem faça resumo longo do texto extraído.",
    "Aprenda o estilo do reviewer a partir de aprovações, reprovações, justificativas, comentários ancorados e memórias persistidas do agente pessoal.",
    "Use o histórico como sinal de preferência do reviewer, não como regra absoluta.",
    "Avalie conteúdo, estrutura, histórico, riscos, omissões, regressões e aderência ao padrão de revisão pessoal do reviewer.",
    "Não marque invalid apenas porque existe histórico negativo; conecte o alerta ao conteúdo atual.",
    "Quando houver sinal positivo e alerta residual, evite contradição: deixe claro se a peça está pronta, não pronta ou exige revisão humana.",
    "Retorne campos: verdict, verdictReason, summary, strengths, weaknesses, suggestions, risks, possibleRejectionReasons, confidence, riskLevel, alertPoints.",
    "verdict deve ser exatamente um de: valid, invalid, needs_review.",
    "verdictReason deve ter no máximo 1 frase e explicar por que a peça está pronta, não pronta ou exige revisão humana.",
    "summary deve ter no máximo 2 frases e começar com 'Veredito:'.",
    "Cada item de risks deve conter title, severity, description e recommendation.",
    "possibleRejectionReasons deve listar objeções prováveis do reviewer ao receber a peça.",
    "alertPoints deve listar alertas curtos e prioritários para a revisão do reviewer.",
    "confidence deve ser um número entre 0 e 1.",
    "riskLevel deve ser um de: low, medium, high, critical.",
    input.reviewerAgentPrompt ? `Diretriz persistida do agente: ${input.reviewerAgentPrompt}` : null,
    modeInstructions[input.analysisType],
  ]
    .filter(Boolean)
    .join(" ");
}

function buildAnalysisUserPrompt(input: {
  analysisType: AnalysisType;
  documentName: string;
  objective: string;
  dueAt: Date;
  caseNumber: string;
  caseTitle: string;
  caseTypeCode: string;
  clientName: string;
  extractedText: string;
  structure: Record<string, unknown>;
  patterns: Record<string, unknown>;
  historicalContext: Record<string, unknown>;
  comparison: Record<string, unknown> | null;
  localAnalysis: ReturnType<typeof buildLocalAnalysis>;
  reviewerAgent: ReviewerAgentContext;
}) {
  return JSON.stringify(
    {
      analysisType: input.analysisType,
      workflowContext: {
        draftingRole: "assistente jurídica",
        reviewerRole: input.reviewerAgent.reviewerDisplayName,
        intendedAudience:
          input.analysisType === "pre_submission" || input.analysisType === "submission_check"
            ? "assistente jurídica"
            : "advogada revisora",
        objective:
          "reduzir erros da assistente antes da revisão e aproximar a análise do padrão decisório do reviewer escolhido ao longo do tempo",
      },
      metadata: {
        documentName: input.documentName,
        objective: input.objective,
        dueAt: formatDueDateForAnalysis(input.dueAt),
        caseNumber: input.caseNumber,
        caseTitle: input.caseTitle,
        caseTypeCode: input.caseTypeCode,
        clientName: input.clientName,
      },
      reviewerAgent: {
        reviewerUserId: input.reviewerAgent.reviewerUserId,
        reviewerName: input.reviewerAgent.reviewerDisplayName,
        reviewerEmail: input.reviewerAgent.reviewerEmail,
        membershipRole: input.reviewerAgent.membershipRole,
        agentId: input.reviewerAgent.agentId,
        agentName: input.reviewerAgent.agentName,
        learningSummary: input.reviewerAgent.learningSummary,
        recentMemories: input.reviewerAgent.recentMemories,
      },
      localAnalysis: input.localAnalysis,
      structure: input.structure,
      identifiedPatterns: input.patterns,
      historicalContext: input.historicalContext,
      comparison: input.comparison,
      extractedTextPreview: truncateText(
        input.extractedText,
        promptCharacterLimitForAnalysisType(input.analysisType),
      ),
    },
    null,
    2,
  );
}

function buildAnchoredCommentSystemPrompt(input: {
  reviewerName: string;
  reviewerAgentPrompt?: string | null;
}) {
  return [
    `Você é a copiloto de revisão documental do agente pessoal de ${input.reviewerName}.`,
    "Responda apenas em JSON válido, sem markdown.",
    "Use português do Brasil.",
    "Analise o comentário humano sobre um trecho específico do documento.",
    "Use apenas o trecho selecionado, o contexto imediato, o histórico fornecido e o comentário do revisor.",
    "Não invente fatos nem afirme algo que não esteja suportado pelo contexto.",
    "Se faltar contexto para concluir, diga isso de forma objetiva e indique o que precisa ser confirmado.",
    "Retorne os campos: assistantResponse, knowledgeSummary.",
    "assistantResponse deve ter no máximo 6 frases e responder diretamente ao comentário do revisor.",
    "knowledgeSummary deve ter no máximo 2 frases, ser reutilizável em análises futuras e evitar repetir dados sensíveis desnecessários.",
    input.reviewerAgentPrompt ? `Diretriz persistida do agente: ${input.reviewerAgentPrompt}` : null,
  ]
    .filter(Boolean)
    .join(" ");
}

function buildAnchoredCommentUserPrompt(input: {
  documentName: string;
  objective: string;
  caseNumber: string;
  caseTitle: string;
  caseTypeCode: string;
  clientName: string;
  selectedText: string;
  commentText: string;
  selectionStart: number | null;
  selectionEnd: number | null;
  pageNumber?: number | null;
  anchorX?: number | null;
  anchorY?: number | null;
  selectionRects?: AnchoredSelectionRect[];
  contextBefore: string;
  contextAfter: string;
  historicalContext: Record<string, unknown>;
  existingAnchoredComments: AnchoredCommentKnowledge[];
  reviewerAgent: ReviewerAgentContext;
}) {
  return JSON.stringify(
    {
      workflowContext: {
        draftingRole: "assistente jurídica",
        reviewerRole: input.reviewerAgent.reviewerDisplayName,
        intendedAudience: "equipe de revisão documental",
      },
      metadata: {
        documentName: input.documentName,
        objective: input.objective,
        caseNumber: input.caseNumber,
        caseTitle: input.caseTitle,
        caseTypeCode: input.caseTypeCode,
        clientName: input.clientName,
      },
      anchoredSelection: {
        startOffset: input.selectionStart,
        endOffset: input.selectionEnd,
        pageNumber: input.pageNumber ?? null,
        anchorX: input.anchorX ?? null,
        anchorY: input.anchorY ?? null,
        selectionRects: input.selectionRects ?? [],
        selectedText: input.selectedText,
        contextBefore: input.contextBefore,
        contextAfter: input.contextAfter,
      },
      reviewerAgent: {
        reviewerUserId: input.reviewerAgent.reviewerUserId,
        reviewerName: input.reviewerAgent.reviewerDisplayName,
        agentId: input.reviewerAgent.agentId,
        agentName: input.reviewerAgent.agentName,
        learningSummary: input.reviewerAgent.learningSummary,
        recentMemories: input.reviewerAgent.recentMemories,
      },
      reviewerComment: input.commentText,
      historicalContext: input.historicalContext,
      existingAnchoredComments: input.existingAnchoredComments.map((comment) => ({
        selectedText: truncateText(comment.selectedText, 300),
        commentText: comment.commentText,
        knowledgeSummary: comment.knowledgeSummary,
      })),
    },
    null,
    2,
  );
}

function normalizeAnchoredCommentReply(aiJson: Record<string, unknown>, fallbackKnowledge: string) {
  const assistantResponse = String(aiJson.assistantResponse ?? "").trim();
  const knowledgeSummary = String(aiJson.knowledgeSummary ?? "").trim();

  return {
    assistantResponse:
      assistantResponse || "O trecho foi registrado, mas a assistente não retornou uma resposta completa.",
    knowledgeSummary: knowledgeSummary || fallbackKnowledge,
  };
}

function normalizeRisk(value: unknown): ReviewRisk | null {
  if (!value || typeof value !== "object") {
    return null;
  }

  const title = String((value as { title?: unknown }).title ?? "").trim();
  const description = String((value as { description?: unknown }).description ?? "").trim();
  const recommendation = String(
    (value as { recommendation?: unknown }).recommendation ?? "",
  ).trim();
  const severityCandidate = String((value as { severity?: unknown }).severity ?? "medium")
    .trim()
    .toLowerCase();
  const severity: RiskSeverity = ["low", "medium", "high", "critical"].includes(severityCandidate)
    ? (severityCandidate as RiskSeverity)
    : "medium";

  if (!title || !description) {
    return null;
  }

  return {
    title,
    description,
    severity,
    recommendation: recommendation || null,
  };
}

function normalizeAiOutput(input: {
  aiJson: Record<string, unknown>;
  fallback: ReturnType<typeof buildLocalAnalysis>;
  analysisType: AnalysisType;
  extractedText: string;
}) {
  const strengths = dedupeStrings([
    ...input.fallback.strengths,
    ...parseJsonValue<string[]>(input.aiJson.strengths, []),
  ]);
  const weaknesses = dedupeStrings([
    ...input.fallback.weaknesses,
    ...parseJsonValue<string[]>(input.aiJson.weaknesses, []),
  ]);
  const suggestions = dedupeStrings([
    ...input.fallback.suggestions,
    ...parseJsonValue<string[]>(input.aiJson.suggestions, []),
  ]);
  const possibleRejectionReasons = dedupeStrings([
    ...input.fallback.possibleRejectionReasons,
    ...parseJsonValue<string[]>(input.aiJson.possibleRejectionReasons, []),
  ]);

  const aiRisks = parseJsonValue<unknown[]>(input.aiJson.risks, [])
    .map((risk) => normalizeRisk(risk))
    .filter((risk): risk is ReviewRisk => Boolean(risk));
  const risks = [...aiRisks, ...input.fallback.risks].reduce<ReviewRisk[]>((acc, risk) => {
    const key = `${risk.title.toLowerCase()}::${risk.description.toLowerCase()}`;
    if (!acc.some((item) => `${item.title.toLowerCase()}::${item.description.toLowerCase()}` === key)) {
      acc.push(risk);
    }
    return acc;
  }, []);

  const confidence = Math.min(
    1,
    Math.max(0, toNumber(input.aiJson.confidence, input.fallback.confidence)),
  );
  const riskLevelCandidate = String(input.aiJson.riskLevel ?? "").trim().toLowerCase();
  const derivedRiskLevel = maxRiskLevel(risks.map((risk) => risk.severity));
  const riskLevel: RiskSeverity = ["low", "medium", "high", "critical"].includes(riskLevelCandidate)
    ? maxRiskLevel([derivedRiskLevel, riskLevelCandidate as RiskSeverity])
    : derivedRiskLevel;

  const alertPoints = dedupeStrings([
    ...input.fallback.alertPoints,
    ...parseJsonValue<string[]>(input.aiJson.alertPoints, []),
    ...risks
      .filter((risk) => severityRank[risk.severity] >= severityRank.medium)
      .map((risk) => `${risk.title}: ${risk.description}`),
  ]);

  const aiVerdictCandidate = String(input.aiJson.verdict ?? "").trim().toLowerCase();
  const verdictDecision =
    aiVerdictCandidate === "valid" ||
    aiVerdictCandidate === "invalid" ||
    aiVerdictCandidate === "needs_review"
      ? {
          verdict: aiVerdictCandidate as ReviewVerdict,
          reason:
            String(input.aiJson.verdictReason ?? "").trim() || input.fallback.verdictReason,
        }
      : {
          verdict: input.fallback.verdict,
          reason: input.fallback.verdictReason,
        };

  const recomputedVerdict = determineVerdict({
    analysisType: input.analysisType,
    extractedText: input.extractedText,
    risks,
    riskLevel,
    confidence,
  });

  const finalVerdict =
    verdictDecision.verdict === "valid" &&
    (recomputedVerdict.verdict === "invalid" || recomputedVerdict.verdict === "needs_review")
      ? recomputedVerdict
      : verdictDecision.verdict === "needs_review" && recomputedVerdict.verdict === "invalid"
        ? recomputedVerdict
        : verdictDecision.verdict === "invalid" &&
            recomputedVerdict.verdict !== "invalid" &&
            input.analysisType !== "final_validation"
          ? {
              verdict: "needs_review" as const,
              reason: verdictDecision.reason,
            }
          : verdictDecision;

  return {
    summary: buildObjectiveSummary({
      analysisType: input.analysisType,
      verdict: finalVerdict.verdict,
      reason: finalVerdict.reason,
    }),
    strengths,
    weaknesses,
    suggestions,
    risks,
    possibleRejectionReasons,
    confidence,
    confidenceLabel: confidenceLabelFromScore(confidence),
    riskLevel,
    verdict: finalVerdict.verdict,
    verdictReason: finalVerdict.reason,
    alertPoints,
  };
}

function requiresConfirmation(analysis: {
  riskLevel: RiskSeverity;
  alertPoints: string[];
  risks: ReviewRisk[];
}) {
  return (
    severityRank[analysis.riskLevel] >= severityRank.medium ||
    analysis.risks.some((risk) => severityRank[risk.severity] >= severityRank.medium)
  );
}

function hydrateVerdictFromStoredAnalysis(input: {
  analysisType: AnalysisType;
  extractedText: string;
  risks: ReviewRisk[];
  riskLevel: RiskSeverity;
  confidence: number;
  verdictReason?: string | null;
}) {
  const derived = determineVerdict({
    analysisType: input.analysisType,
    extractedText: input.extractedText,
    risks: input.risks,
    riskLevel: input.riskLevel,
    confidence: input.confidence,
  });

  const reason = String(input.verdictReason ?? "").trim() || derived.reason;

  return {
    verdict: derived.verdict,
    verdictReason: reason,
    summary: buildObjectiveSummary({
      analysisType: input.analysisType,
      verdict: derived.verdict,
      reason,
    }),
  };
}

async function fetchCaseClientContext(db: typeof prisma, input: {
  lawFirmId: string;
  caseId: string;
  clientId: string;
}) {
  const [row] = await db.$queryRaw<
    Array<{
      case_id: string;
      case_number: string;
      case_title: string;
      case_type_code: string;
      client_id: string;
      first_name: string;
      last_name: string;
    }>
  >`
    SELECT
      c.id AS case_id,
      c.case_number,
      c.title AS case_title,
      c.case_type_code,
      cl.id AS client_id,
      cl.first_name,
      cl.last_name
    FROM cases c
    INNER JOIN clients cl ON cl.id = c.client_id
    WHERE c.id = ${input.caseId}
      AND c.client_id = ${input.clientId}
      AND c.law_firm_id = ${input.lawFirmId}
      AND cl.law_firm_id = ${input.lawFirmId}
      AND c.deleted_at IS NULL
      AND cl.deleted_at IS NULL
    LIMIT 1
  `;

  if (!row) {
    throw new DocumentReviewServiceError(404, "Caso ou cliente não encontrado para revisão.");
  }

  return {
    caseId: row.case_id,
    caseNumber: row.case_number,
    caseTitle: row.case_title,
    caseTypeCode: row.case_type_code,
    clientId: row.client_id,
    clientName: `${row.first_name} ${row.last_name}`.trim(),
  };
}

async function fetchItemContext(db: typeof prisma, input: {
  lawFirmId: string;
  itemId: string;
}) {
  const [row] = await db.$queryRaw<
    Array<{
      item_id: string;
      law_firm_id: string;
      case_id: string;
      case_number: string;
      case_title: string;
      case_type_code: string;
      client_id: string;
      first_name: string;
      last_name: string;
      document_name: string;
      objective: string;
      due_at: Date;
      current_status: string;
      latest_version_number: number;
    }>
  >`
    SELECT
      dri.id AS item_id,
      dri.law_firm_id,
      dri.case_id,
      c.case_number,
      c.title AS case_title,
      c.case_type_code,
      dri.client_id,
      cl.first_name,
      cl.last_name,
      dri.document_name,
      dri.objective,
      dri.due_at,
      dri.current_status,
      dri.latest_version_number
    FROM document_review_items dri
    INNER JOIN cases c ON c.id = dri.case_id
    INNER JOIN clients cl ON cl.id = dri.client_id
    WHERE dri.id = ${input.itemId}
      AND dri.law_firm_id = ${input.lawFirmId}
    LIMIT 1
  `;

  if (!row) {
    throw new DocumentReviewServiceError(404, "Item de revisão documental não encontrado.");
  }

  return {
    itemId: row.item_id,
    lawFirmId: row.law_firm_id,
    caseId: row.case_id,
    caseNumber: row.case_number,
    caseTitle: row.case_title,
    caseTypeCode: row.case_type_code,
    clientId: row.client_id,
    clientName: `${row.first_name} ${row.last_name}`.trim(),
    documentName: row.document_name,
    objective: row.objective,
    dueAt: row.due_at,
    currentStatus: row.current_status,
    latestVersionNumber: toNumber(row.latest_version_number, 0),
  } satisfies ItemContext;
}

async function fetchVersionContext(db: typeof prisma, input: {
  lawFirmId: string;
  itemId: string;
  versionId: string;
}) {
  const [row] = await db.$queryRaw<
    Array<{
      item_id: string;
      law_firm_id: string;
      case_id: string;
      case_number: string;
      case_title: string;
      case_type_code: string;
      client_id: string;
      first_name: string;
      last_name: string;
      document_name: string;
      objective: string;
      due_at: Date;
      current_status: string;
      version_id: string;
      version_number: number;
      file_id: string;
      original_file_name: string;
      mime_type: string;
      object_key: string;
      storage_provider: string;
      extracted_text: string | null;
      structure_json: unknown;
      identified_patterns_json: unknown;
      comparison_json: unknown;
    }>
  >`
    SELECT
      dri.id AS item_id,
      dri.law_firm_id,
      dri.case_id,
      c.case_number,
      c.title AS case_title,
      c.case_type_code,
      dri.client_id,
      cl.first_name,
      cl.last_name,
      dri.document_name,
      dri.objective,
      dri.due_at,
      dri.current_status,
      drv.id AS version_id,
      drv.version_number,
      drv.file_id,
      f.original_file_name,
      f.mime_type,
      f.object_key,
      f.storage_provider,
      drv.extracted_text,
      drv.structure_json,
      drv.identified_patterns_json,
      drv.comparison_json
    FROM document_review_versions drv
    INNER JOIN document_review_items dri ON dri.id = drv.item_id
    INNER JOIN cases c ON c.id = dri.case_id
    INNER JOIN clients cl ON cl.id = dri.client_id
    INNER JOIN files f ON f.id = drv.file_id
    WHERE drv.id = ${input.versionId}
      AND drv.item_id = ${input.itemId}
      AND drv.law_firm_id = ${input.lawFirmId}
    LIMIT 1
  `;

  if (!row) {
    throw new DocumentReviewServiceError(404, "Versão de revisão documental não encontrada.");
  }

  return {
    itemId: row.item_id,
    lawFirmId: row.law_firm_id,
    caseId: row.case_id,
    caseNumber: row.case_number,
    caseTitle: row.case_title,
    caseTypeCode: row.case_type_code,
    clientId: row.client_id,
    clientName: `${row.first_name} ${row.last_name}`.trim(),
    documentName: row.document_name,
    objective: row.objective,
    dueAt: row.due_at,
    currentStatus: row.current_status,
    versionId: row.version_id,
    versionNumber: toNumber(row.version_number, 1),
    fileId: row.file_id,
    originalFileName: row.original_file_name,
    mimeType: row.mime_type,
    objectKey: row.object_key,
    storageProvider: row.storage_provider,
    extractedText: row.extracted_text,
    structureJson: row.structure_json,
    identifiedPatternsJson: row.identified_patterns_json,
    comparisonJson: row.comparison_json,
  } satisfies VersionContext;
}

async function fetchAcceptedTeamMemberContext(db: typeof prisma, input: {
  lawFirmId: string;
  userId: string;
  teamId?: string | null;
}) {
  const [row] = await db.$queryRaw<
    Array<{
      team_id: string;
      membership_role: string;
      display_name: string | null;
      first_name: string | null;
      last_name: string | null;
      email: string;
    }>
  >`
    SELECT
      tm.team_id,
      tm.membership_role,
      u.display_name,
      u.first_name,
      u.last_name,
      u.email
    FROM team_memberships tm
    INNER JOIN users u ON u.id = tm.user_id
    WHERE tm.law_firm_id = ${input.lawFirmId}
      AND tm.user_id = ${input.userId}
      AND (${input.teamId ?? null} IS NULL OR tm.team_id = ${input.teamId ?? null})
    ORDER BY tm.joined_at DESC
    LIMIT 1
  `;

  if (!row) {
    throw new DocumentReviewServiceError(
      400,
      "Você precisa fazer parte de um team aceito antes de enviar ou revisar documentos.",
    );
  }

  return {
    teamId: row.team_id,
    membershipRole: row.membership_role,
    displayName: getUserDisplayName({
      display_name: row.display_name,
      first_name: row.first_name,
      last_name: row.last_name,
      email: row.email,
    }),
    email: row.email,
  };
}

async function fetchReviewerAgentContext(db: typeof prisma, input: {
  lawFirmId: string;
  teamId: string;
  reviewerUserId: string;
}) {
  const [row] = await db.$queryRaw<
    Array<{
      team_id: string;
      reviewer_user_id: string;
      reviewer_display_name: string | null;
      reviewer_first_name: string | null;
      reviewer_last_name: string | null;
      reviewer_email: string;
      membership_role: string;
      agent_id: string | null;
      agent_name: string | null;
      system_prompt: string | null;
      learning_summary: string | null;
    }>
  >`
    SELECT
      tm.team_id,
      tm.user_id AS reviewer_user_id,
      u.display_name AS reviewer_display_name,
      u.first_name AS reviewer_first_name,
      u.last_name AS reviewer_last_name,
      u.email AS reviewer_email,
      CASE
        WHEN EXISTS (
          SELECT 1
          FROM team_memberships owner_tm
          WHERE owner_tm.law_firm_id = ${input.lawFirmId}
            AND owner_tm.user_id = tm.user_id
            AND owner_tm.membership_role = 'owner'
        ) THEN 'owner'
        ELSE tm.membership_role
      END AS membership_role,
      ta.id AS agent_id,
      ta.agent_name,
      ta.system_prompt,
      ta.learning_summary
    FROM team_memberships tm
    INNER JOIN users u ON u.id = tm.user_id
    LEFT JOIN team_agents ta
      ON ta.id = (
        SELECT ta2.id
        FROM team_agents ta2
        WHERE ta2.law_firm_id = ${input.lawFirmId}
          AND ta2.user_id = tm.user_id
        ORDER BY ta2.created_at ASC, ta2.id ASC
        LIMIT 1
      )
    WHERE tm.law_firm_id = ${input.lawFirmId}
      AND tm.team_id = ${input.teamId}
      AND tm.user_id = ${input.reviewerUserId}
    LIMIT 1
  `;

  if (!row || !row.agent_id || !row.agent_name) {
    throw new DocumentReviewServiceError(
      400,
      "O revisor escolhido não faz parte do mesmo team ou ainda não possui agente configurado.",
    );
  }

  const memoryRows = await db.$queryRaw<Array<{ memory_text: string }>>`
    SELECT tam.memory_text
    FROM team_agent_memories tam
    INNER JOIN team_agents ta ON ta.id = tam.agent_id
    WHERE tam.law_firm_id = ${input.lawFirmId}
      AND ta.user_id = ${row.reviewer_user_id}
    ORDER BY tam.created_at DESC
    LIMIT 8
  `;

  const learningSummary = memoryRows.length
    ? memoryRows
        .slice(0, 6)
        .map((memory) => `- ${truncateInsight(memory.memory_text, 220)}`)
        .join("\n")
    : row.learning_summary;

  return {
    teamId: row.team_id,
    reviewerUserId: row.reviewer_user_id,
    reviewerDisplayName: getUserDisplayName({
      display_name: row.reviewer_display_name,
      first_name: row.reviewer_first_name,
      last_name: row.reviewer_last_name,
      email: row.reviewer_email,
    }),
    reviewerEmail: row.reviewer_email,
    membershipRole: row.membership_role,
    agentId: row.agent_id,
    agentName: row.agent_name,
    systemPrompt: row.system_prompt,
    learningSummary,
    recentMemories: memoryRows.map((memory) => memory.memory_text).filter(Boolean),
  } satisfies ReviewerAgentContext;
}

async function fetchItemAssignmentContext(db: typeof prisma, input: {
  lawFirmId: string;
  itemId: string;
}) {
  const [row] = await db.$queryRaw<
    Array<{
      team_id: string | null;
      assigned_reviewer_user_id: string | null;
      reviewer_display_name: string | null;
      reviewer_first_name: string | null;
      reviewer_last_name: string | null;
      reviewer_email: string | null;
      assigned_reviewer_agent_id: string | null;
      agent_name: string | null;
      assigned_by_user_id: string | null;
      assigned_by_display_name: string | null;
      assigned_by_first_name: string | null;
      assigned_by_last_name: string | null;
      assigned_by_email: string | null;
      assigned_at: Date | null;
    }>
  >`
    SELECT
      dri.team_id,
      dri.assigned_reviewer_user_id,
      ru.display_name AS reviewer_display_name,
      ru.first_name AS reviewer_first_name,
      ru.last_name AS reviewer_last_name,
      ru.email AS reviewer_email,
      dri.assigned_reviewer_agent_id,
      ta.agent_name,
      dri.assigned_by_user_id,
      au.display_name AS assigned_by_display_name,
      au.first_name AS assigned_by_first_name,
      au.last_name AS assigned_by_last_name,
      au.email AS assigned_by_email,
      dri.assigned_at
    FROM document_review_items dri
    LEFT JOIN users ru ON ru.id = dri.assigned_reviewer_user_id
    LEFT JOIN team_agents ta ON ta.id = dri.assigned_reviewer_agent_id
    LEFT JOIN users au ON au.id = dri.assigned_by_user_id
    WHERE dri.id = ${input.itemId}
      AND dri.law_firm_id = ${input.lawFirmId}
    LIMIT 1
  `;

  if (!row) {
    throw new DocumentReviewServiceError(404, "Item de revisão documental não encontrado.");
  }

  return {
    teamId: row.team_id,
    assignedReviewerUserId: row.assigned_reviewer_user_id,
    assignedReviewerDisplayName:
      row.assigned_reviewer_user_id && row.reviewer_email
        ? getUserDisplayName({
            display_name: row.reviewer_display_name,
            first_name: row.reviewer_first_name,
            last_name: row.reviewer_last_name,
            email: row.reviewer_email,
          })
        : null,
    assignedReviewerEmail: row.reviewer_email,
    assignedReviewerAgentId: row.assigned_reviewer_agent_id,
    assignedReviewerAgentName: row.agent_name,
    assignedByUserId: row.assigned_by_user_id,
    assignedByDisplayName:
      row.assigned_by_user_id && row.assigned_by_email
        ? getUserDisplayName({
            display_name: row.assigned_by_display_name,
            first_name: row.assigned_by_first_name,
            last_name: row.assigned_by_last_name,
            email: row.assigned_by_email,
          })
        : null,
    assignedAt: row.assigned_at,
  } satisfies ItemAssignmentContext;
}

function attachReviewerAgentContext(
  historicalContext: Record<string, unknown>,
  reviewerAgent: ReviewerAgentContext,
) {
  return {
    ...historicalContext,
    reviewerAgent: {
      reviewerUserId: reviewerAgent.reviewerUserId,
      reviewerName: reviewerAgent.reviewerDisplayName,
      reviewerEmail: reviewerAgent.reviewerEmail,
      membershipRole: reviewerAgent.membershipRole,
      agentId: reviewerAgent.agentId,
      agentName: reviewerAgent.agentName,
      systemPrompt: reviewerAgent.systemPrompt,
      learningSummary: reviewerAgent.learningSummary,
      recentMemories: reviewerAgent.recentMemories,
    },
  };
}

function ensureAssignedReviewer(input: {
  actorUserId: string;
  assignment: ItemAssignmentContext;
  reviewerActionLabel: string;
}) {
  if (!input.assignment.assignedReviewerUserId) {
    throw new DocumentReviewServiceError(
      400,
      "Este documento ainda não possui um reviewer atribuído no team.",
    );
  }

  if (input.assignment.assignedReviewerUserId !== input.actorUserId) {
    throw new DocumentReviewServiceError(
      403,
      `Apenas o reviewer atribuído pode ${input.reviewerActionLabel}.`,
    );
  }
}

async function fetchPreviousVersionData(db: typeof prisma, input: {
  itemId: string;
}) {
  const rows = await db.$queryRaw<
    Array<{
      id: string;
      version_number: number;
      extracted_text: string | null;
    }>
  >`
    SELECT id, version_number, extracted_text
    FROM document_review_versions
    WHERE item_id = ${input.itemId}
      AND submission_status <> 'cancelled'
    ORDER BY version_number DESC
    LIMIT 1
  `;

  const latest = rows[0];
  if (!latest) {
    return null;
  }

  return {
    versionId: latest.id,
    versionNumber: toNumber(latest.version_number, 1),
    extractedText: latest.extracted_text,
  };
}

async function fetchHistoricalContext(db: typeof prisma, input: {
  lawFirmId: string;
  documentName: string;
  caseTypeCode: string;
  excludeItemId?: string | null;
  reviewerUserId?: string | null;
}) {
  const rows = await db.$queryRaw<HistoricalDecisionRow[]>`
    SELECT
      dri.id AS item_id,
      dri.document_name,
      dri.current_status,
      c.case_number,
      c.title AS case_title,
      c.case_type_code,
      drd.decision_code,
      drd.justification,
      drd.rejection_reason,
      drd.guidance_for_new_version,
      drd.created_at AS decision_created_at
    FROM document_review_items dri
    INNER JOIN cases c ON c.id = dri.case_id
    LEFT JOIN document_review_decisions drd ON drd.item_id = dri.id
    WHERE dri.law_firm_id = ${input.lawFirmId}
      AND (${input.excludeItemId ?? null} IS NULL OR dri.id <> ${input.excludeItemId ?? null})
      AND (${input.reviewerUserId ?? null} IS NULL OR drd.reviewer_user_id = ${input.reviewerUserId ?? null})
      AND (
        LOWER(dri.document_name) = LOWER(${input.documentName})
        OR c.case_type_code = ${input.caseTypeCode}
      )
    ORDER BY COALESCE(drd.created_at, dri.created_at) DESC
    LIMIT 40
  `;

  const approvedDocuments = rows.filter((row) => row.decision_code === "approved");
  const rejectedDocuments = rows.filter((row) => row.decision_code === "rejected");
  const rejectionReasonPatterns = buildTextPatterns(
    rejectedDocuments.map((row) => row.rejection_reason ?? row.justification ?? ""),
    "reason",
  );
  const guidancePatterns = buildTextPatterns(
    rejectedDocuments.map((row) => row.guidance_for_new_version ?? ""),
    "guidance",
  );
  const approvalJustificationPatterns = buildTextPatterns(
    approvedDocuments.map((row) => row.justification ?? ""),
    "signal",
  );
  const reviewerPreferenceSignals = dedupeStrings([
    rejectionReasonPatterns[0]
      ? `A advogada costuma devolver peças por: ${formatPatternWithCount(
          rejectionReasonPatterns[0].reason,
          rejectionReasonPatterns[0].count,
        )}.`
      : null,
    guidancePatterns[0]
      ? `Orientação recorrente da advogada: ${formatPatternWithCount(
          guidancePatterns[0].guidance,
          guidancePatterns[0].count,
        )}.`
      : null,
    approvalJustificationPatterns[0]
      ? `Critério recorrente de aprovação: ${formatPatternWithCount(
          approvalJustificationPatterns[0].signal,
          approvalJustificationPatterns[0].count,
        )}.`
      : null,
  ]);
  const decisionCount = approvedDocuments.length + rejectedDocuments.length;
  const approvalRate = decisionCount === 0 ? 0 : approvedDocuments.length / decisionCount;

  return {
    approvedCount: approvedDocuments.length,
    rejectedCount: rejectedDocuments.length,
    approvalRate: Number(approvalRate.toFixed(4)),
    rejectionReasonPatterns,
    guidancePatterns,
    approvalJustificationPatterns,
    reviewerPreferenceSignals,
    recentApprovedDocuments: approvedDocuments.slice(0, 5).map((row) => ({
      itemId: row.item_id,
      documentName: row.document_name,
      caseNumber: row.case_number,
      caseTitle: row.case_title,
      decisionAt: row.decision_created_at,
    })),
    recentRejectedDocuments: rejectedDocuments.slice(0, 5).map((row) => ({
      itemId: row.item_id,
      documentName: row.document_name,
      caseNumber: row.case_number,
      caseTitle: row.case_title,
      reason: row.rejection_reason ?? row.justification ?? null,
      guidance: row.guidance_for_new_version,
      decisionAt: row.decision_created_at,
    })),
  };
}

async function fetchAnchoredCommentKnowledge(db: typeof prisma, input: {
  lawFirmId: string;
  itemId: string;
  versionId?: string | null;
  limit?: number;
}) {
  const safeLimit = Math.max(1, Math.min(20, input.limit ?? 12));
  const rows = await db.$queryRaw<
    Array<{
      id: string;
      version_id: string;
      selected_text: string;
      comment_text: string;
      assistant_response: string;
      knowledge_summary: string | null;
      created_at: Date;
      created_display_name: string | null;
      created_first_name: string | null;
      created_last_name: string | null;
      created_email: string | null;
    }>
  >`
    SELECT
      drc.id,
      drc.version_id,
      drc.selected_text,
      drc.comment_text,
      drc.assistant_response,
      drc.knowledge_summary,
      drc.created_at,
      cu.display_name AS created_display_name,
      cu.first_name AS created_first_name,
      cu.last_name AS created_last_name,
      cu.email AS created_email
    FROM document_review_comments drc
    LEFT JOIN users cu ON cu.id = drc.created_by_user_id
    WHERE drc.law_firm_id = ${input.lawFirmId}
      AND drc.item_id = ${input.itemId}
      AND (${input.versionId ?? null} IS NULL OR drc.version_id = ${input.versionId ?? null})
    ORDER BY drc.created_at DESC
    LIMIT ${safeLimit}
  `;

  return rows.map((row) => ({
    id: row.id,
    versionId: row.version_id,
    selectedText: row.selected_text,
    commentText: row.comment_text,
    assistantResponse: row.assistant_response,
    knowledgeSummary: row.knowledge_summary,
    createdAt: row.created_at,
    createdBy: getUserDisplayName({
      display_name: row.created_display_name,
      first_name: row.created_first_name,
      last_name: row.created_last_name,
      email: row.created_email,
    }),
  })) satisfies AnchoredCommentKnowledge[];
}

function attachAnchoredCommentKnowledge(
  historicalContext: Record<string, unknown>,
  anchoredComments: AnchoredCommentKnowledge[],
) {
  if (anchoredComments.length === 0) {
    return historicalContext;
  }

  return {
    ...historicalContext,
    anchoredReviewerComments: anchoredComments.map((comment) => ({
      versionId: comment.versionId,
      selectedText: truncateText(comment.selectedText, 320),
      commentText: comment.commentText,
      assistantResponse: truncateText(comment.assistantResponse, 420),
      knowledgeSummary: comment.knowledgeSummary,
      createdAt: comment.createdAt,
      createdBy: comment.createdBy,
    })),
  };
}

async function findReusableAnalysis(db: typeof prisma, input: {
  lawFirmId: string;
  analysisId: string | null | undefined;
  analysisTypes: AnalysisType[];
  checksum: string;
  expectedSource: Record<string, unknown>;
}) {
  if (!input.analysisId || input.analysisTypes.length === 0) {
    return null;
  }

  const [row] = await db.$queryRaw<
    Array<{
      id: string;
      ai_run_id: string | null;
      analysis_type: string;
      summary: string | null;
      risks: unknown;
      strengths: unknown;
      weaknesses: unknown;
      suggestions: unknown;
      possible_rejection_reasons: unknown;
      confidence: unknown;
      confidence_label: string | null;
      triggered_manually: number;
      extracted_text: string | null;
      structure_json: unknown;
      identified_patterns_json: unknown;
      historical_context_json: unknown;
      comparison_json: unknown;
      source_payload_json: unknown;
      alert_points_json: unknown;
      risk_level: string | null;
      raw_result_json: unknown;
    }>
  >`
    SELECT
      id,
      ai_run_id,
      analysis_type,
      summary,
      risks,
      strengths,
      weaknesses,
      suggestions,
      possible_rejection_reasons,
      confidence,
      confidence_label,
      triggered_manually,
      extracted_text,
      structure_json,
      identified_patterns_json,
      historical_context_json,
      comparison_json,
      source_payload_json,
      alert_points_json,
      risk_level,
      raw_result_json
    FROM document_review_ai_analyses
    WHERE id = ${input.analysisId}
      AND law_firm_id = ${input.lawFirmId}
      AND analysis_type IN (${Prisma.join(input.analysisTypes)})
      AND item_id IS NULL
      AND version_id IS NULL
    LIMIT 1
  `;

  if (!row) {
    return null;
  }

  const sourcePayload = parseJsonValue<Record<string, unknown>>(row.source_payload_json, {});
  if (String(sourcePayload.checksum ?? "") !== input.checksum) {
    return null;
  }

  for (const [key, value] of Object.entries(input.expectedSource)) {
    if (String(sourcePayload[key] ?? "") !== String(value ?? "")) {
      return null;
    }
  }

  const risks = parseJsonValue<unknown[]>(row.risks, [])
    .map((risk) => normalizeRisk(risk))
    .filter((risk): risk is ReviewRisk => Boolean(risk));
  const confidence = toNumber(row.confidence, 0);
  const riskLevel = String(row.risk_level ?? "low") as RiskSeverity;
  const extractedText = String(row.extracted_text ?? "");
  const rawResult = parseJsonValue<Record<string, unknown>>(row.raw_result_json, {});
  const verdict = hydrateVerdictFromStoredAnalysis({
    analysisType: String(row.analysis_type) as AnalysisType,
    extractedText,
    risks,
    riskLevel,
    confidence,
    verdictReason: String(rawResult.verdictReason ?? ""),
  });

  return {
    id: row.id,
    aiRunId: row.ai_run_id,
    analysisType: String(row.analysis_type) as AnalysisType,
    summary: verdict.summary,
    strengths: parseJsonValue<string[]>(row.strengths, []),
    weaknesses: parseJsonValue<string[]>(row.weaknesses, []),
    suggestions: parseJsonValue<string[]>(row.suggestions, []),
    risks,
    possibleRejectionReasons: parseJsonValue<string[]>(row.possible_rejection_reasons, []),
    confidence,
    confidenceLabel: (String(row.confidence_label ?? "low") as "low" | "medium" | "high"),
    riskLevel,
    verdict: verdict.verdict,
    verdictReason: verdict.verdictReason,
    alertPoints: parseJsonValue<string[]>(row.alert_points_json, []),
    extractedText,
    structure: parseJsonValue<Record<string, unknown>>(row.structure_json, {}),
    identifiedPatterns: parseJsonValue<Record<string, unknown>>(row.identified_patterns_json, {}),
    historicalContext: parseJsonValue<Record<string, unknown>>(row.historical_context_json, {}),
    comparison: parseJsonValue<Record<string, unknown> | null>(row.comparison_json, null),
    sourcePayload,
    rawResult,
    triggeredManually: Boolean(row.triggered_manually),
    requiresConfirmation: requiresConfirmation({
      riskLevel: String(row.risk_level ?? "low") as RiskSeverity,
      alertPoints: parseJsonValue<string[]>(row.alert_points_json, []),
      risks: parseJsonValue<unknown[]>(row.risks, [])
        .map((risk) => normalizeRisk(risk))
        .filter((risk): risk is ReviewRisk => Boolean(risk)),
    }),
  } satisfies ReviewAnalysis;
}

async function persistDetachedAnalysis(db: typeof prisma, input: {
  lawFirmId: string;
  actorUserId: string;
  analysis: ReviewAnalysis;
}) {
  const id = createId();

  await db.$executeRaw`
    INSERT INTO document_review_ai_analyses (
      id, law_firm_id, item_id, version_id, ai_run_id, analysis_type, summary, risks, strengths,
      weaknesses, suggestions, possible_rejection_reasons, confidence, confidence_label,
      triggered_manually, extracted_text, structure_json, identified_patterns_json,
      historical_context_json, comparison_json, source_payload_json, alert_points_json,
      risk_level, raw_result_json, created_by_user_id, created_at
    ) VALUES (
      ${id},
      ${input.lawFirmId},
      NULL,
      NULL,
      ${input.analysis.aiRunId},
      ${input.analysis.analysisType},
      ${input.analysis.summary},
      ${JSON.stringify(input.analysis.risks)},
      ${JSON.stringify(input.analysis.strengths)},
      ${JSON.stringify(input.analysis.weaknesses)},
      ${JSON.stringify(input.analysis.suggestions)},
      ${JSON.stringify(input.analysis.possibleRejectionReasons)},
      ${input.analysis.confidence},
      ${input.analysis.confidenceLabel},
      ${input.analysis.triggeredManually ? 1 : 0},
      ${input.analysis.extractedText},
      ${JSON.stringify(input.analysis.structure)},
      ${JSON.stringify(input.analysis.identifiedPatterns)},
      ${JSON.stringify(input.analysis.historicalContext)},
      ${input.analysis.comparison ? JSON.stringify(input.analysis.comparison) : null},
      ${input.analysis.sourcePayload ? JSON.stringify(input.analysis.sourcePayload) : null},
      ${JSON.stringify(input.analysis.alertPoints)},
      ${input.analysis.riskLevel},
      ${JSON.stringify(input.analysis.rawResult)},
      ${input.actorUserId},
      CURRENT_TIMESTAMP
    )
  `;

  return {
    ...input.analysis,
    id,
  } satisfies PersistedAnalysis;
}

async function linkAnalysisToVersion(
  tx: Prisma.TransactionClient | typeof prisma,
  input: {
  analysis: PersistedAnalysis | ReviewAnalysis;
  lawFirmId: string;
  actorUserId: string;
  itemId: string;
  versionId: string;
},
) {
  if (input.analysis.id) {
    await tx.$executeRaw`
      UPDATE document_review_ai_analyses
      SET item_id = ${input.itemId},
          version_id = ${input.versionId}
      WHERE id = ${input.analysis.id}
        AND law_firm_id = ${input.lawFirmId}
    `;

    return input.analysis.id;
  }

  const id = createId();
  await tx.$executeRaw`
    INSERT INTO document_review_ai_analyses (
      id, law_firm_id, item_id, version_id, ai_run_id, analysis_type, summary, risks, strengths,
      weaknesses, suggestions, possible_rejection_reasons, confidence, confidence_label,
      triggered_manually, extracted_text, structure_json, identified_patterns_json,
      historical_context_json, comparison_json, source_payload_json, alert_points_json,
      risk_level, raw_result_json, created_by_user_id, created_at
    ) VALUES (
      ${id},
      ${input.lawFirmId},
      ${input.itemId},
      ${input.versionId},
      ${input.analysis.aiRunId},
      ${input.analysis.analysisType},
      ${input.analysis.summary},
      ${JSON.stringify(input.analysis.risks)},
      ${JSON.stringify(input.analysis.strengths)},
      ${JSON.stringify(input.analysis.weaknesses)},
      ${JSON.stringify(input.analysis.suggestions)},
      ${JSON.stringify(input.analysis.possibleRejectionReasons)},
      ${input.analysis.confidence},
      ${input.analysis.confidenceLabel},
      ${input.analysis.triggeredManually ? 1 : 0},
      ${input.analysis.extractedText},
      ${JSON.stringify(input.analysis.structure)},
      ${JSON.stringify(input.analysis.identifiedPatterns)},
      ${JSON.stringify(input.analysis.historicalContext)},
      ${input.analysis.comparison ? JSON.stringify(input.analysis.comparison) : null},
      ${input.analysis.sourcePayload ? JSON.stringify(input.analysis.sourcePayload) : null},
      ${JSON.stringify(input.analysis.alertPoints)},
      ${input.analysis.riskLevel},
      ${JSON.stringify(input.analysis.rawResult)},
      ${input.actorUserId},
      CURRENT_TIMESTAMP
    )
  `;

  return id;
}

async function runAiBackedAnalysis(deps: DocumentReviewDependencies, input: {
  lawFirmId: string;
  clientId: string;
  caseId: string;
  actorUserId: string;
  analysisType: AnalysisType;
  triggeredManually: boolean;
  documentName: string;
  objective: string;
  dueAt: Date;
  caseNumber: string;
  caseTitle: string;
  caseTypeCode: string;
  clientName: string;
  extractedText: string;
  structure: Record<string, unknown>;
  identifiedPatterns: Record<string, unknown>;
  historicalContext: Record<string, unknown>;
  comparison: Record<string, unknown> | null;
  sourcePayload: Record<string, unknown> | null;
  reviewerAgent: ReviewerAgentContext;
}) {
  const localAnalysis = buildLocalAnalysis({
    analysisType: input.analysisType,
    documentName: input.documentName,
    objective: input.objective,
    extractedText: input.extractedText,
    structure: buildDocumentStructure(input.extractedText),
    patterns: detectDocumentPatterns(input.extractedText, buildDocumentStructure(input.extractedText)),
    comparison: input.comparison,
    historicalContext: input.historicalContext,
  });

  const aiRun = await deps.createAiRun({
    lawFirmId: input.lawFirmId,
    caseId: input.caseId,
    clientId: input.clientId,
    runType: analysisRunTypeByType[input.analysisType],
  });

  try {
    const aiResponse = await deps.runJsonChatCompletion({
      lawFirmId: input.lawFirmId,
      systemPrompt: buildAnalysisSystemPrompt({
        analysisType: input.analysisType,
        reviewerName: input.reviewerAgent.reviewerDisplayName,
        reviewerAgentPrompt: input.reviewerAgent.systemPrompt,
      }),
      userPrompt: buildAnalysisUserPrompt({
        analysisType: input.analysisType,
        documentName: input.documentName,
        objective: input.objective,
        dueAt: input.dueAt,
        caseNumber: input.caseNumber,
        caseTitle: input.caseTitle,
        caseTypeCode: input.caseTypeCode,
        clientName: input.clientName,
        extractedText: input.extractedText,
        structure: input.structure,
        patterns: input.identifiedPatterns,
        historicalContext: input.historicalContext,
        comparison: input.comparison,
        localAnalysis,
        reviewerAgent: input.reviewerAgent,
      }),
      maxCompletionTokens: 2200,
    });

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

    const normalized = normalizeAiOutput({
      aiJson: aiResponse.json,
      fallback: localAnalysis,
      analysisType: input.analysisType,
      extractedText: input.extractedText,
    });

    return {
      id: null,
      aiRunId: aiRun.id,
      analysisType: input.analysisType,
      summary: normalized.summary,
      strengths: normalized.strengths,
      weaknesses: normalized.weaknesses,
      suggestions: normalized.suggestions,
      risks: normalized.risks,
      possibleRejectionReasons: normalized.possibleRejectionReasons,
      confidence: normalized.confidence,
      confidenceLabel: normalized.confidenceLabel,
      riskLevel: normalized.riskLevel,
      verdict: normalized.verdict,
      verdictReason: normalized.verdictReason,
      alertPoints: normalized.alertPoints,
      extractedText: input.extractedText,
      structure: input.structure,
      identifiedPatterns: input.identifiedPatterns,
      historicalContext: input.historicalContext,
      comparison: input.comparison,
      sourcePayload: input.sourcePayload,
      rawResult: aiResponse.json,
      triggeredManually: input.triggeredManually,
      requiresConfirmation: requiresConfirmation({
        riskLevel: normalized.riskLevel,
        alertPoints: normalized.alertPoints,
        risks: normalized.risks,
      }),
    } satisfies ReviewAnalysis;
  } catch (error) {
    await deps.finishAiRun({
      aiRunId: aiRun.id,
      status: "failed",
      errorMessage: error instanceof Error ? error.message : "AI analysis failed",
    });
    throw error;
  }
}

async function runAnchoredCommentReply(deps: DocumentReviewDependencies, input: {
  lawFirmId: string;
  clientId: string;
  caseId: string;
  documentName: string;
  objective: string;
  caseNumber: string;
  caseTitle: string;
  caseTypeCode: string;
  clientName: string;
  selectedText: string;
  commentText: string;
  selectionStart: number | null;
  selectionEnd: number | null;
  pageNumber?: number | null;
  anchorX?: number | null;
  anchorY?: number | null;
  selectionRects?: AnchoredSelectionRect[];
  contextBefore: string;
  contextAfter: string;
  historicalContext: Record<string, unknown>;
  existingAnchoredComments: AnchoredCommentKnowledge[];
  reviewerAgent: ReviewerAgentContext;
}) {
  const aiRun = await deps.createAiRun({
    lawFirmId: input.lawFirmId,
    caseId: input.caseId,
    clientId: input.clientId,
    runType: "review_comment_reply",
  });

  const fallbackKnowledge = truncateText(
    `Registrar a observação sobre o trecho "${input.selectedText}" e confirmar o ajuste pedido: ${input.commentText}.`,
    280,
  );

  try {
    const aiResponse = await deps.runJsonChatCompletion({
      lawFirmId: input.lawFirmId,
      systemPrompt: buildAnchoredCommentSystemPrompt({
        reviewerName: input.reviewerAgent.reviewerDisplayName,
        reviewerAgentPrompt: input.reviewerAgent.systemPrompt,
      }),
      userPrompt: buildAnchoredCommentUserPrompt({
        documentName: input.documentName,
        objective: input.objective,
        caseNumber: input.caseNumber,
        caseTitle: input.caseTitle,
        caseTypeCode: input.caseTypeCode,
        clientName: input.clientName,
        selectedText: input.selectedText,
        commentText: input.commentText,
        selectionStart: input.selectionStart,
        selectionEnd: input.selectionEnd,
        pageNumber: input.pageNumber ?? null,
        anchorX: input.anchorX ?? null,
        anchorY: input.anchorY ?? null,
        selectionRects: input.selectionRects ?? [],
        contextBefore: input.contextBefore,
        contextAfter: input.contextAfter,
        historicalContext: input.historicalContext,
        existingAnchoredComments: input.existingAnchoredComments,
        reviewerAgent: input.reviewerAgent,
      }),
      maxCompletionTokens: 900,
    });

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

    const normalized = normalizeAnchoredCommentReply(aiResponse.json, fallbackKnowledge);

    return {
      aiRunId: aiRun.id,
      assistantResponse: normalized.assistantResponse,
      knowledgeSummary: normalized.knowledgeSummary,
      rawResult: aiResponse.json,
    };
  } catch (error) {
    await deps.finishAiRun({
      aiRunId: aiRun.id,
      status: "failed",
      errorMessage: error instanceof Error ? error.message : "Anchored comment reply failed",
    });
    throw error;
  }
}

async function createVersionRecords(
  tx: Prisma.TransactionClient,
  deps: DocumentReviewDependencies,
  input: {
    lawFirmId: string;
    caseId: string;
    clientId: string;
    actorUserId: string;
    itemId: string;
    versionId: string;
    versionNumber: number;
    documentName: string;
    originalFileName: string;
    mimeType: string;
    fileBuffer: Buffer;
    extractedText: string;
    structure: Record<string, unknown>;
    identifiedPatterns: Record<string, unknown>;
    historicalContext: Record<string, unknown>;
    comparison: Record<string, unknown> | null;
    analysis: ReviewAnalysis | PersistedAnalysis;
    submissionNote?: string | null;
    previousVersionId?: string | null;
  },
) {
  const stored = await deps.saveBinaryFile({
    lawFirmId: input.lawFirmId,
    caseId: input.caseId,
    fileName: input.originalFileName,
    bytes: input.fileBuffer,
    kind: "uploads",
  });

  const repositoryItemId = createId();
  const fileId = createId();

  await tx.$executeRaw`
    INSERT INTO repository_items (
      id, law_firm_id, client_id, case_id, item_type_code, channel_code, source_entity_type,
      source_entity_id, subject, body_text, summary_text, metadata_json, authored_by_user_id,
      created_by_user_id, occurred_at, created_at, updated_at
    ) VALUES (
      ${repositoryItemId},
      ${input.lawFirmId},
      ${input.clientId},
      ${input.caseId},
      'document',
      'internal',
      'document_review_version',
      ${input.versionId},
      ${input.documentName},
      ${truncateText(input.extractedText, 4_000)},
      ${`Revisão documental v${input.versionNumber}`},
      ${JSON.stringify({
        itemId: input.itemId,
        versionId: input.versionId,
        versionNumber: input.versionNumber,
      })},
      ${input.actorUserId},
      ${input.actorUserId},
      NOW(),
      CURRENT_TIMESTAMP,
      CURRENT_TIMESTAMP
    )
  `;

  await tx.$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},
      ${input.lawFirmId},
      ${input.clientId},
      ${input.caseId},
      ${repositoryItemId},
      'local_dev',
      'workspace',
      ${stored.relativeObjectKey},
      'local',
      ${input.originalFileName},
      ${stored.storedFileName},
      ${input.mimeType},
      ${input.fileBuffer.length},
      ${stored.checksumSha256},
      0,
      ${input.actorUserId},
      NOW(),
      CURRENT_TIMESTAMP
    )
  `;

  await tx.$executeRaw`
    INSERT INTO document_review_versions (
      id, law_firm_id, item_id, case_id, client_id, file_id, previous_version_id, version_number,
      submission_status, submission_note, extracted_text, structure_json, identified_patterns_json,
      historical_context_json, comparison_json, ai_alerts_json, possible_rejection_reasons,
      has_ai_alert, risk_level, last_analysis_confidence, submitted_by_user_id, submitted_at,
      created_at, updated_at
    ) VALUES (
      ${input.versionId},
      ${input.lawFirmId},
      ${input.itemId},
      ${input.caseId},
      ${input.clientId},
      ${fileId},
      ${input.previousVersionId ?? null},
      ${input.versionNumber},
      'submitted',
      ${input.submissionNote ?? null},
      ${input.extractedText},
      ${JSON.stringify(input.structure)},
      ${JSON.stringify(input.identifiedPatterns)},
      ${JSON.stringify(input.historicalContext)},
      ${input.comparison ? JSON.stringify(input.comparison) : null},
      ${JSON.stringify(input.analysis.alertPoints)},
      ${JSON.stringify(input.analysis.possibleRejectionReasons)},
      ${input.analysis.requiresConfirmation ? 1 : 0},
      ${input.analysis.riskLevel},
      ${input.analysis.confidence},
      ${input.actorUserId},
      NOW(),
      CURRENT_TIMESTAMP,
      CURRENT_TIMESTAMP
    )
  `;

  const analysisId = await linkAnalysisToVersion(tx, {
    analysis: input.analysis,
    lawFirmId: input.lawFirmId,
    actorUserId: input.actorUserId,
    itemId: input.itemId,
    versionId: input.versionId,
  });

  return {
    repositoryItemId,
    fileId,
    analysisId,
  };
}

async function fetchLinkedAnalysis(db: typeof prisma, analysisId: string) {
  const [row] = await db.$queryRaw<
    Array<{
      id: string;
      item_id: string | null;
      version_id: string | null;
      analysis_type: string;
      confidence: unknown;
      risk_level: string | null;
      summary: string | null;
      alert_points_json: unknown;
    }>
  >`
    SELECT id, item_id, version_id, analysis_type, confidence, risk_level, summary, alert_points_json
    FROM document_review_ai_analyses
    WHERE id = ${analysisId}
    LIMIT 1
  `;

  return row ?? null;
}

function buildSourcePayload(input: Record<string, unknown>) {
  return Object.fromEntries(
    Object.entries(input).map(([key, value]) => [key, value ?? ""]),
  );
}

async function insertAssignmentRecord(
  tx: Prisma.TransactionClient,
  input: {
    lawFirmId: string;
    itemId: string;
    versionId?: string | null;
    teamId: string;
    assignmentType: "initial" | "transfer" | "resubmission";
    fromReviewerUserId?: string | null;
    fromReviewerAgentId?: string | null;
    toReviewerUserId: string;
    toReviewerAgentId: string;
    assignedByUserId: string;
    transferNote?: string | null;
  },
) {
  await tx.$executeRaw`
    INSERT INTO document_review_assignments (
      id, law_firm_id, item_id, version_id, team_id, assignment_type,
      from_reviewer_user_id, from_reviewer_agent_id, to_reviewer_user_id, to_reviewer_agent_id,
      assigned_by_user_id, transfer_note, created_at
    ) VALUES (
      ${createId()},
      ${input.lawFirmId},
      ${input.itemId},
      ${input.versionId ?? null},
      ${input.teamId},
      ${input.assignmentType},
      ${input.fromReviewerUserId ?? null},
      ${input.fromReviewerAgentId ?? null},
      ${input.toReviewerUserId},
      ${input.toReviewerAgentId},
      ${input.assignedByUserId},
      ${input.transferNote ? String(input.transferNote).trim() : null},
      CURRENT_TIMESTAMP
    )
  `;
}

async function appendAgentMemory(
  db: typeof prisma,
  input: {
    lawFirmId: string;
    teamId: string;
    agentId: string;
    sourceType: string;
    sourceEntityId?: string | null;
    memoryText: string;
    createdByUserId: string;
  },
) {
  const memoryText = String(input.memoryText ?? "").trim();
  if (!memoryText) {
    return;
  }

  await db.$executeRaw`
    INSERT INTO team_agent_memories (
      id, law_firm_id, team_id, agent_id, source_type, source_entity_id,
      memory_text, created_by_user_id, created_at
    ) VALUES (
      ${createId()},
      ${input.lawFirmId},
      ${input.teamId},
      ${input.agentId},
      ${input.sourceType},
      ${input.sourceEntityId ?? null},
      ${memoryText},
      ${input.createdByUserId},
      CURRENT_TIMESTAMP
    )
  `;

  const [agentOwner] = await db.$queryRaw<Array<{ user_id: string }>>`
    SELECT user_id
    FROM team_agents
    WHERE law_firm_id = ${input.lawFirmId}
      AND id = ${input.agentId}
    LIMIT 1
  `;

  if (!agentOwner) {
    return;
  }

  const memoryRows = await db.$queryRaw<Array<{ memory_text: string }>>`
    SELECT tam.memory_text
    FROM team_agent_memories tam
    INNER JOIN team_agents ta ON ta.id = tam.agent_id
    WHERE tam.law_firm_id = ${input.lawFirmId}
      AND ta.user_id = ${agentOwner.user_id}
    ORDER BY tam.created_at DESC
    LIMIT 6
  `;

  const learningSummary = memoryRows.length
    ? memoryRows
        .map((row) => `- ${truncateInsight(row.memory_text, 220)}`)
        .join("\n")
    : null;

  const [memoryStats] = await db.$queryRaw<
    Array<{
      memory_count: number;
      last_learning_at: Date | null;
    }>
  >`
    SELECT
      COUNT(*) AS memory_count,
      MAX(tam.created_at) AS last_learning_at
    FROM team_agent_memories tam
    INNER JOIN team_agents ta ON ta.id = tam.agent_id
    WHERE tam.law_firm_id = ${input.lawFirmId}
      AND ta.user_id = ${agentOwner.user_id}
  `;

  await db.$executeRaw`
    UPDATE team_agents
    SET
      learning_summary = ${learningSummary},
      memory_count = ${Number(memoryStats?.memory_count ?? 0)},
      last_learning_at = ${memoryStats?.last_learning_at ?? null},
      updated_at = CURRENT_TIMESTAMP
    WHERE law_firm_id = ${input.lawFirmId}
      AND user_id = ${agentOwner.user_id}
  `;
}

export function createDocumentReviewService(deps: DocumentReviewDependencies = defaultDependencies) {
  async function submitNewDocument(input: {
    lawFirmId: string;
    actor: ActorContext;
    clientId: string;
    caseId: string;
    teamId?: string | null;
    reviewerUserId?: string | null;
    documentName: string;
    objective: string;
    dueAt: Date;
    originalFileName: string;
    mimeType: string;
    fileBuffer: Buffer;
    providedText?: string | null;
    forceSubmit?: boolean;
    reuseAnalysisId?: string | null;
  }) {
    const documentName = String(input.documentName).trim();
    const objective = String(input.objective).trim();

    if (!documentName) {
      throw new DocumentReviewServiceError(400, "Nome do documento é obrigatório.");
    }

    if (!objective) {
      throw new DocumentReviewServiceError(400, "Objetivo do documento é obrigatório.");
    }

    const actorTeamMembership = await fetchAcceptedTeamMemberContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      userId: input.actor.actorUserId,
      teamId: input.teamId ?? null,
    });
    const effectiveReviewerUserId =
      String(input.reviewerUserId ?? "").trim() || input.actor.actorUserId;
    const reviewerAgent = await fetchReviewerAgentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: actorTeamMembership.teamId,
      reviewerUserId: effectiveReviewerUserId,
    });

    const caseContext = await fetchCaseClientContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      caseId: input.caseId,
      clientId: input.clientId,
    });

    const extractedText = await extractDocumentText({
      fileName: input.originalFileName,
      mimeType: input.mimeType,
      bytes: input.fileBuffer,
      providedText: input.providedText,
    });
    const structure = buildDocumentStructure(extractedText);
    const identifiedPatterns = detectDocumentPatterns(extractedText, structure);
    const historicalContext = await fetchHistoricalContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      documentName,
      caseTypeCode: caseContext.caseTypeCode,
      excludeItemId: null,
      reviewerUserId: reviewerAgent.reviewerUserId,
    });
    const enrichedHistoricalContext = attachReviewerAgentContext(
      historicalContext,
      reviewerAgent,
    );
    const checksum = buildChecksum(input.fileBuffer);
    const expectedSource = buildSourcePayload({
      checksum,
      documentName,
      objective,
      dueAt: formatDueDateForAnalysis(input.dueAt),
      caseId: input.caseId,
      clientId: input.clientId,
      reviewerUserId: reviewerAgent.reviewerUserId,
      originalFileName: input.originalFileName,
      mimeType: input.mimeType,
    });

    const reusableAnalysis =
      input.reuseAnalysisId
        ? await findReusableAnalysis(deps.prisma, {
            lawFirmId: input.lawFirmId,
            analysisId: input.reuseAnalysisId,
            analysisTypes: ["submission_check", "pre_submission"],
            checksum,
            expectedSource,
          })
        : null;

    const analysis =
      reusableAnalysis ??
      (await runAiBackedAnalysis(deps, {
        lawFirmId: input.lawFirmId,
        clientId: input.clientId,
        caseId: input.caseId,
        actorUserId: input.actor.actorUserId,
        analysisType: "submission_check",
        triggeredManually: false,
        documentName,
        objective,
        dueAt: input.dueAt,
        caseNumber: caseContext.caseNumber,
        caseTitle: caseContext.caseTitle,
        caseTypeCode: caseContext.caseTypeCode,
        clientName: caseContext.clientName,
        extractedText,
        structure,
        identifiedPatterns,
        historicalContext: enrichedHistoricalContext,
        comparison: null,
        sourcePayload: expectedSource,
        reviewerAgent,
      }));

    if (!input.forceSubmit && analysis.requiresConfirmation) {
      const persistedAnalysis = reusableAnalysis
        ? ({ ...analysis, id: reusableAnalysis.id } as PersistedAnalysis)
        : await persistDetachedAnalysis(deps.prisma, {
            lawFirmId: input.lawFirmId,
            actorUserId: input.actor.actorUserId,
            analysis,
          });

      await deps.writeAuditLog({
        lawFirmId: input.lawFirmId,
        officeId: input.actor.officeId ?? null,
        actorUserId: input.actor.actorUserId,
        entityType: "document_review_item",
        entityId: null,
        action: "document_review.submission_alert",
        afterJson: {
          documentName,
          caseId: input.caseId,
          clientId: input.clientId,
          reviewerUserId: reviewerAgent.reviewerUserId,
          reviewerAgentId: reviewerAgent.agentId,
          riskLevel: persistedAnalysis.riskLevel,
          alertPoints: persistedAnalysis.alertPoints,
          analysisId: persistedAnalysis.id,
        },
        request: input.actor.request,
      });

      return {
        requiresConfirmation: true,
        created: false,
        itemId: null,
        versionId: null,
        analysis: persistedAnalysis,
      };
    }

    const itemId = createId();
    const versionId = createId();

    const result = await deps.prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        INSERT INTO document_review_items (
          id, law_firm_id, team_id, case_id, client_id, document_name, objective, due_at, current_status,
          latest_version_number, has_ai_alert, latest_risk_level, latest_analysis_confidence,
          assigned_reviewer_user_id, assigned_reviewer_agent_id, assigned_by_user_id, assigned_at,
          created_by_user_id, last_submitted_by_user_id, last_submitted_at, created_at, updated_at
        ) VALUES (
          ${itemId},
          ${input.lawFirmId},
          ${actorTeamMembership.teamId},
          ${input.caseId},
          ${input.clientId},
          ${documentName},
          ${objective},
          ${input.dueAt},
          'submitted',
          1,
          ${analysis.requiresConfirmation ? 1 : 0},
          ${analysis.riskLevel},
          ${analysis.confidence},
          ${reviewerAgent.reviewerUserId},
          ${reviewerAgent.agentId},
          ${input.actor.actorUserId},
          NOW(),
          ${input.actor.actorUserId},
          ${input.actor.actorUserId},
          NOW(),
          CURRENT_TIMESTAMP,
          CURRENT_TIMESTAMP
        )
      `;

      return createVersionRecords(tx, deps, {
        lawFirmId: input.lawFirmId,
        caseId: input.caseId,
        clientId: input.clientId,
        actorUserId: input.actor.actorUserId,
        itemId,
        versionId,
        versionNumber: 1,
        documentName,
        originalFileName: input.originalFileName,
        mimeType: input.mimeType,
        fileBuffer: input.fileBuffer,
        extractedText,
        structure,
        identifiedPatterns,
        historicalContext: enrichedHistoricalContext,
        comparison: null,
        analysis,
      });
    });

    await deps.prisma.$transaction(async (tx) => {
      await insertAssignmentRecord(tx, {
        lawFirmId: input.lawFirmId,
        itemId,
        versionId,
        teamId: actorTeamMembership.teamId,
        assignmentType: "initial",
        toReviewerUserId: reviewerAgent.reviewerUserId,
        toReviewerAgentId: reviewerAgent.agentId,
        assignedByUserId: input.actor.actorUserId,
      });
    });

    await deps.writeAuditLog({
      lawFirmId: input.lawFirmId,
      officeId: input.actor.officeId ?? null,
      actorUserId: input.actor.actorUserId,
      entityType: "document_review_item",
      entityId: itemId,
      action: analysis.requiresConfirmation
        ? "document_review.submitted_with_ai_alert"
        : "document_review.submitted",
      afterJson: {
        versionId,
        versionNumber: 1,
        reviewerUserId: reviewerAgent.reviewerUserId,
        reviewerAgentId: reviewerAgent.agentId,
        riskLevel: analysis.riskLevel,
        alertPoints: analysis.alertPoints,
        analysisId: result.analysisId,
      },
      request: input.actor.request,
    });

    return {
      requiresConfirmation: false,
      created: true,
      itemId,
      versionId,
      repositoryItemId: result.repositoryItemId,
      fileId: result.fileId,
      analysis: {
        ...analysis,
        id: result.analysisId,
      },
    };
  }

  async function submitNewVersion(input: {
    lawFirmId: string;
    actor: ActorContext;
    itemId: string;
    reviewerUserId?: string | null;
    originalFileName: string;
    mimeType: string;
    fileBuffer: Buffer;
    submissionNote?: string | null;
    providedText?: string | null;
    forceSubmit?: boolean;
    reuseAnalysisId?: string | null;
  }) {
    const assignmentContext = await fetchItemAssignmentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });
    const itemContext = await fetchItemContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });
    const actorTeamMembership = await fetchAcceptedTeamMemberContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      userId: input.actor.actorUserId,
      teamId: assignmentContext.teamId ?? null,
    });
    const effectiveReviewerUserId =
      String(input.reviewerUserId ?? "").trim() || assignmentContext.assignedReviewerUserId || "";

    if (!effectiveReviewerUserId) {
      throw new DocumentReviewServiceError(
        400,
        "Selecione uma pessoa do team para revisar esta nova versão.",
      );
    }

    const reviewerAgent = await fetchReviewerAgentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: assignmentContext.teamId ?? actorTeamMembership.teamId,
      reviewerUserId: effectiveReviewerUserId,
    });
    const previousVersion = await fetchPreviousVersionData(deps.prisma, {
      itemId: input.itemId,
    });

    const extractedText = await extractDocumentText({
      fileName: input.originalFileName,
      mimeType: input.mimeType,
      bytes: input.fileBuffer,
      providedText: input.providedText,
    });
    const structure = buildDocumentStructure(extractedText);
    const identifiedPatterns = detectDocumentPatterns(extractedText, structure);
    const historicalContext = await fetchHistoricalContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      documentName: itemContext.documentName,
      caseTypeCode: itemContext.caseTypeCode,
      excludeItemId: itemContext.itemId,
      reviewerUserId: reviewerAgent.reviewerUserId,
    });
    const anchoredCommentKnowledge = await fetchAnchoredCommentKnowledge(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: itemContext.itemId,
    });
    const enrichedHistoricalContext = attachAnchoredCommentKnowledge(
      historicalContext,
      anchoredCommentKnowledge,
    );
    const reviewerHistoricalContext = attachReviewerAgentContext(
      enrichedHistoricalContext,
      reviewerAgent,
    );
    const comparison = buildComparison({
      currentText: extractedText,
      previousText: previousVersion?.extractedText ?? null,
      previousVersionNumber: previousVersion?.versionNumber ?? null,
    });
    const checksum = buildChecksum(input.fileBuffer);
    const expectedSource = buildSourcePayload({
      checksum,
      documentName: itemContext.documentName,
      objective: itemContext.objective,
      dueAt: formatDueDateForAnalysis(itemContext.dueAt),
      caseId: itemContext.caseId,
      clientId: itemContext.clientId,
      itemId: input.itemId,
      reviewerUserId: reviewerAgent.reviewerUserId,
      originalFileName: input.originalFileName,
      mimeType: input.mimeType,
    });

    const reusableAnalysis =
      input.reuseAnalysisId
        ? await findReusableAnalysis(deps.prisma, {
            lawFirmId: input.lawFirmId,
            analysisId: input.reuseAnalysisId,
            analysisTypes: ["submission_check", "pre_submission"],
            checksum,
            expectedSource,
          })
        : null;

    const analysis =
      reusableAnalysis ??
      (await runAiBackedAnalysis(deps, {
        lawFirmId: input.lawFirmId,
        clientId: itemContext.clientId,
        caseId: itemContext.caseId,
        actorUserId: input.actor.actorUserId,
        analysisType: "submission_check",
        triggeredManually: false,
        documentName: itemContext.documentName,
        objective: itemContext.objective,
        dueAt: itemContext.dueAt,
        caseNumber: itemContext.caseNumber,
        caseTitle: itemContext.caseTitle,
        caseTypeCode: itemContext.caseTypeCode,
        clientName: itemContext.clientName,
        extractedText,
        structure,
        identifiedPatterns,
        historicalContext: reviewerHistoricalContext,
        comparison,
        sourcePayload: expectedSource,
        reviewerAgent,
      }));

    if (!input.forceSubmit && analysis.requiresConfirmation) {
      const persistedAnalysis = reusableAnalysis
        ? ({ ...analysis, id: reusableAnalysis.id } as PersistedAnalysis)
        : await persistDetachedAnalysis(deps.prisma, {
            lawFirmId: input.lawFirmId,
            actorUserId: input.actor.actorUserId,
            analysis,
          });

      await deps.writeAuditLog({
        lawFirmId: input.lawFirmId,
        officeId: input.actor.officeId ?? null,
        actorUserId: input.actor.actorUserId,
        entityType: "document_review_item",
        entityId: input.itemId,
        action: "document_review.new_version_alert",
        afterJson: {
          reviewerUserId: reviewerAgent.reviewerUserId,
          reviewerAgentId: reviewerAgent.agentId,
          riskLevel: persistedAnalysis.riskLevel,
          alertPoints: persistedAnalysis.alertPoints,
          analysisId: persistedAnalysis.id,
        },
        request: input.actor.request,
      });

      return {
        requiresConfirmation: true,
        created: false,
        itemId: input.itemId,
        versionId: null,
        analysis: persistedAnalysis,
      };
    }

    const versionId = createId();
    const nextVersionNumber = itemContext.latestVersionNumber + 1;

    const result = await deps.prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        UPDATE document_review_items
        SET
          current_status = 'submitted',
          latest_version_number = ${nextVersionNumber},
          has_ai_alert = ${analysis.requiresConfirmation ? 1 : 0},
          latest_risk_level = ${analysis.riskLevel},
          latest_analysis_confidence = ${analysis.confidence},
          latest_decision_code = NULL,
          latest_decision_reason = NULL,
          latest_final_validation_confidence = NULL,
          team_id = ${assignmentContext.teamId ?? actorTeamMembership.teamId},
          assigned_reviewer_user_id = ${reviewerAgent.reviewerUserId},
          assigned_reviewer_agent_id = ${reviewerAgent.agentId},
          assigned_by_user_id = ${input.actor.actorUserId},
          assigned_at = NOW(),
          last_submitted_by_user_id = ${input.actor.actorUserId},
          last_submitted_at = NOW(),
          updated_at = CURRENT_TIMESTAMP
        WHERE id = ${input.itemId}
      `;

      return createVersionRecords(tx, deps, {
        lawFirmId: input.lawFirmId,
        caseId: itemContext.caseId,
        clientId: itemContext.clientId,
        actorUserId: input.actor.actorUserId,
        itemId: input.itemId,
        versionId,
        versionNumber: nextVersionNumber,
        documentName: itemContext.documentName,
        originalFileName: input.originalFileName,
        mimeType: input.mimeType,
        fileBuffer: input.fileBuffer,
        extractedText,
        structure,
        identifiedPatterns,
        historicalContext: reviewerHistoricalContext,
        comparison,
        analysis,
        submissionNote: input.submissionNote ?? null,
        previousVersionId: previousVersion?.versionId ?? null,
      });
    });

    await deps.prisma.$transaction(async (tx) => {
      await insertAssignmentRecord(tx, {
        lawFirmId: input.lawFirmId,
        itemId: input.itemId,
        versionId,
        teamId: assignmentContext.teamId ?? actorTeamMembership.teamId,
        assignmentType:
          assignmentContext.assignedReviewerUserId &&
          assignmentContext.assignedReviewerUserId !== reviewerAgent.reviewerUserId
            ? "resubmission"
            : "resubmission",
        fromReviewerUserId: assignmentContext.assignedReviewerUserId,
        fromReviewerAgentId: assignmentContext.assignedReviewerAgentId,
        toReviewerUserId: reviewerAgent.reviewerUserId,
        toReviewerAgentId: reviewerAgent.agentId,
        assignedByUserId: input.actor.actorUserId,
        transferNote: input.submissionNote ?? null,
      });
    });

    await deps.writeAuditLog({
      lawFirmId: input.lawFirmId,
      officeId: input.actor.officeId ?? null,
      actorUserId: input.actor.actorUserId,
      entityType: "document_review_item",
      entityId: input.itemId,
      action: analysis.requiresConfirmation
        ? "document_review.new_version_submitted_with_alert"
        : "document_review.new_version_submitted",
      afterJson: {
        versionId,
        versionNumber: nextVersionNumber,
        submissionNote: input.submissionNote ?? null,
        reviewerUserId: reviewerAgent.reviewerUserId,
        reviewerAgentId: reviewerAgent.agentId,
        riskLevel: analysis.riskLevel,
        alertPoints: analysis.alertPoints,
        analysisId: result.analysisId,
      },
      request: input.actor.request,
    });

    return {
      requiresConfirmation: false,
      created: true,
      itemId: input.itemId,
      versionId,
      repositoryItemId: result.repositoryItemId,
      fileId: result.fileId,
      analysis: {
        ...analysis,
        id: result.analysisId,
      },
    };
  }

  async function runPreSubmissionAnalysis(input: {
    lawFirmId: string;
    actor: ActorContext;
    clientId: string;
    caseId: string;
    teamId?: string | null;
    reviewerUserId?: string | null;
    documentName: string;
    objective: string;
    dueAt: Date;
    originalFileName: string;
    mimeType: string;
    fileBuffer: Buffer;
    providedText?: string | null;
    itemId?: string | null;
  }) {
    const documentName = String(input.documentName).trim();
    const objective = String(input.objective).trim();

    if (!documentName) {
      throw new DocumentReviewServiceError(400, "Nome do documento é obrigatório para o pré-check.");
    }

    if (!objective) {
      throw new DocumentReviewServiceError(400, "Objetivo do documento é obrigatório para o pré-check.");
    }

    const existingAssignment =
      input.itemId
        ? await fetchItemAssignmentContext(deps.prisma, {
            lawFirmId: input.lawFirmId,
            itemId: input.itemId,
          })
        : null;
    const actorTeamMembership = await fetchAcceptedTeamMemberContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      userId: input.actor.actorUserId,
      teamId: existingAssignment?.teamId ?? input.teamId ?? null,
    });
    const effectiveReviewerUserId =
      String(input.reviewerUserId ?? "").trim() ||
      existingAssignment?.assignedReviewerUserId ||
      "";

    if (!effectiveReviewerUserId) {
      throw new DocumentReviewServiceError(
        400,
        "Selecione uma pessoa do team para direcionar o pré-check.",
      );
    }

    const reviewerAgent = await fetchReviewerAgentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: existingAssignment?.teamId ?? actorTeamMembership.teamId,
      reviewerUserId: effectiveReviewerUserId,
    });

    const caseContext = await fetchCaseClientContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      caseId: input.caseId,
      clientId: input.clientId,
    });
    const extractedText = await extractDocumentText({
      fileName: input.originalFileName,
      mimeType: input.mimeType,
      bytes: input.fileBuffer,
      providedText: input.providedText,
    });
    const structure = buildDocumentStructure(extractedText);
    const identifiedPatterns = detectDocumentPatterns(extractedText, structure);
    const historicalContext = await fetchHistoricalContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      documentName,
      caseTypeCode: caseContext.caseTypeCode,
      excludeItemId: input.itemId ?? null,
      reviewerUserId: reviewerAgent.reviewerUserId,
    });
    const anchoredCommentKnowledge =
      input.itemId
        ? await fetchAnchoredCommentKnowledge(deps.prisma, {
            lawFirmId: input.lawFirmId,
            itemId: input.itemId,
          })
        : [];

    const analysis = await runAiBackedAnalysis(deps, {
      lawFirmId: input.lawFirmId,
      clientId: input.clientId,
      caseId: input.caseId,
      actorUserId: input.actor.actorUserId,
      analysisType: "pre_submission",
      triggeredManually: true,
      documentName,
      objective,
      dueAt: input.dueAt,
      caseNumber: caseContext.caseNumber,
      caseTitle: caseContext.caseTitle,
      caseTypeCode: caseContext.caseTypeCode,
      clientName: caseContext.clientName,
      extractedText,
      structure,
      identifiedPatterns,
      historicalContext: attachReviewerAgentContext(
        attachAnchoredCommentKnowledge(historicalContext, anchoredCommentKnowledge),
        reviewerAgent,
      ),
      comparison: null,
      sourcePayload: buildSourcePayload({
        checksum: buildChecksum(input.fileBuffer),
        documentName,
        objective,
        dueAt: formatDueDateForAnalysis(input.dueAt),
        caseId: input.caseId,
        clientId: input.clientId,
        itemId: input.itemId ?? null,
        reviewerUserId: reviewerAgent.reviewerUserId,
        originalFileName: input.originalFileName,
        mimeType: input.mimeType,
      }),
      reviewerAgent,
    });

    const persistedAnalysis = await persistDetachedAnalysis(deps.prisma, {
      lawFirmId: input.lawFirmId,
      actorUserId: input.actor.actorUserId,
      analysis,
    });

    await deps.writeAuditLog({
      lawFirmId: input.lawFirmId,
      officeId: input.actor.officeId ?? null,
      actorUserId: input.actor.actorUserId,
      entityType: "document_review_ai_analysis",
      entityId: persistedAnalysis.id,
      action: "document_review.precheck",
      afterJson: {
        documentName,
        caseId: input.caseId,
        clientId: input.clientId,
        reviewerUserId: reviewerAgent.reviewerUserId,
        reviewerAgentId: reviewerAgent.agentId,
        riskLevel: persistedAnalysis.riskLevel,
      },
      request: input.actor.request,
    });

    return persistedAnalysis;
  }

  async function runReviewSupportAnalysis(input: {
    lawFirmId: string;
    actor: ActorContext;
    itemId: string;
    versionId: string;
  }) {
    const assignmentContext = await fetchItemAssignmentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });
    ensureAssignedReviewer({
      actorUserId: input.actor.actorUserId,
      assignment: assignmentContext,
      reviewerActionLabel: "rodar a análise de revisão",
    });

    const versionContext = await fetchVersionContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
      versionId: input.versionId,
    });
    const reviewerAgent = await fetchReviewerAgentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: assignmentContext.teamId!,
      reviewerUserId: assignmentContext.assignedReviewerUserId!,
    });

    const historicalContext = attachAnchoredCommentKnowledge(
      await fetchHistoricalContext(deps.prisma, {
        lawFirmId: input.lawFirmId,
        documentName: versionContext.documentName,
        caseTypeCode: versionContext.caseTypeCode,
        excludeItemId: versionContext.itemId,
        reviewerUserId: reviewerAgent.reviewerUserId,
      }),
      await fetchAnchoredCommentKnowledge(deps.prisma, {
        lawFirmId: input.lawFirmId,
        itemId: input.itemId,
        versionId: input.versionId,
      }),
    );

    const analysis = await runAiBackedAnalysis(deps, {
      lawFirmId: input.lawFirmId,
      clientId: versionContext.clientId,
      caseId: versionContext.caseId,
      actorUserId: input.actor.actorUserId,
      analysisType: "review_support",
      triggeredManually: true,
      documentName: versionContext.documentName,
      objective: versionContext.objective,
      dueAt: versionContext.dueAt,
      caseNumber: versionContext.caseNumber,
      caseTitle: versionContext.caseTitle,
      caseTypeCode: versionContext.caseTypeCode,
      clientName: versionContext.clientName,
      extractedText: versionContext.extractedText ?? "",
      structure: parseJsonValue<Record<string, unknown>>(versionContext.structureJson, {}),
      identifiedPatterns: parseJsonValue<Record<string, unknown>>(
        versionContext.identifiedPatternsJson,
        {},
      ),
      historicalContext: attachReviewerAgentContext(historicalContext, reviewerAgent),
      comparison: parseJsonValue<Record<string, unknown> | null>(versionContext.comparisonJson, null),
      sourcePayload: buildSourcePayload({
        itemId: input.itemId,
        versionId: input.versionId,
      }),
      reviewerAgent,
    });

    const analysisId = await linkAnalysisToVersion(deps.prisma as unknown as Prisma.TransactionClient, {
      analysis,
      lawFirmId: input.lawFirmId,
      actorUserId: input.actor.actorUserId,
      itemId: input.itemId,
      versionId: input.versionId,
    });

    await deps.prisma.$executeRaw`
      UPDATE document_review_items
      SET
        current_status = CASE
          WHEN current_status IN ('submitted', 'draft') THEN 'in_review'
          ELSE current_status
        END,
        has_ai_alert = ${analysis.requiresConfirmation ? 1 : 0},
        latest_risk_level = ${analysis.riskLevel},
        latest_analysis_confidence = ${analysis.confidence},
        last_reviewed_at = NOW(),
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ${input.itemId}
    `;

    await deps.writeAuditLog({
      lawFirmId: input.lawFirmId,
      officeId: input.actor.officeId ?? null,
      actorUserId: input.actor.actorUserId,
      entityType: "document_review_item",
      entityId: input.itemId,
      action: "document_review.review_support_analysis",
      afterJson: {
        versionId: input.versionId,
        analysisId,
        reviewerUserId: reviewerAgent.reviewerUserId,
        reviewerAgentId: reviewerAgent.agentId,
        riskLevel: analysis.riskLevel,
      },
      request: input.actor.request,
    });

    return {
      ...analysis,
      id: analysisId,
    } satisfies PersistedAnalysis;
  }

  async function runFinalValidation(input: {
    lawFirmId: string;
    actor: ActorContext;
    itemId: string;
    versionId: string;
  }) {
    const assignmentContext = await fetchItemAssignmentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });
    ensureAssignedReviewer({
      actorUserId: input.actor.actorUserId,
      assignment: assignmentContext,
      reviewerActionLabel: "executar a validação final",
    });

    const versionContext = await fetchVersionContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
      versionId: input.versionId,
    });
    const reviewerAgent = await fetchReviewerAgentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: assignmentContext.teamId!,
      reviewerUserId: assignmentContext.assignedReviewerUserId!,
    });

    const historicalContext = attachAnchoredCommentKnowledge(
      await fetchHistoricalContext(deps.prisma, {
        lawFirmId: input.lawFirmId,
        documentName: versionContext.documentName,
        caseTypeCode: versionContext.caseTypeCode,
        excludeItemId: versionContext.itemId,
        reviewerUserId: reviewerAgent.reviewerUserId,
      }),
      await fetchAnchoredCommentKnowledge(deps.prisma, {
        lawFirmId: input.lawFirmId,
        itemId: input.itemId,
        versionId: input.versionId,
      }),
    );

    const analysis = await runAiBackedAnalysis(deps, {
      lawFirmId: input.lawFirmId,
      clientId: versionContext.clientId,
      caseId: versionContext.caseId,
      actorUserId: input.actor.actorUserId,
      analysisType: "final_validation",
      triggeredManually: true,
      documentName: versionContext.documentName,
      objective: versionContext.objective,
      dueAt: versionContext.dueAt,
      caseNumber: versionContext.caseNumber,
      caseTitle: versionContext.caseTitle,
      caseTypeCode: versionContext.caseTypeCode,
      clientName: versionContext.clientName,
      extractedText: versionContext.extractedText ?? "",
      structure: parseJsonValue<Record<string, unknown>>(versionContext.structureJson, {}),
      identifiedPatterns: parseJsonValue<Record<string, unknown>>(
        versionContext.identifiedPatternsJson,
        {},
      ),
      historicalContext: attachReviewerAgentContext(historicalContext, reviewerAgent),
      comparison: parseJsonValue<Record<string, unknown> | null>(versionContext.comparisonJson, null),
      sourcePayload: buildSourcePayload({
        itemId: input.itemId,
        versionId: input.versionId,
        validation: "final",
      }),
      reviewerAgent,
    });

    const analysisId = await linkAnalysisToVersion(deps.prisma as unknown as Prisma.TransactionClient, {
      analysis,
      lawFirmId: input.lawFirmId,
      actorUserId: input.actor.actorUserId,
      itemId: input.itemId,
      versionId: input.versionId,
    });

    await deps.prisma.$executeRaw`
      UPDATE document_review_items
      SET
        latest_risk_level = ${analysis.riskLevel},
        latest_final_validation_confidence = ${analysis.confidence},
        has_ai_alert = ${analysis.requiresConfirmation ? 1 : 0},
        last_reviewed_at = NOW(),
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ${input.itemId}
    `;

    await deps.writeAuditLog({
      lawFirmId: input.lawFirmId,
      officeId: input.actor.officeId ?? null,
      actorUserId: input.actor.actorUserId,
      entityType: "document_review_item",
      entityId: input.itemId,
      action: "document_review.final_validation",
      afterJson: {
        versionId: input.versionId,
        analysisId,
        reviewerUserId: reviewerAgent.reviewerUserId,
        reviewerAgentId: reviewerAgent.agentId,
        riskLevel: analysis.riskLevel,
        confidence: analysis.confidence,
      },
      request: input.actor.request,
    });

    return {
      ...analysis,
      id: analysisId,
    } satisfies PersistedAnalysis;
  }

  async function createAnchoredComment(input: {
    lawFirmId: string;
    actor: ActorContext;
    itemId: string;
    versionId: string;
    selectedText: string;
    selectionStart?: number | null;
    selectionEnd?: number | null;
    selectionContextBefore?: string | null;
    selectionContextAfter?: string | null;
    pageNumber?: number | null;
    anchorX?: number | null;
    anchorY?: number | null;
    selectionRects?: AnchoredSelectionRect[];
    commentText: string;
  }) {
    const assignmentContext = await fetchItemAssignmentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });
    ensureAssignedReviewer({
      actorUserId: input.actor.actorUserId,
      assignment: assignmentContext,
      reviewerActionLabel: "comentar este documento",
    });

    const versionContext = await fetchVersionContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
      versionId: input.versionId,
    });
    const reviewerAgent = await fetchReviewerAgentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: assignmentContext.teamId!,
      reviewerUserId: assignmentContext.assignedReviewerUserId!,
    });

    const commentText = String(input.commentText ?? "").trim();
    if (!commentText) {
      throw new DocumentReviewServiceError(400, "Comentário é obrigatório.");
    }

    if (commentText.length > 4_000) {
      throw new DocumentReviewServiceError(400, "Comentário excede o limite de 4000 caracteres.");
    }

    const resolvedSelection = resolveAnchoredSelection({
      extractedText: String(versionContext.extractedText ?? ""),
      selectedText: input.selectedText,
      selectionStart: input.selectionStart ?? null,
      selectionEnd: input.selectionEnd ?? null,
      selectionContextBefore: input.selectionContextBefore ?? null,
      selectionContextAfter: input.selectionContextAfter ?? null,
    });
    const selectionRects = normalizeAnchoredSelectionRects(input.selectionRects);
    const pageNumber =
      input.pageNumber === null || input.pageNumber === undefined
        ? null
        : Math.max(1, Math.floor(Number(input.pageNumber)));
    const derivedAnchorX =
      selectionRects[0] ? clampNumber(selectionRects[0].x + selectionRects[0].width / 2, 0, 1) : null;
    const derivedAnchorY =
      selectionRects[0] ? clampNumber(selectionRects[0].y, 0, 1) : null;
    const anchorX =
      input.anchorX === null || input.anchorX === undefined
        ? derivedAnchorX
        : clampNumber(Number(input.anchorX), 0, 1);
    const anchorY =
      input.anchorY === null || input.anchorY === undefined
        ? derivedAnchorY
        : clampNumber(Number(input.anchorY), 0, 1);
    const existingAnchoredComments = await fetchAnchoredCommentKnowledge(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
      versionId: input.versionId,
      limit: 6,
    });
    const historicalContext = attachAnchoredCommentKnowledge(
      await fetchHistoricalContext(deps.prisma, {
        lawFirmId: input.lawFirmId,
        documentName: versionContext.documentName,
        caseTypeCode: versionContext.caseTypeCode,
        excludeItemId: versionContext.itemId,
        reviewerUserId: reviewerAgent.reviewerUserId,
      }),
      existingAnchoredComments,
    );
    const reply = await runAnchoredCommentReply(deps, {
      lawFirmId: input.lawFirmId,
      clientId: versionContext.clientId,
      caseId: versionContext.caseId,
      documentName: versionContext.documentName,
      objective: versionContext.objective,
      caseNumber: versionContext.caseNumber,
      caseTitle: versionContext.caseTitle,
      caseTypeCode: versionContext.caseTypeCode,
      clientName: versionContext.clientName,
      selectedText: resolvedSelection.selectedText,
      commentText,
      selectionStart: resolvedSelection.selectionStart,
      selectionEnd: resolvedSelection.selectionEnd,
      pageNumber,
      anchorX,
      anchorY,
      selectionRects,
      contextBefore: resolvedSelection.contextBefore,
      contextAfter: resolvedSelection.contextAfter,
      historicalContext: attachReviewerAgentContext(historicalContext, reviewerAgent),
      existingAnchoredComments,
      reviewerAgent,
    });

    const commentId = createId();
    const createdAt = new Date();
    const repositoryItemId = createId();
    const repositoryBodyText = truncateText(
      reply.knowledgeSummary || reply.assistantResponse,
      900,
    );

    await deps.prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        INSERT INTO document_review_comments (
          id, law_firm_id, item_id, version_id, ai_run_id, page_number, anchor_x_ratio,
          anchor_y_ratio, selection_rects_json, selection_start_offset, selection_end_offset,
          selected_text, comment_text, assistant_response, knowledge_summary, created_by_user_id,
          created_at
        ) VALUES (
          ${commentId},
          ${input.lawFirmId},
          ${input.itemId},
          ${input.versionId},
          ${reply.aiRunId},
          ${pageNumber},
          ${anchorX},
          ${anchorY},
          ${selectionRects.length > 0 ? JSON.stringify(selectionRects) : null},
          ${resolvedSelection.selectionStart},
          ${resolvedSelection.selectionEnd},
          ${resolvedSelection.selectedText},
          ${commentText},
          ${reply.assistantResponse},
          ${reply.knowledgeSummary},
          ${input.actor.actorUserId},
          ${createdAt}
        )
      `;

      await tx.$executeRaw`
        INSERT INTO repository_items (
          id, law_firm_id, client_id, case_id, item_type_code, channel_code, source_entity_type,
          source_entity_id, subject, body_text, summary_text, metadata_json, authored_by_user_id,
          created_by_user_id, occurred_at, created_at, updated_at
        ) VALUES (
          ${repositoryItemId},
          ${input.lawFirmId},
          ${versionContext.clientId},
          ${versionContext.caseId},
          'note',
          'internal',
          'document_review_comment',
          ${commentId},
          ${`Comentário ancorado • ${versionContext.documentName}`},
          ${repositoryBodyText},
          'Conhecimento derivado de comentário ancorado na revisão documental',
          ${JSON.stringify({
            itemId: input.itemId,
            versionId: input.versionId,
            pageNumber,
            anchorX,
            anchorY,
            selectionRects,
            selectionStart: resolvedSelection.selectionStart,
            selectionEnd: resolvedSelection.selectionEnd,
            selectedText: resolvedSelection.selectedText,
            commentText,
            assistantResponse: reply.assistantResponse,
            knowledgeSummary: reply.knowledgeSummary,
          })},
          ${input.actor.actorUserId},
          ${input.actor.actorUserId},
          ${createdAt},
          CURRENT_TIMESTAMP,
          CURRENT_TIMESTAMP
        )
      `;
    });

    await deps.writeAuditLog({
      lawFirmId: input.lawFirmId,
      officeId: input.actor.officeId ?? null,
      actorUserId: input.actor.actorUserId,
      entityType: "document_review_comment",
      entityId: commentId,
      action: "document_review.comment.create",
      afterJson: {
        itemId: input.itemId,
        versionId: input.versionId,
        pageNumber,
        anchorX,
        anchorY,
        selectionStart: resolvedSelection.selectionStart,
        selectionEnd: resolvedSelection.selectionEnd,
        reviewerUserId: reviewerAgent.reviewerUserId,
        reviewerAgentId: reviewerAgent.agentId,
        knowledgeSummary: reply.knowledgeSummary,
      },
      request: input.actor.request,
    });

    await appendAgentMemory(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: reviewerAgent.teamId,
      agentId: reviewerAgent.agentId,
      sourceType: "anchored_comment",
      sourceEntityId: commentId,
      memoryText:
        reply.knowledgeSummary ||
        `Comentário sobre o trecho "${truncateInsight(resolvedSelection.selectedText, 120)}": ${truncateInsight(commentText, 180)}.`,
      createdByUserId: input.actor.actorUserId,
    });

    return {
      id: commentId,
      itemId: input.itemId,
      versionId: input.versionId,
      pageNumber,
      anchorX,
      anchorY,
      selectionRects,
      selectionStart: resolvedSelection.selectionStart,
      selectionEnd: resolvedSelection.selectionEnd,
      selectedText: resolvedSelection.selectedText,
      commentText,
      assistantResponse: reply.assistantResponse,
      knowledgeSummary: reply.knowledgeSummary,
      createdAt,
    };
  }

  async function recordDecision(input: {
    lawFirmId: string;
    actor: ActorContext;
    itemId: string;
    versionId: string;
    decisionCode: DecisionCode;
    justification: string;
    rejectionReason?: string | null;
    guidanceForNewVersion?: string | null;
    finalValidationAnalysisId?: string | null;
  }) {
    const assignmentContext = await fetchItemAssignmentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });
    ensureAssignedReviewer({
      actorUserId: input.actor.actorUserId,
      assignment: assignmentContext,
      reviewerActionLabel: "registrar a decisão final",
    });

    const versionContext = await fetchVersionContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
      versionId: input.versionId,
    });
    const reviewerAgent = await fetchReviewerAgentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: assignmentContext.teamId!,
      reviewerUserId: assignmentContext.assignedReviewerUserId!,
    });

    const justification = String(input.justification ?? "").trim();
    if (!justification) {
      throw new DocumentReviewServiceError(400, "Justificativa é obrigatória para registrar a decisão.");
    }

    if (input.decisionCode === "rejected") {
      const rejectionReason = String(input.rejectionReason ?? "").trim();
      const guidanceForNewVersion = String(input.guidanceForNewVersion ?? "").trim();

      if (!rejectionReason) {
        throw new DocumentReviewServiceError(400, "Motivo da reprovação é obrigatório.");
      }

      if (!guidanceForNewVersion) {
        throw new DocumentReviewServiceError(400, "Orientação para nova versão é obrigatória.");
      }
    }

    let analysisSnapshot: Record<string, unknown> | null = null;
    if (input.decisionCode === "approved") {
      if (!input.finalValidationAnalysisId) {
        throw new DocumentReviewServiceError(
          400,
          "A aprovação exige uma validação final de IA imediatamente anterior.",
        );
      }

      const linkedAnalysis = await fetchLinkedAnalysis(deps.prisma, input.finalValidationAnalysisId);
      if (
        !linkedAnalysis ||
        linkedAnalysis.item_id !== input.itemId ||
        linkedAnalysis.version_id !== input.versionId ||
        linkedAnalysis.analysis_type !== "final_validation"
      ) {
        throw new DocumentReviewServiceError(
          400,
          "A análise de validação final não corresponde à versão selecionada.",
        );
      }

      analysisSnapshot = {
        analysisId: linkedAnalysis.id,
        summary: linkedAnalysis.summary,
        confidence: toNumber(linkedAnalysis.confidence, 0),
        riskLevel: linkedAnalysis.risk_level,
        alertPoints: parseJsonValue<string[]>(linkedAnalysis.alert_points_json, []),
      };
    }

    const decisionId = createId();

    await deps.prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        INSERT INTO document_review_decisions (
          id, law_firm_id, item_id, version_id, decision_code, justification, rejection_reason,
          guidance_for_new_version, final_validation_analysis_id, analysis_snapshot_json,
          reviewer_user_id, created_at
        ) VALUES (
          ${decisionId},
          ${input.lawFirmId},
          ${input.itemId},
          ${input.versionId},
          ${input.decisionCode},
          ${justification},
          ${input.rejectionReason ? String(input.rejectionReason).trim() : null},
          ${input.guidanceForNewVersion ? String(input.guidanceForNewVersion).trim() : null},
          ${input.finalValidationAnalysisId ?? null},
          ${analysisSnapshot ? JSON.stringify(analysisSnapshot) : null},
          ${input.actor.actorUserId},
          CURRENT_TIMESTAMP
        )
      `;

      await tx.$executeRaw`
        UPDATE document_review_items
        SET
          current_status = ${input.decisionCode},
          latest_decision_code = ${input.decisionCode},
          latest_decision_reason = ${
            input.decisionCode === "approved"
              ? justification
              : String(input.rejectionReason ?? "").trim()
          },
          approved_at = CASE WHEN ${input.decisionCode === "approved" ? 1 : 0} = 1 THEN NOW() ELSE approved_at END,
          rejected_at = CASE WHEN ${input.decisionCode === "rejected" ? 1 : 0} = 1 THEN NOW() ELSE rejected_at END,
          last_reviewed_at = NOW(),
          updated_at = CURRENT_TIMESTAMP
        WHERE id = ${input.itemId}
      `;
    });

    await deps.writeAuditLog({
      lawFirmId: input.lawFirmId,
      officeId: input.actor.officeId ?? null,
      actorUserId: input.actor.actorUserId,
      entityType: "document_review_item",
      entityId: input.itemId,
      action:
        input.decisionCode === "approved"
          ? "document_review.approved"
          : "document_review.rejected",
      changeReason: justification,
      afterJson: {
        decisionId,
        versionId: input.versionId,
        currentStatus: input.decisionCode,
        reviewerUserId: reviewerAgent.reviewerUserId,
        reviewerAgentId: reviewerAgent.agentId,
        finalValidationAnalysisId: input.finalValidationAnalysisId ?? null,
        rejectionReason: input.rejectionReason ? String(input.rejectionReason).trim() : null,
        guidanceForNewVersion: input.guidanceForNewVersion
          ? String(input.guidanceForNewVersion).trim()
          : null,
      },
      request: input.actor.request,
    });

    await appendAgentMemory(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId: reviewerAgent.teamId,
      agentId: reviewerAgent.agentId,
      sourceType: input.decisionCode === "approved" ? "approval_decision" : "rejection_decision",
      sourceEntityId: decisionId,
      memoryText:
        input.decisionCode === "approved"
          ? `Aprovou ${versionContext.documentName} no caso ${versionContext.caseNumber}: ${truncateInsight(justification, 220)}.`
          : `Reprovou ${versionContext.documentName} no caso ${versionContext.caseNumber} por ${truncateInsight(String(input.rejectionReason ?? ""), 180)}. Orientação: ${truncateInsight(String(input.guidanceForNewVersion ?? ""), 180)}.`,
      createdByUserId: input.actor.actorUserId,
    });

    return {
      id: decisionId,
      itemId: input.itemId,
      versionId: input.versionId,
      decisionCode: input.decisionCode,
      justification,
      rejectionReason: input.rejectionReason ? String(input.rejectionReason).trim() : null,
      guidanceForNewVersion: input.guidanceForNewVersion
        ? String(input.guidanceForNewVersion).trim()
        : null,
      finalValidationAnalysisId: input.finalValidationAnalysisId ?? null,
      reviewerName: reviewerAgent.reviewerDisplayName,
    };
  }

  async function transferReview(input: {
    lawFirmId: string;
    actor: ActorContext;
    itemId: string;
    reviewerUserId: string;
    note?: string | null;
  }) {
    const itemContext = await fetchItemContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });
    const assignmentContext = await fetchItemAssignmentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });

    ensureAssignedReviewer({
      actorUserId: input.actor.actorUserId,
      assignment: assignmentContext,
      reviewerActionLabel: "transferir esta revisão",
    });

    if (!assignmentContext.teamId) {
      throw new DocumentReviewServiceError(
        400,
        "Este documento ainda não está vinculado a um team.",
      );
    }
    const teamId = assignmentContext.teamId;

    const nextReviewerAgent = await fetchReviewerAgentContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      teamId,
      reviewerUserId: input.reviewerUserId,
    });

    if (nextReviewerAgent.reviewerUserId === assignmentContext.assignedReviewerUserId) {
      throw new DocumentReviewServiceError(
        400,
        "Selecione outra pessoa do team para transferir a revisão.",
      );
    }

    await deps.prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        UPDATE document_review_items
        SET
          assigned_reviewer_user_id = ${nextReviewerAgent.reviewerUserId},
          assigned_reviewer_agent_id = ${nextReviewerAgent.agentId},
          assigned_by_user_id = ${input.actor.actorUserId},
          assigned_at = NOW(),
          updated_at = CURRENT_TIMESTAMP
        WHERE id = ${input.itemId}
          AND law_firm_id = ${input.lawFirmId}
      `;

      await insertAssignmentRecord(tx, {
        lawFirmId: input.lawFirmId,
        itemId: input.itemId,
        teamId,
        assignmentType: "transfer",
        fromReviewerUserId: assignmentContext.assignedReviewerUserId,
        fromReviewerAgentId: assignmentContext.assignedReviewerAgentId,
        toReviewerUserId: nextReviewerAgent.reviewerUserId,
        toReviewerAgentId: nextReviewerAgent.agentId,
        assignedByUserId: input.actor.actorUserId,
        transferNote: input.note ?? null,
      });
    });

    await deps.writeAuditLog({
      lawFirmId: input.lawFirmId,
      officeId: input.actor.officeId ?? null,
      actorUserId: input.actor.actorUserId,
      entityType: "document_review_item",
      entityId: input.itemId,
      action: "document_review.transfer",
      afterJson: {
        previousReviewerUserId: assignmentContext.assignedReviewerUserId,
        nextReviewerUserId: nextReviewerAgent.reviewerUserId,
        nextReviewerAgentId: nextReviewerAgent.agentId,
        note: input.note ? String(input.note).trim() : null,
      },
      request: input.actor.request,
    });

    return {
      itemId: input.itemId,
      documentName: itemContext.documentName,
      assignedReviewerUserId: nextReviewerAgent.reviewerUserId,
      assignedReviewerName: nextReviewerAgent.reviewerDisplayName,
      assignedReviewerAgentId: nextReviewerAgent.agentId,
      assignedReviewerAgentName: nextReviewerAgent.agentName,
      assignedAt: new Date(),
    };
  }

  async function listItems(input: {
    lawFirmId: string;
    filters?: {
      status?: string | null;
      clientId?: string | null;
      caseId?: string | null;
      teamId?: string | null;
      from?: Date | null;
      to?: Date | null;
      hasAiAlert?: boolean | null;
      decisionCode?: string | null;
    };
  }) {
    const rows = await deps.prisma.$queryRaw<
      Array<{
        id: string;
        document_name: string;
        objective: string;
        due_at: Date;
        current_status: string;
        latest_version_number: number;
        has_ai_alert: number;
        latest_risk_level: string | null;
        last_submitted_at: Date | null;
        created_at: Date;
        case_id: string;
        case_number: string;
        case_title: string;
        client_id: string;
        client_first_name: string;
        client_last_name: string;
        submitted_display_name: string | null;
        submitted_first_name: string | null;
        submitted_last_name: string | null;
        submitted_email: string | null;
        assigned_reviewer_user_id: string | null;
        reviewer_display_name: string | null;
        reviewer_first_name: string | null;
        reviewer_last_name: string | null;
        reviewer_email: string | null;
        assigned_reviewer_agent_id: string | null;
        reviewer_agent_name: string | null;
        assigned_at: Date | null;
        latest_version_id: string | null;
        latest_analysis_id: string | null;
        latest_analysis_summary: string | null;
        latest_analysis_confidence: unknown;
        latest_analysis_confidence_label: string | null;
      }>
    >`
      SELECT
        dri.id,
        dri.document_name,
        dri.objective,
        dri.due_at,
        dri.current_status,
        dri.latest_version_number,
        dri.has_ai_alert,
        dri.latest_risk_level,
        dri.last_submitted_at,
        dri.created_at,
        c.id AS case_id,
        c.case_number,
        c.title AS case_title,
        cl.id AS client_id,
        cl.first_name AS client_first_name,
        cl.last_name AS client_last_name,
        su.display_name AS submitted_display_name,
        su.first_name AS submitted_first_name,
        su.last_name AS submitted_last_name,
        su.email AS submitted_email,
        dri.assigned_reviewer_user_id,
        ru.display_name AS reviewer_display_name,
        ru.first_name AS reviewer_first_name,
        ru.last_name AS reviewer_last_name,
        ru.email AS reviewer_email,
        dri.assigned_reviewer_agent_id,
        ta.agent_name AS reviewer_agent_name,
        dri.assigned_at,
        drv.id AS latest_version_id,
        dra.id AS latest_analysis_id,
        dra.summary AS latest_analysis_summary,
        dra.confidence AS latest_analysis_confidence,
        dra.confidence_label AS latest_analysis_confidence_label
      FROM document_review_items dri
      INNER JOIN cases c ON c.id = dri.case_id
      INNER JOIN clients cl ON cl.id = dri.client_id
      LEFT JOIN users su ON su.id = dri.last_submitted_by_user_id
      LEFT JOIN users ru ON ru.id = dri.assigned_reviewer_user_id
      LEFT JOIN team_agents ta ON ta.id = dri.assigned_reviewer_agent_id
      LEFT JOIN document_review_versions drv
        ON drv.item_id = dri.id
        AND drv.version_number = dri.latest_version_number
      LEFT JOIN document_review_ai_analyses dra
        ON dra.id = (
          SELECT dra2.id
          FROM document_review_ai_analyses dra2
          WHERE dra2.item_id = dri.id
            AND (drv.id IS NULL OR dra2.version_id = drv.id)
          ORDER BY dra2.created_at DESC
          LIMIT 1
        )
      WHERE dri.law_firm_id = ${input.lawFirmId}
        AND (${input.filters?.status ?? null} IS NULL OR dri.current_status = ${input.filters?.status ?? null})
        AND (${input.filters?.clientId ?? null} IS NULL OR dri.client_id = ${input.filters?.clientId ?? null})
        AND (${input.filters?.caseId ?? null} IS NULL OR dri.case_id = ${input.filters?.caseId ?? null})
        AND (${input.filters?.teamId ?? null} IS NULL OR dri.team_id = ${input.filters?.teamId ?? null})
        AND (${input.filters?.hasAiAlert === null || input.filters?.hasAiAlert === undefined ? null : input.filters.hasAiAlert ? 1 : 0} IS NULL
          OR dri.has_ai_alert = ${input.filters?.hasAiAlert === null || input.filters?.hasAiAlert === undefined ? null : input.filters.hasAiAlert ? 1 : 0})
        AND (${input.filters?.decisionCode ?? null} IS NULL OR dri.latest_decision_code = ${input.filters?.decisionCode ?? null})
        AND (${input.filters?.from ?? null} IS NULL OR COALESCE(dri.last_submitted_at, dri.created_at) >= ${input.filters?.from ?? null})
        AND (${input.filters?.to ?? null} IS NULL OR COALESCE(dri.last_submitted_at, dri.created_at) <= ${input.filters?.to ?? null})
      ORDER BY COALESCE(dri.last_submitted_at, dri.created_at) DESC, dri.created_at DESC
    `;

    return rows.map((row) => ({
      id: row.id,
      documentName: row.document_name,
      objective: row.objective,
      dueAt: row.due_at,
      status: row.current_status,
      latestVersionNumber: toNumber(row.latest_version_number, 0),
      hasAiAlert: Boolean(row.has_ai_alert),
      latestRiskLevel: row.latest_risk_level,
      submittedAt: row.last_submitted_at ?? row.created_at,
      createdAt: row.created_at,
      client: {
        id: row.client_id,
        name: `${row.client_first_name} ${row.client_last_name}`.trim(),
      },
      case: {
        id: row.case_id,
        caseNumber: row.case_number,
        title: row.case_title,
      },
      submittedBy:
        row.submitted_display_name || row.submitted_first_name || row.submitted_email
          ? getUserDisplayName({
              display_name: row.submitted_display_name,
              first_name: row.submitted_first_name,
              last_name: row.submitted_last_name,
              email: row.submitted_email,
            })
          : null,
      assignedAt: row.assigned_at,
      assignedReviewer:
        row.assigned_reviewer_user_id && row.reviewer_email
          ? {
              id: row.assigned_reviewer_user_id,
              name: getUserDisplayName({
                display_name: row.reviewer_display_name,
                first_name: row.reviewer_first_name,
                last_name: row.reviewer_last_name,
                email: row.reviewer_email,
              }),
              email: row.reviewer_email,
              agentId: row.assigned_reviewer_agent_id,
              agentName: row.reviewer_agent_name,
            }
          : null,
      latestVersionId: row.latest_version_id,
      latestAnalysis:
        row.latest_analysis_id
          ? {
              id: row.latest_analysis_id,
              summary: row.latest_analysis_summary,
              confidence: toNumber(row.latest_analysis_confidence, 0),
              confidenceLabel: row.latest_analysis_confidence_label,
            }
          : null,
    }));
  }

  async function getItemDetail(input: {
    lawFirmId: string;
    itemId: string;
  }) {
    const itemContext = await fetchItemContext(deps.prisma, {
      lawFirmId: input.lawFirmId,
      itemId: input.itemId,
    });

    const [itemRow, versionRows, analysisRows, decisionRows, commentRows, assignmentRows] =
      await Promise.all([
      deps.prisma.$queryRaw<
        Array<{
          id: string;
          team_id: string | null;
          document_name: string;
          objective: string;
          due_at: Date;
          current_status: string;
          latest_version_number: number;
          has_ai_alert: number;
          latest_risk_level: string | null;
          latest_decision_code: string | null;
          latest_decision_reason: string | null;
          latest_analysis_confidence: unknown;
          latest_final_validation_confidence: unknown;
          assigned_reviewer_user_id: string | null;
          reviewer_display_name: string | null;
          reviewer_first_name: string | null;
          reviewer_last_name: string | null;
          reviewer_email: string | null;
          assigned_reviewer_agent_id: string | null;
          reviewer_agent_name: string | null;
          assigned_by_user_id: string | null;
          assigned_by_display_name: string | null;
          assigned_by_first_name: string | null;
          assigned_by_last_name: string | null;
          assigned_by_email: string | null;
          assigned_at: Date | null;
          last_submitted_at: Date | null;
          last_reviewed_at: Date | null;
          approved_at: Date | null;
          rejected_at: Date | null;
          created_at: Date;
          updated_at: Date;
          client_first_name: string;
          client_last_name: string;
          case_number: string;
          case_title: string;
          case_type_code: string;
          created_display_name: string | null;
          created_first_name: string | null;
          created_last_name: string | null;
          created_email: string | null;
        }>
      >`
        SELECT
          dri.id,
          dri.team_id,
          dri.document_name,
          dri.objective,
          dri.due_at,
          dri.current_status,
          dri.latest_version_number,
          dri.has_ai_alert,
          dri.latest_risk_level,
          dri.latest_decision_code,
          dri.latest_decision_reason,
          dri.latest_analysis_confidence,
          dri.latest_final_validation_confidence,
          dri.assigned_reviewer_user_id,
          ru.display_name AS reviewer_display_name,
          ru.first_name AS reviewer_first_name,
          ru.last_name AS reviewer_last_name,
          ru.email AS reviewer_email,
          dri.assigned_reviewer_agent_id,
          ta.agent_name AS reviewer_agent_name,
          dri.assigned_by_user_id,
          au.display_name AS assigned_by_display_name,
          au.first_name AS assigned_by_first_name,
          au.last_name AS assigned_by_last_name,
          au.email AS assigned_by_email,
          dri.assigned_at,
          dri.last_submitted_at,
          dri.last_reviewed_at,
          dri.approved_at,
          dri.rejected_at,
          dri.created_at,
          dri.updated_at,
          cl.first_name AS client_first_name,
          cl.last_name AS client_last_name,
          c.case_number,
          c.title AS case_title,
          c.case_type_code,
          cu.display_name AS created_display_name,
          cu.first_name AS created_first_name,
          cu.last_name AS created_last_name,
          cu.email AS created_email
        FROM document_review_items dri
        INNER JOIN clients cl ON cl.id = dri.client_id
        INNER JOIN cases c ON c.id = dri.case_id
        LEFT JOIN users cu ON cu.id = dri.created_by_user_id
        LEFT JOIN users ru ON ru.id = dri.assigned_reviewer_user_id
        LEFT JOIN team_agents ta ON ta.id = dri.assigned_reviewer_agent_id
        LEFT JOIN users au ON au.id = dri.assigned_by_user_id
        WHERE dri.id = ${input.itemId}
          AND dri.law_firm_id = ${input.lawFirmId}
        LIMIT 1
      `,
      deps.prisma.$queryRaw<
        Array<{
          id: string;
          version_number: number;
          submission_status: string;
          submission_note: string | null;
          extracted_text: string | null;
          structure_json: unknown;
          identified_patterns_json: unknown;
          historical_context_json: unknown;
          comparison_json: unknown;
          ai_alerts_json: unknown;
          possible_rejection_reasons: unknown;
          has_ai_alert: number;
          risk_level: string | null;
          last_analysis_confidence: unknown;
          submitted_at: Date | null;
          cancelled_at: Date | null;
          created_at: Date;
          file_id: string;
          original_file_name: string;
          mime_type: string;
          size_bytes: bigint;
          submitted_display_name: string | null;
          submitted_first_name: string | null;
          submitted_last_name: string | null;
          submitted_email: string | null;
        }>
      >`
        SELECT
          drv.id,
          drv.version_number,
          drv.submission_status,
          drv.submission_note,
          drv.extracted_text,
          drv.structure_json,
          drv.identified_patterns_json,
          drv.historical_context_json,
          drv.comparison_json,
          drv.ai_alerts_json,
          drv.possible_rejection_reasons,
          drv.has_ai_alert,
          drv.risk_level,
          drv.last_analysis_confidence,
          drv.submitted_at,
          drv.cancelled_at,
          drv.created_at,
          drv.file_id,
          f.original_file_name,
          f.mime_type,
          f.size_bytes,
          su.display_name AS submitted_display_name,
          su.first_name AS submitted_first_name,
          su.last_name AS submitted_last_name,
          su.email AS submitted_email
        FROM document_review_versions drv
        INNER JOIN files f ON f.id = drv.file_id
        LEFT JOIN users su ON su.id = drv.submitted_by_user_id
        WHERE drv.item_id = ${input.itemId}
          AND drv.law_firm_id = ${input.lawFirmId}
        ORDER BY drv.version_number DESC
      `,
      deps.prisma.$queryRaw<
        Array<{
          id: string;
          version_id: string | null;
          analysis_type: string;
          summary: string | null;
          risks: unknown;
          strengths: unknown;
          weaknesses: unknown;
          suggestions: unknown;
          possible_rejection_reasons: unknown;
          confidence: unknown;
          confidence_label: string | null;
          triggered_manually: number;
          extracted_text: string | null;
          structure_json: unknown;
          identified_patterns_json: unknown;
          historical_context_json: unknown;
          comparison_json: unknown;
          alert_points_json: unknown;
          risk_level: string | null;
          raw_result_json: unknown;
          created_at: Date;
          created_display_name: string | null;
          created_first_name: string | null;
          created_last_name: string | null;
          created_email: string | null;
        }>
      >`
        SELECT
          dra.id,
          dra.version_id,
          dra.analysis_type,
          dra.summary,
          dra.risks,
          dra.strengths,
          dra.weaknesses,
          dra.suggestions,
          dra.possible_rejection_reasons,
          dra.confidence,
          dra.confidence_label,
          dra.triggered_manually,
          dra.extracted_text,
          dra.structure_json,
          dra.identified_patterns_json,
          dra.historical_context_json,
          dra.comparison_json,
          dra.alert_points_json,
          dra.risk_level,
          dra.raw_result_json,
          dra.created_at,
          cu.display_name AS created_display_name,
          cu.first_name AS created_first_name,
          cu.last_name AS created_last_name,
          cu.email AS created_email
        FROM document_review_ai_analyses dra
        LEFT JOIN users cu ON cu.id = dra.created_by_user_id
        WHERE dra.item_id = ${input.itemId}
          AND dra.law_firm_id = ${input.lawFirmId}
        ORDER BY dra.created_at DESC
      `,
      deps.prisma.$queryRaw<
        Array<{
          id: string;
          version_id: string;
          decision_code: string;
          justification: string;
          rejection_reason: string | null;
          guidance_for_new_version: string | null;
          final_validation_analysis_id: string | null;
          analysis_snapshot_json: unknown;
          created_at: Date;
          reviewer_display_name: string | null;
          reviewer_first_name: string | null;
          reviewer_last_name: string | null;
          reviewer_email: string | null;
        }>
      >`
        SELECT
          drd.id,
          drd.version_id,
          drd.decision_code,
          drd.justification,
          drd.rejection_reason,
          drd.guidance_for_new_version,
          drd.final_validation_analysis_id,
          drd.analysis_snapshot_json,
          drd.created_at,
          ru.display_name AS reviewer_display_name,
          ru.first_name AS reviewer_first_name,
          ru.last_name AS reviewer_last_name,
          ru.email AS reviewer_email
        FROM document_review_decisions drd
        LEFT JOIN users ru ON ru.id = drd.reviewer_user_id
        WHERE drd.item_id = ${input.itemId}
          AND drd.law_firm_id = ${input.lawFirmId}
        ORDER BY drd.created_at DESC
      `,
      deps.prisma.$queryRaw<
        Array<{
          id: string;
          version_id: string;
          page_number: number | null;
          anchor_x_ratio: unknown;
          anchor_y_ratio: unknown;
          selection_rects_json: unknown;
          selection_start_offset: number | null;
          selection_end_offset: number | null;
          selected_text: string;
          comment_text: string;
          assistant_response: string;
          knowledge_summary: string | null;
          created_at: Date;
          created_display_name: string | null;
          created_first_name: string | null;
          created_last_name: string | null;
          created_email: string | null;
        }>
      >`
        SELECT
          drc.id,
          drc.version_id,
          drc.page_number,
          drc.anchor_x_ratio,
          drc.anchor_y_ratio,
          drc.selection_rects_json,
          drc.selection_start_offset,
          drc.selection_end_offset,
          drc.selected_text,
          drc.comment_text,
          drc.assistant_response,
          drc.knowledge_summary,
          drc.created_at,
          cu.display_name AS created_display_name,
          cu.first_name AS created_first_name,
          cu.last_name AS created_last_name,
          cu.email AS created_email
        FROM document_review_comments drc
        LEFT JOIN users cu ON cu.id = drc.created_by_user_id
        WHERE drc.item_id = ${input.itemId}
          AND drc.law_firm_id = ${input.lawFirmId}
        ORDER BY drc.created_at DESC
      `,
      deps.prisma.$queryRaw<
        Array<{
          id: string;
          version_id: string | null;
          assignment_type: string;
          transfer_note: string | null;
          created_at: Date;
          from_reviewer_display_name: string | null;
          from_reviewer_first_name: string | null;
          from_reviewer_last_name: string | null;
          from_reviewer_email: string | null;
          to_reviewer_display_name: string | null;
          to_reviewer_first_name: string | null;
          to_reviewer_last_name: string | null;
          to_reviewer_email: string | null;
          assigned_by_display_name: string | null;
          assigned_by_first_name: string | null;
          assigned_by_last_name: string | null;
          assigned_by_email: string | null;
        }>
      >`
        SELECT
          dra.id,
          dra.version_id,
          dra.assignment_type,
          dra.transfer_note,
          dra.created_at,
          fr.display_name AS from_reviewer_display_name,
          fr.first_name AS from_reviewer_first_name,
          fr.last_name AS from_reviewer_last_name,
          fr.email AS from_reviewer_email,
          tr.display_name AS to_reviewer_display_name,
          tr.first_name AS to_reviewer_first_name,
          tr.last_name AS to_reviewer_last_name,
          tr.email AS to_reviewer_email,
          au.display_name AS assigned_by_display_name,
          au.first_name AS assigned_by_first_name,
          au.last_name AS assigned_by_last_name,
          au.email AS assigned_by_email
        FROM document_review_assignments dra
        LEFT JOIN users fr ON fr.id = dra.from_reviewer_user_id
        LEFT JOIN users tr ON tr.id = dra.to_reviewer_user_id
        LEFT JOIN users au ON au.id = dra.assigned_by_user_id
        WHERE dra.item_id = ${input.itemId}
          AND dra.law_firm_id = ${input.lawFirmId}
        ORDER BY dra.created_at DESC
      `,
    ]);

    const item = itemRow[0];
    if (!item) {
      throw new DocumentReviewServiceError(404, "Item de revisão documental não encontrado.");
    }

    const analysesByVersion = analysisRows.reduce<Map<string, Array<Record<string, unknown>>>>(
      (acc, row) => {
        const key = String(row.version_id ?? "__none__");
        const list = acc.get(key) ?? [];
        const risks = parseJsonValue<unknown[]>(row.risks, [])
          .map((risk) => normalizeRisk(risk))
          .filter((risk): risk is ReviewRisk => Boolean(risk));
        const confidence = toNumber(row.confidence, 0);
        const analysisType = String(row.analysis_type) as AnalysisType;
        const extractedText = String(row.extracted_text ?? "");
        const riskLevel = String(row.risk_level ?? "low") as RiskSeverity;
        const rawResult = parseJsonValue<Record<string, unknown>>(row.raw_result_json, {});
        const verdict = hydrateVerdictFromStoredAnalysis({
          analysisType,
          extractedText,
          risks,
          riskLevel,
          confidence,
          verdictReason: String(rawResult.verdictReason ?? ""),
        });
        list.push({
          id: row.id,
          versionId: row.version_id,
          analysisType,
          summary: verdict.summary,
          risks,
          strengths: parseJsonValue<string[]>(row.strengths, []),
          weaknesses: parseJsonValue<string[]>(row.weaknesses, []),
          suggestions: parseJsonValue<string[]>(row.suggestions, []),
          possibleRejectionReasons: parseJsonValue<string[]>(
            row.possible_rejection_reasons,
            [],
          ),
          confidence,
          confidenceLabel: row.confidence_label,
          triggeredManually: Boolean(row.triggered_manually),
          extractedText,
          structure: parseJsonValue<Record<string, unknown>>(row.structure_json, {}),
          identifiedPatterns: parseJsonValue<Record<string, unknown>>(
            row.identified_patterns_json,
            {},
          ),
          historicalContext: parseJsonValue<Record<string, unknown>>(
            row.historical_context_json,
            {},
          ),
          comparison: parseJsonValue<Record<string, unknown> | null>(row.comparison_json, null),
          alertPoints: parseJsonValue<string[]>(row.alert_points_json, []),
          riskLevel,
          verdict: verdict.verdict,
          verdictReason: verdict.verdictReason,
          rawResult,
          createdAt: row.created_at,
          createdBy: getUserDisplayName({
            display_name: row.created_display_name,
            first_name: row.created_first_name,
            last_name: row.created_last_name,
            email: row.created_email,
          }),
        });
        acc.set(key, list);
        return acc;
      },
      new Map(),
    );
    const commentsByVersion = commentRows.reduce<Map<string, Array<Record<string, unknown>>>>(
      (acc, row) => {
        const list = acc.get(row.version_id) ?? [];
        list.push({
          id: row.id,
          versionId: row.version_id,
          pageNumber:
            row.page_number === null || row.page_number === undefined
              ? null
              : toNumber(row.page_number, 0),
          anchorX:
            row.anchor_x_ratio === null || row.anchor_x_ratio === undefined
              ? null
              : clampNumber(toNumber(row.anchor_x_ratio, 0), 0, 1),
          anchorY:
            row.anchor_y_ratio === null || row.anchor_y_ratio === undefined
              ? null
              : clampNumber(toNumber(row.anchor_y_ratio, 0), 0, 1),
          selectionRects: normalizeAnchoredSelectionRects(
            parseJsonValue<unknown[]>(row.selection_rects_json, []),
          ),
          selectionStart:
            row.selection_start_offset === null || row.selection_start_offset === undefined
              ? null
              : toNumber(row.selection_start_offset, 0),
          selectionEnd:
            row.selection_end_offset === null || row.selection_end_offset === undefined
              ? null
              : toNumber(row.selection_end_offset, 0),
          selectedText: row.selected_text,
          commentText: row.comment_text,
          assistantResponse: row.assistant_response,
          knowledgeSummary: row.knowledge_summary,
          createdAt: row.created_at,
          createdBy: getUserDisplayName({
            display_name: row.created_display_name,
            first_name: row.created_first_name,
            last_name: row.created_last_name,
            email: row.created_email,
          }),
        });
        acc.set(row.version_id, list);
        return acc;
      },
      new Map(),
    );

    return {
      item: {
        id: item.id,
        teamId: item.team_id,
        documentName: item.document_name,
        objective: item.objective,
        dueAt: item.due_at,
        status: item.current_status,
        latestVersionNumber: toNumber(item.latest_version_number, 0),
        hasAiAlert: Boolean(item.has_ai_alert),
        latestRiskLevel: item.latest_risk_level,
        latestDecisionCode: item.latest_decision_code,
        latestDecisionReason: item.latest_decision_reason,
        latestAnalysisConfidence: toNumber(item.latest_analysis_confidence, 0),
        latestFinalValidationConfidence: toNumber(item.latest_final_validation_confidence, 0),
        lastSubmittedAt: item.last_submitted_at,
        lastReviewedAt: item.last_reviewed_at,
        approvedAt: item.approved_at,
        rejectedAt: item.rejected_at,
        createdAt: item.created_at,
        updatedAt: item.updated_at,
        client: {
          id: itemContext.clientId,
          name: `${item.client_first_name} ${item.client_last_name}`.trim(),
        },
        case: {
          id: itemContext.caseId,
          caseNumber: item.case_number,
          title: item.case_title,
          caseTypeCode: item.case_type_code,
        },
        assignedReviewer:
          item.assigned_reviewer_user_id && item.reviewer_email
            ? {
                id: item.assigned_reviewer_user_id,
                name: getUserDisplayName({
                  display_name: item.reviewer_display_name,
                  first_name: item.reviewer_first_name,
                  last_name: item.reviewer_last_name,
                  email: item.reviewer_email,
                }),
                email: item.reviewer_email,
                agentId: item.assigned_reviewer_agent_id,
                agentName: item.reviewer_agent_name,
              }
            : null,
        assignedBy:
          item.assigned_by_user_id && item.assigned_by_email
            ? getUserDisplayName({
                display_name: item.assigned_by_display_name,
                first_name: item.assigned_by_first_name,
                last_name: item.assigned_by_last_name,
                email: item.assigned_by_email,
              })
            : null,
        assignedAt: item.assigned_at,
        createdBy: getUserDisplayName({
          display_name: item.created_display_name,
          first_name: item.created_first_name,
          last_name: item.created_last_name,
          email: item.created_email,
        }),
      },
      versions: versionRows.map((row) => ({
        id: row.id,
        versionNumber: toNumber(row.version_number, 1),
        submissionStatus: row.submission_status,
        submissionNote: row.submission_note,
        extractedText: row.extracted_text,
        structure: parseJsonValue<Record<string, unknown>>(row.structure_json, {}),
        identifiedPatterns: parseJsonValue<Record<string, unknown>>(row.identified_patterns_json, {}),
        historicalContext: parseJsonValue<Record<string, unknown>>(row.historical_context_json, {}),
        comparison: parseJsonValue<Record<string, unknown> | null>(row.comparison_json, null),
        aiAlerts: parseJsonValue<string[]>(row.ai_alerts_json, []),
        possibleRejectionReasons: parseJsonValue<string[]>(row.possible_rejection_reasons, []),
        hasAiAlert: Boolean(row.has_ai_alert),
        riskLevel: row.risk_level,
        lastAnalysisConfidence: toNumber(row.last_analysis_confidence, 0),
        submittedAt: row.submitted_at,
        cancelledAt: row.cancelled_at,
        createdAt: row.created_at,
        file: {
          id: row.file_id,
          originalFileName: row.original_file_name,
          mimeType: row.mime_type,
          sizeBytes: Number(row.size_bytes),
        },
        submittedBy:
          row.submitted_display_name || row.submitted_first_name || row.submitted_email
            ? getUserDisplayName({
                display_name: row.submitted_display_name,
                first_name: row.submitted_first_name,
                last_name: row.submitted_last_name,
                email: row.submitted_email,
              })
            : null,
        analyses: analysesByVersion.get(row.id) ?? [],
        comments: commentsByVersion.get(row.id) ?? [],
      })),
      decisions: decisionRows.map((row) => ({
        id: row.id,
        versionId: row.version_id,
        decisionCode: row.decision_code,
        justification: row.justification,
        rejectionReason: row.rejection_reason,
        guidanceForNewVersion: row.guidance_for_new_version,
        finalValidationAnalysisId: row.final_validation_analysis_id,
        analysisSnapshot: parseJsonValue<Record<string, unknown> | null>(
          row.analysis_snapshot_json,
          null,
        ),
        createdAt: row.created_at,
        reviewer: getUserDisplayName({
          display_name: row.reviewer_display_name,
          first_name: row.reviewer_first_name,
          last_name: row.reviewer_last_name,
          email: row.reviewer_email,
        }),
      })),
      assignments: assignmentRows.map((row) => ({
        id: row.id,
        versionId: row.version_id,
        assignmentType: row.assignment_type,
        note: row.transfer_note,
        createdAt: row.created_at,
        fromReviewer:
          row.from_reviewer_display_name || row.from_reviewer_first_name || row.from_reviewer_email
            ? getUserDisplayName({
                display_name: row.from_reviewer_display_name,
                first_name: row.from_reviewer_first_name,
                last_name: row.from_reviewer_last_name,
                email: row.from_reviewer_email,
              })
            : null,
        toReviewer: getUserDisplayName({
          display_name: row.to_reviewer_display_name,
          first_name: row.to_reviewer_first_name,
          last_name: row.to_reviewer_last_name,
          email: row.to_reviewer_email,
        }),
        assignedBy:
          row.assigned_by_display_name || row.assigned_by_first_name || row.assigned_by_email
            ? getUserDisplayName({
                display_name: row.assigned_by_display_name,
                first_name: row.assigned_by_first_name,
                last_name: row.assigned_by_last_name,
                email: row.assigned_by_email,
              })
            : null,
      })),
    };
  }

  async function getMetrics(input: {
    lawFirmId: string;
    filters?: {
      clientId?: string | null;
      caseId?: string | null;
      teamId?: string | null;
    };
  }) {
    const [summaryRows, decisionRows] = await Promise.all([
      deps.prisma.$queryRaw<
        Array<{
          current_status: string;
          total: bigint;
        }>
      >`
        SELECT current_status, COUNT(*) AS total
        FROM document_review_items
        WHERE law_firm_id = ${input.lawFirmId}
          AND (${input.filters?.clientId ?? null} IS NULL OR client_id = ${input.filters?.clientId ?? null})
          AND (${input.filters?.caseId ?? null} IS NULL OR case_id = ${input.filters?.caseId ?? null})
          AND (${input.filters?.teamId ?? null} IS NULL OR team_id = ${input.filters?.teamId ?? null})
        GROUP BY current_status
      `,
      deps.prisma.$queryRaw<
        Array<{
          decision_code: string;
          month_bucket: Date;
          total: bigint;
        }>
      >`
        SELECT
          decision_code,
          DATE_FORMAT(created_at, '%Y-%m-01') AS month_bucket,
          COUNT(*) AS total
        FROM document_review_decisions
        WHERE law_firm_id = ${input.lawFirmId}
          AND (${input.filters?.clientId ?? null} IS NULL OR item_id IN (
            SELECT id
            FROM document_review_items
            WHERE client_id = ${input.filters?.clientId ?? null}
              AND law_firm_id = ${input.lawFirmId}
              AND (${input.filters?.teamId ?? null} IS NULL OR team_id = ${input.filters?.teamId ?? null})
          ))
          AND (${input.filters?.caseId ?? null} IS NULL OR item_id IN (
            SELECT id
            FROM document_review_items
            WHERE case_id = ${input.filters?.caseId ?? null}
              AND law_firm_id = ${input.lawFirmId}
              AND (${input.filters?.teamId ?? null} IS NULL OR team_id = ${input.filters?.teamId ?? null})
          ))
          AND (${input.filters?.teamId ?? null} IS NULL OR item_id IN (
            SELECT id
            FROM document_review_items
            WHERE law_firm_id = ${input.lawFirmId}
              AND team_id = ${input.filters?.teamId ?? null}
          ))
          AND created_at >= DATE_SUB(CURRENT_DATE(), INTERVAL 11 MONTH)
        GROUP BY decision_code, DATE_FORMAT(created_at, '%Y-%m-01')
      `,
    ]);

    const approvedCount = summaryRows
      .filter((row) => row.current_status === "approved")
      .reduce((total, row) => total + Number(row.total), 0);
    const rejectedCount = summaryRows
      .filter((row) => row.current_status === "rejected")
      .reduce((total, row) => total + Number(row.total), 0);
    const reviewedTotal = approvedCount + rejectedCount;

    const months: string[] = [];
    const now = new Date();
    for (let offset = 11; offset >= 0; offset -= 1) {
      const monthDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - offset, 1));
      months.push(toIsoMonth(monthDate));
    }

    const buckets = new Map(
      months.map((month) => [
        month,
        {
          month,
          approved: 0,
          rejected: 0,
        },
      ]),
    );

    for (const row of decisionRows) {
      const month = toIsoMonth(new Date(row.month_bucket));
      const bucket = buckets.get(month);
      if (!bucket) {
        continue;
      }

      if (row.decision_code === "approved") {
        bucket.approved += Number(row.total);
      }

      if (row.decision_code === "rejected") {
        bucket.rejected += Number(row.total);
      }
    }

    return {
      approvedCount,
      rejectedCount,
      approvalRate: reviewedTotal > 0 ? approvedCount / reviewedTotal : 0,
      rejectionRate: reviewedTotal > 0 ? rejectedCount / reviewedTotal : 0,
      monthlySeries: Array.from(buckets.values()),
    };
  }

  return {
    submitNewDocument,
    submitNewVersion,
    runPreSubmissionAnalysis,
    runReviewSupportAnalysis,
    runFinalValidation,
    createAnchoredComment,
    recordDecision,
    transferReview,
    listItems,
    getItemDetail,
    getMetrics,
  };
}

export const documentReviewService = createDocumentReviewService();
