import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { requireSession } from "../../lib/auth.js";
import { writeAuditLog } from "../../lib/audit.js";
import { createId } from "../../lib/id.js";
import { prisma } from "../../lib/prisma.js";
import { getSessionProfile } from "../../lib/session.js";
import { createAiRun, finishAiRun, runTextChatCompletion } from "../../lib/tenant-ai.js";

const createTeamSchema = z.object({
  name: z.string().min(2).max(150),
  description: z.string().max(500).optional().or(z.literal("")),
});

const workspaceQuerySchema = z.object({
  teamId: z.string().uuid().optional(),
});

const createInvitationSchema = z.object({
  teamId: z.string().uuid(),
  email: z.string().email(),
});

const invitationParamsSchema = z.object({
  invitationId: z.string().uuid(),
});

const respondInvitationSchema = z.object({
  decision: z.enum(["accepted", "rejected"]),
});

const askTeamAgentSchema = z.object({
  agentId: z.union([z.string().uuid(), z.literal("global")]),
  question: z.string().trim().min(1).max(4000),
  history: z
    .array(
      z.object({
        role: z.enum(["user", "assistant"]),
        content: z.string().trim().min(1).max(4000),
      }),
    )
    .max(16)
    .optional()
    .default([]),
});

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

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 buildDefaultAgentPrompt(input: {
  displayName: string;
  email: string;
  membershipRole: string;
}) {
  const roleLabel = input.membershipRole === "owner" ? "líder do time" : "membro do time";

  return [
    `Você representa o estilo de revisão documental de ${input.displayName}.`,
    `Essa pessoa participa de um ou mais teams deste workspace como ${roleLabel} e usa o e-mail ${input.email}.`,
    "Aprenda com reprovações, aprovações, orientações e comentários ancorados dessa pessoa.",
    "Quando houver memória prévia, priorize esse histórico individual antes de recorrer a padrões genéricos.",
    "Se faltar histórico suficiente, seja explícito e conservador na conclusão.",
  ].join(" ");
}

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

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

async function ensureTeamAgent(input: {
  lawFirmId: string;
  teamId: string;
  userId: string;
  displayName: string;
  email: string;
  membershipRole: string;
}) {
  const [existingAgent] = await prisma.$queryRaw<
    Array<{
      id: string;
      agent_name: string;
      learning_summary: string | null;
      memory_count: number;
      last_learning_at: Date | null;
    }>
  >`
    SELECT id, agent_name, learning_summary, memory_count, last_learning_at
    FROM team_agents
    WHERE law_firm_id = ${input.lawFirmId}
      AND user_id = ${input.userId}
    ORDER BY created_at ASC, id ASC
    LIMIT 1
  `;

  if (existingAgent) {
    return {
      id: existingAgent.id,
      agentName: existingAgent.agent_name,
      learningSummary: existingAgent.learning_summary,
      memoryCount: Number(existingAgent.memory_count ?? 0),
      lastLearningAt: existingAgent.last_learning_at,
    };
  }

  const agentId = createId();
  const agentName = `Agente de ${input.displayName}`;

  await prisma.$executeRaw`
    INSERT INTO team_agents (
      id, law_firm_id, team_id, user_id, agent_name, system_prompt, learning_summary,
      memory_count, last_learning_at, created_at, updated_at
    ) VALUES (
      ${agentId},
      ${input.lawFirmId},
      ${input.teamId},
      ${input.userId},
      ${agentName},
      ${buildDefaultAgentPrompt(input)},
      NULL,
      0,
      NULL,
      CURRENT_TIMESTAMP,
      CURRENT_TIMESTAMP
    )
  `;

  return {
    id: agentId,
    agentName,
    learningSummary: null,
    memoryCount: 0,
    lastLearningAt: null,
  };
}

async function getTeamContext(input: {
  lawFirmId: string;
  userId: string;
  teamId: string;
}) {
  const [teamRow] = await prisma.$queryRaw<
    Array<{
      team_id: string;
      team_name: string;
      team_description: string | null;
      team_created_at: Date;
      created_display_name: string | null;
      created_first_name: string | null;
      created_last_name: string | null;
      created_email: string | null;
      my_membership_role: string | null;
    }>
  >`
    SELECT
      t.id AS team_id,
      t.name AS team_name,
      t.description AS team_description,
      t.created_at AS team_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,
      tm.membership_role AS my_membership_role
    FROM teams t
    LEFT JOIN users cu ON cu.id = t.created_by_user_id
    LEFT JOIN team_memberships tm
      ON tm.team_id = t.id
      AND tm.user_id = ${input.userId}
      AND tm.law_firm_id = ${input.lawFirmId}
    WHERE t.law_firm_id = ${input.lawFirmId}
      AND t.id = ${input.teamId}
    LIMIT 1
  `;

  if (!teamRow) {
    return null;
  }

  return {
    teamId: teamRow.team_id,
    name: teamRow.team_name,
    description: teamRow.team_description,
    createdAt: teamRow.team_created_at,
    createdBy: getUserDisplayName({
      display_name: teamRow.created_display_name,
      first_name: teamRow.created_first_name,
      last_name: teamRow.created_last_name,
      email: teamRow.created_email,
    }),
    myRole: teamRow.my_membership_role,
  };
}

async function listAccessibleAgents(input: {
  lawFirmId: string;
  userId: string;
}) {
  const rows = await prisma.$queryRaw<
    Array<{
      agent_id: string | null;
      agent_name: string | null;
      team_id: string | null;
      team_name: string | null;
      seed_team_id: string | null;
      user_id: string;
      email: string;
      display_name: string | null;
      first_name: string | null;
      last_name: string | null;
      avatar_url: string | null;
      membership_role: string;
      memory_count: number | null;
      last_learning_at: Date | null;
    }>
  >`
    SELECT
      (
        SELECT ta2.id
        FROM team_agents ta2
        WHERE ta2.law_firm_id = ${input.lawFirmId}
          AND ta2.user_id = accessible_users.user_id
        ORDER BY ta2.created_at ASC, ta2.id ASC
        LIMIT 1
      ) AS agent_id,
      (
        SELECT ta2.agent_name
        FROM team_agents ta2
        WHERE ta2.law_firm_id = ${input.lawFirmId}
          AND ta2.user_id = accessible_users.user_id
        ORDER BY ta2.created_at ASC, ta2.id ASC
        LIMIT 1
      ) AS agent_name,
      (
        SELECT ta2.team_id
        FROM team_agents ta2
        WHERE ta2.law_firm_id = ${input.lawFirmId}
          AND ta2.user_id = accessible_users.user_id
        ORDER BY ta2.created_at ASC, ta2.id ASC
        LIMIT 1
      ) AS team_id,
      (
        SELECT t2.name
        FROM team_agents ta2
        INNER JOIN teams t2 ON t2.id = ta2.team_id
        WHERE ta2.law_firm_id = ${input.lawFirmId}
          AND ta2.user_id = accessible_users.user_id
        ORDER BY ta2.created_at ASC, ta2.id ASC
        LIMIT 1
      ) AS team_name,
      (
        SELECT tm2.team_id
        FROM team_memberships tm2
        WHERE tm2.law_firm_id = ${input.lawFirmId}
          AND tm2.user_id = accessible_users.user_id
        ORDER BY tm2.joined_at ASC, tm2.team_id ASC
        LIMIT 1
      ) AS seed_team_id,
      accessible_users.user_id,
      u.email,
      u.display_name,
      u.first_name,
      u.last_name,
      u.avatar_url,
      CASE
        WHEN EXISTS (
          SELECT 1
          FROM team_memberships owner_tm
          WHERE owner_tm.law_firm_id = ${input.lawFirmId}
            AND owner_tm.user_id = accessible_users.user_id
            AND owner_tm.membership_role = 'owner'
        ) THEN 'owner'
        ELSE 'member'
      END AS membership_role,
      (
        SELECT COUNT(*)
        FROM team_agent_memories tam
        INNER JOIN team_agents ta3 ON ta3.id = tam.agent_id
        WHERE tam.law_firm_id = ${input.lawFirmId}
          AND ta3.user_id = accessible_users.user_id
      ) AS memory_count,
      (
        SELECT MAX(tam.created_at)
        FROM team_agent_memories tam
        INNER JOIN team_agents ta3 ON ta3.id = tam.agent_id
        WHERE tam.law_firm_id = ${input.lawFirmId}
          AND ta3.user_id = accessible_users.user_id
      ) AS last_learning_at
    FROM (
      SELECT DISTINCT tm.user_id
      FROM team_memberships tm
      INNER JOIN team_memberships my_tm
        ON my_tm.team_id = tm.team_id
       AND my_tm.user_id = ${input.userId}
       AND my_tm.law_firm_id = ${input.lawFirmId}
      WHERE tm.law_firm_id = ${input.lawFirmId}
    ) AS accessible_users
    INNER JOIN users u ON u.id = accessible_users.user_id
    WHERE u.deleted_at IS NULL
    ORDER BY
      CASE WHEN accessible_users.user_id = ${input.userId} THEN 0 ELSE 1 END,
      COALESCE(u.display_name, CONCAT(u.first_name, ' ', u.last_name), u.email) ASC
  `;

  const normalizedRows = await Promise.all(
    rows.map(async (row) => {
      const displayName = getUserDisplayName({
        display_name: row.display_name,
        first_name: row.first_name,
        last_name: row.last_name,
        email: row.email,
      });

      const ensuredAgent =
        row.agent_id || !row.seed_team_id
          ? null
          : await ensureTeamAgent({
              lawFirmId: input.lawFirmId,
              teamId: row.seed_team_id,
              userId: row.user_id,
              displayName,
              email: row.email,
              membershipRole: row.membership_role,
            });

      return {
        agentId: row.agent_id ?? ensuredAgent?.id ?? null,
        agentName: row.agent_name ?? ensuredAgent?.agentName ?? `Agente de ${displayName}`,
        teamId: row.team_id ?? row.seed_team_id ?? "",
        teamName: row.team_name ?? "Team",
        userId: row.user_id,
        userDisplayName: displayName,
        userEmail: row.email,
        avatarUrl: row.avatar_url,
        membershipRole: row.membership_role,
        memoryCount: Number(row.memory_count ?? ensuredAgent?.memoryCount ?? 0),
        lastLearningAt: row.last_learning_at ?? ensuredAgent?.lastLearningAt ?? null,
        isCurrentUser: row.user_id === input.userId,
      };
    }),
  );

  return normalizedRows.flatMap((row) =>
    row.agentId
      ? [
          {
            ...row,
            agentId: row.agentId,
          },
        ]
      : [],
  );
}

async function getAccessibleAgentContext(input: {
  lawFirmId: string;
  userId: string;
  agentId: string;
}) {
  const [row] = await prisma.$queryRaw<
    Array<{
      agent_id: string;
      agent_name: string;
      system_prompt: string | null;
      learning_summary: string | null;
      memory_count: number | null;
      last_learning_at: Date | null;
      team_id: string;
      team_name: string | null;
      agent_user_id: string;
      email: string;
      display_name: string | null;
      first_name: string | null;
      last_name: string | null;
      avatar_url: string | null;
      membership_role: string;
    }>
  >`
    SELECT
      ta.id AS agent_id,
      ta.agent_name,
      ta.system_prompt,
      ta.learning_summary,
      (
        SELECT COUNT(*)
        FROM team_agent_memories tam
        INNER JOIN team_agents ta2 ON ta2.id = tam.agent_id
        WHERE tam.law_firm_id = ${input.lawFirmId}
          AND ta2.user_id = ta.user_id
      ) AS memory_count,
      (
        SELECT MAX(tam.created_at)
        FROM team_agent_memories tam
        INNER JOIN team_agents ta2 ON ta2.id = tam.agent_id
        WHERE tam.law_firm_id = ${input.lawFirmId}
          AND ta2.user_id = ta.user_id
      ) AS last_learning_at,
      ta.team_id,
      t.name AS team_name,
      ta.user_id AS agent_user_id,
      u.email,
      u.display_name,
      u.first_name,
      u.last_name,
      u.avatar_url,
      CASE
        WHEN EXISTS (
          SELECT 1
          FROM team_memberships owner_tm
          WHERE owner_tm.law_firm_id = ${input.lawFirmId}
            AND owner_tm.user_id = ta.user_id
            AND owner_tm.membership_role = 'owner'
        ) THEN 'owner'
        ELSE 'member'
      END AS membership_role
    FROM team_agents ta
    INNER JOIN teams t ON t.id = ta.team_id
    INNER JOIN users u ON u.id = ta.user_id
    WHERE ta.law_firm_id = ${input.lawFirmId}
      AND ta.user_id = (
        SELECT selected_agent.user_id
        FROM team_agents selected_agent
        WHERE selected_agent.law_firm_id = ${input.lawFirmId}
          AND selected_agent.id = ${input.agentId}
        LIMIT 1
      )
      AND EXISTS (
        SELECT 1
        FROM team_memberships my_tm
        INNER JOIN team_memberships agent_tm
          ON agent_tm.team_id = my_tm.team_id
         AND agent_tm.user_id = ta.user_id
         AND agent_tm.law_firm_id = ${input.lawFirmId}
        WHERE my_tm.law_firm_id = ${input.lawFirmId}
          AND my_tm.user_id = ${input.userId}
      )
    ORDER BY ta.created_at ASC, ta.id ASC
    LIMIT 1
  `;

  if (!row) {
    return null;
  }

  const memoryRows = await prisma.$queryRaw<Array<{ memory_text: string; created_at: Date }>>`
    SELECT tam.memory_text, tam.created_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 = ${row.agent_user_id}
    ORDER BY tam.created_at DESC
    LIMIT 18
  `;

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

  return {
    agentId: row.agent_id,
    agentName: row.agent_name,
    systemPrompt: row.system_prompt,
    learningSummary,
    memoryCount: Number(row.memory_count ?? 0),
    lastLearningAt: row.last_learning_at,
    teamId: row.team_id,
    teamName: row.team_name ?? "Team",
    userId: row.agent_user_id,
    userDisplayName: getUserDisplayName({
      display_name: row.display_name,
      first_name: row.first_name,
      last_name: row.last_name,
      email: row.email,
    }),
    userEmail: row.email,
    avatarUrl: row.avatar_url,
    membershipRole: row.membership_role,
    recentMemories: memoryRows.map((memory) => truncateInsight(memory.memory_text)),
  };
}

function buildAskAgentSystemPrompt(input: {
  userDisplayName: string;
  agentName: string;
  membershipRole: string;
  systemPrompt?: string | null;
}) {
  return [
    `Você é a interface conversacional do ${input.agentName}, construída para representar o repertório profissional de ${input.userDisplayName}.`,
    `A pessoa representada atua em um ou mais teams deste workspace como ${input.membershipRole}.`,
    "Responda sempre em português do Brasil.",
    "Baseie sua resposta apenas no resumo persistido, nas memórias recentes fornecidas e no histórico desta conversa.",
    "Não afirme que é a pessoa real; deixe implícito que responde a partir do histórico do agente.",
    "Quando faltar base suficiente nas memórias, diga isso com clareza em vez de inventar.",
    "Prefira respostas objetivas, úteis e práticas.",
    "Se o histórico mostrar padrões de revisão, interpretação ou prioridade, preserve esse estilo na resposta.",
    input.systemPrompt ? `Diretriz-base do agente: ${input.systemPrompt}` : null,
  ]
    .filter(Boolean)
    .join(" ");
}

const workspaceSearchStopWords = new Set([
  "a",
  "o",
  "as",
  "os",
  "de",
  "da",
  "do",
  "das",
  "dos",
  "e",
  "em",
  "no",
  "na",
  "nos",
  "nas",
  "um",
  "uma",
  "uns",
  "umas",
  "para",
  "por",
  "com",
  "sem",
  "que",
  "como",
  "qual",
  "quais",
  "sobre",
  "entre",
  "ser",
  "estar",
  "esta",
  "esse",
  "essa",
  "isso",
  "ao",
  "aos",
  "à",
  "às",
  "se",
  "eu",
  "você",
  "voce",
  "me",
  "minha",
  "meu",
  "seu",
  "sua",
  "dele",
  "dela",
  "eles",
  "elas",
  "já",
  "ja",
  "mais",
  "menos",
  "muito",
  "muita",
  "muitos",
  "muitas",
  "tem",
  "tenho",
  "quero",
  "preciso",
  "pode",
  "poderia",
  "favor",
  "favor",
  "sobre",
]);

function normalizeSearchText(value: string | null | undefined) {
  return String(value ?? "")
    .normalize("NFD")
    .replace(/\p{Diacritic}/gu, "")
    .toLowerCase();
}

function extractWorkspaceSearchTerms(question: string) {
  const terms = normalizeSearchText(question)
    .replace(/[^\p{L}\p{N}\s]/gu, " ")
    .split(/\s+/)
    .map((term) => term.trim())
    .filter((term) => term.length >= 3 && !workspaceSearchStopWords.has(term));

  return Array.from(new Set(terms)).slice(0, 12);
}

function scoreWorkspaceRelevance(text: string, searchTerms: string[]) {
  const normalized = normalizeSearchText(text);
  let score = 0;

  for (const term of searchTerms) {
    if (normalized.includes(term)) {
      score += term.length >= 6 ? 3 : 2;
    }
  }

  return score;
}

function pickRelevantWorkspaceRows<T>(
  rows: T[],
  searchTerms: string[],
  getText: (row: T) => string,
  getDate: (row: T) => Date | string | null | undefined,
  limit: number,
) {
  const scoredRows = rows.map((row) => {
    const value = getDate(row);
    const timestamp = value ? new Date(value).getTime() : 0;

    return {
      row,
      score: scoreWorkspaceRelevance(getText(row), searchTerms),
      timestamp: Number.isNaN(timestamp) ? 0 : timestamp,
    };
  });

  const matchingRows = scoredRows
    .filter((item) => item.score > 0)
    .sort((left, right) => right.score - left.score || right.timestamp - left.timestamp)
    .slice(0, limit);

  if (matchingRows.length >= limit) {
    return matchingRows.map((item) => item.row);
  }

  const seen = new Set(matchingRows.map((item) => item.row));
  const fallbackRows = scoredRows
    .filter((item) => !seen.has(item.row))
    .sort((left, right) => right.timestamp - left.timestamp)
    .slice(0, limit - matchingRows.length);

  return [...matchingRows, ...fallbackRows].map((item) => item.row);
}

function formatWorkspaceAssistantDate(value: Date | string | null | undefined) {
  if (!value) {
    return null;
  }

  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    return null;
  }

  return date.toISOString().slice(0, 10);
}

function buildGlobalAssistantDirectoryItem(input: {
  lawFirmName: string;
  memoryCount: number;
  lastLearningAt?: Date | string | null;
}) {
  return {
    agentId: "global",
    agentName: "GLOBAL",
    teamId: "",
    teamName: input.lawFirmName,
    userId: "",
    userDisplayName: "GLOBAL",
    userEmail: input.lawFirmName,
    avatarUrl: null,
    membershipRole: "workspace",
    memoryCount: input.memoryCount,
    lastLearningAt: input.lastLearningAt ? new Date(input.lastLearningAt) : null,
    isCurrentUser: false,
  };
}

type WorkspaceKnowledgeDocumentRow = {
  id: string;
  title: string | null;
  objective: string | null;
  body_text: string | null;
  created_at: Date;
  file_name: string | null;
  mime_type: string | null;
};

async function loadWorkspaceKnowledgeDocumentRows(lawFirmId: string) {
  return prisma.$queryRaw<Array<WorkspaceKnowledgeDocumentRow>>`
    SELECT
      ri.id,
      ri.subject AS title,
      ri.summary_text AS objective,
      ri.body_text,
      ri.created_at,
      f.original_file_name AS file_name,
      f.mime_type
    FROM repository_items ri
    LEFT JOIN files f ON f.repository_item_id = ri.id
    WHERE ri.law_firm_id = ${lawFirmId}
      AND ri.source_entity_type = 'knowledge_base_document'
    ORDER BY ri.created_at DESC
    LIMIT 120
  `;
}

function summarizeWorkspaceKnowledgeDocuments(input: {
  rows: WorkspaceKnowledgeDocumentRow[];
  question: string;
  limit: number;
}) {
  const searchTerms = extractWorkspaceSearchTerms(input.question);
  const selectedRows = pickRelevantWorkspaceRows(
    input.rows,
    searchTerms,
    (row) =>
      `${row.title ?? ""} ${row.objective ?? ""} ${row.body_text ?? ""} ${row.file_name ?? ""} ${row.mime_type ?? ""}`,
    (row) => row.created_at,
    input.limit,
  );

  return selectedRows.map((row) => {
    const createdAt = formatWorkspaceAssistantDate(row.created_at);
    const fileLabel = row.file_name?.trim() || row.mime_type?.trim() || "arquivo sem nome";

    return `${row.title?.trim() || "Documento sem título"} • objetivo ${row.objective?.trim() || "não informado"}${createdAt ? ` • enviado em ${createdAt}` : ""} • ${fileLabel}${row.body_text ? ` • ${truncateInsight(row.body_text, 320)}` : ""}`;
  });
}

async function getGlobalWorkspaceAssistantContext(input: {
  lawFirmId: string;
  lawFirmName: string;
  question: string;
}) {
  const searchTerms = extractWorkspaceSearchTerms(input.question);

  const [
    counts,
    agentRows,
    memoryRows,
    caseRows,
    clientRows,
    reviewRows,
    knowledgeDocumentRows,
  ] = await Promise.all([
    prisma.$queryRaw<
      Array<{
        total_teams: number;
        total_members: number;
        total_agents: number;
        total_memories: number;
        total_cases: number;
        total_clients: number;
        total_reviews: number;
        total_knowledge_documents: number;
      }>
    >`
      SELECT
        (SELECT COUNT(*) FROM teams WHERE law_firm_id = ${input.lawFirmId}) AS total_teams,
        (SELECT COUNT(*) FROM workspace_memberships WHERE law_firm_id = ${input.lawFirmId}) AS total_members,
        (SELECT COUNT(*) FROM team_agents WHERE law_firm_id = ${input.lawFirmId}) AS total_agents,
        (SELECT COUNT(*) FROM team_agent_memories WHERE law_firm_id = ${input.lawFirmId}) AS total_memories,
        (SELECT COUNT(*) FROM cases WHERE law_firm_id = ${input.lawFirmId} AND deleted_at IS NULL) AS total_cases,
        (SELECT COUNT(*) FROM clients WHERE law_firm_id = ${input.lawFirmId} AND deleted_at IS NULL) AS total_clients,
        (SELECT COUNT(*) FROM document_review_items WHERE law_firm_id = ${input.lawFirmId}) AS total_reviews,
        (
          SELECT COUNT(*)
          FROM repository_items
          WHERE law_firm_id = ${input.lawFirmId}
            AND source_entity_type = 'knowledge_base_document'
        ) AS total_knowledge_documents
    `,
    prisma.$queryRaw<
      Array<{
        user_id: string;
        email: string;
        display_name: string | null;
        first_name: string | null;
        last_name: string | null;
        memory_count: number | null;
        last_learning_at: Date | null;
      }>
    >`
      SELECT
        wm.user_id,
        u.email,
        u.display_name,
        u.first_name,
        u.last_name,
        (
          SELECT COUNT(*)
          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 = wm.user_id
        ) AS memory_count,
        (
          SELECT MAX(tam.created_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 = wm.user_id
        ) AS last_learning_at
      FROM workspace_memberships wm
      INNER JOIN users u ON u.id = wm.user_id
      WHERE wm.law_firm_id = ${input.lawFirmId}
        AND u.deleted_at IS NULL
      ORDER BY
        COALESCE(
          (
            SELECT COUNT(*)
            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 = wm.user_id
          ),
          0
        ) DESC,
        COALESCE(u.display_name, CONCAT(u.first_name, ' ', u.last_name), u.email) ASC
      LIMIT 80
    `,
    prisma.$queryRaw<
      Array<{
        created_at: Date;
        memory_text: string;
        agent_name: string;
        email: string;
        display_name: string | null;
        first_name: string | null;
        last_name: string | null;
      }>
    >`
      SELECT
        tam.created_at,
        tam.memory_text,
        ta.agent_name,
        u.email,
        u.display_name,
        u.first_name,
        u.last_name
      FROM team_agent_memories tam
      INNER JOIN team_agents ta ON ta.id = tam.agent_id
      INNER JOIN users u ON u.id = ta.user_id
      WHERE tam.law_firm_id = ${input.lawFirmId}
      ORDER BY tam.created_at DESC
      LIMIT 180
    `,
    prisma.$queryRaw<
      Array<{
        id: string;
        case_number: string;
        title: string;
        description: string | null;
        status_code: string;
        priority_code: string;
        updated_at: Date;
        client_preferred_name: string | null;
        client_first_name: string | null;
        client_last_name: string | null;
      }>
    >`
      SELECT
        c.id,
        c.case_number,
        c.title,
        c.description,
        c.status_code,
        c.priority_code,
        c.updated_at,
        cl.preferred_name AS client_preferred_name,
        cl.first_name AS client_first_name,
        cl.last_name AS client_last_name
      FROM cases c
      INNER JOIN clients cl ON cl.id = c.client_id
      WHERE c.law_firm_id = ${input.lawFirmId}
        AND c.deleted_at IS NULL
      ORDER BY c.updated_at DESC
      LIMIT 120
    `,
    prisma.$queryRaw<
      Array<{
        id: string;
        preferred_name: string | null;
        first_name: string;
        last_name: string;
        email: string | null;
        phone: string | null;
        immigration_status: string | null;
        updated_at: Date;
      }>
    >`
      SELECT
        id,
        preferred_name,
        first_name,
        last_name,
        email,
        phone,
        immigration_status,
        updated_at
      FROM clients
      WHERE law_firm_id = ${input.lawFirmId}
        AND deleted_at IS NULL
      ORDER BY updated_at DESC
      LIMIT 100
    `,
    prisma.$queryRaw<
      Array<{
        id: string;
        document_name: string;
        objective: string;
        due_at: Date;
        current_status: string;
        latest_decision_code: string | null;
        latest_decision_reason: string | null;
        updated_at: Date;
        case_number: string;
        case_title: string;
      }>
    >`
      SELECT
        dri.id,
        dri.document_name,
        dri.objective,
        dri.due_at,
        dri.current_status,
        dri.latest_decision_code,
        dri.latest_decision_reason,
        dri.updated_at,
        c.case_number,
        c.title AS case_title
      FROM document_review_items dri
      INNER JOIN cases c ON c.id = dri.case_id
      WHERE dri.law_firm_id = ${input.lawFirmId}
      ORDER BY dri.updated_at DESC
      LIMIT 120
    `,
    loadWorkspaceKnowledgeDocumentRows(input.lawFirmId),
  ]);

  const totals = counts[0] ?? {
    total_teams: 0,
    total_members: 0,
    total_agents: 0,
    total_memories: 0,
    total_cases: 0,
    total_clients: 0,
    total_reviews: 0,
    total_knowledge_documents: 0,
  };

  const selectedAgents = pickRelevantWorkspaceRows(
    agentRows,
    searchTerms,
    (row) =>
      `${getUserDisplayName({
        display_name: row.display_name,
        first_name: row.first_name,
        last_name: row.last_name,
        email: row.email,
      })} ${row.email}`,
    (row) => row.last_learning_at,
    10,
  );

  const selectedMemories = pickRelevantWorkspaceRows(
    memoryRows,
    searchTerms,
    (row) =>
      `${row.agent_name} ${getUserDisplayName({
        display_name: row.display_name,
        first_name: row.first_name,
        last_name: row.last_name,
        email: row.email,
      })} ${row.memory_text}`,
    (row) => row.created_at,
    16,
  );

  const selectedCases = pickRelevantWorkspaceRows(
    caseRows,
    searchTerms,
    (row) =>
      `${row.case_number} ${row.title} ${row.description ?? ""} ${row.status_code} ${row.priority_code} ${row.client_preferred_name ?? ""} ${row.client_first_name ?? ""} ${row.client_last_name ?? ""}`,
    (row) => row.updated_at,
    10,
  );

  const selectedClients = pickRelevantWorkspaceRows(
    clientRows,
    searchTerms,
    (row) =>
      `${row.preferred_name ?? ""} ${row.first_name} ${row.last_name} ${row.email ?? ""} ${row.phone ?? ""} ${row.immigration_status ?? ""}`,
    (row) => row.updated_at,
    8,
  );

  const selectedReviews = pickRelevantWorkspaceRows(
    reviewRows,
    searchTerms,
    (row) =>
      `${row.document_name} ${row.objective} ${row.current_status} ${row.latest_decision_code ?? ""} ${row.latest_decision_reason ?? ""} ${row.case_number} ${row.case_title}`,
    (row) => row.updated_at,
    10,
  );

  const knowledgeDocumentSummaries = summarizeWorkspaceKnowledgeDocuments({
    rows: knowledgeDocumentRows,
    question: input.question,
    limit: 10,
  });

  return {
    directoryItem: buildGlobalAssistantDirectoryItem({
      lawFirmName: input.lawFirmName,
      memoryCount: Number(totals.total_memories ?? 0),
      lastLearningAt:
        selectedMemories[0]?.created_at ?? selectedAgents[0]?.last_learning_at ?? null,
    }),
    overview: [
      `Workspace ${input.lawFirmName}.`,
      `${Number(totals.total_teams ?? 0)} teams.`,
      `${Number(totals.total_members ?? 0)} usuários no workspace.`,
      `${Number(totals.total_agents ?? 0)} agentes cadastrados.`,
      `${Number(totals.total_memories ?? 0)} memórias acumuladas.`,
      `${Number(totals.total_cases ?? 0)} casos.`,
      `${Number(totals.total_clients ?? 0)} clientes.`,
      `${Number(totals.total_reviews ?? 0)} revisões documentais.`,
      `${Number(totals.total_knowledge_documents ?? 0)} documentos na base de conhecimento.`,
    ].join(" "),
    agentSummaries: selectedAgents.map((row) => {
      const displayName = getUserDisplayName({
        display_name: row.display_name,
        first_name: row.first_name,
        last_name: row.last_name,
        email: row.email,
      });
      const lastLearningAt = formatWorkspaceAssistantDate(row.last_learning_at);
      return `${displayName} <${row.email}> • ${Number(row.memory_count ?? 0)} memórias${lastLearningAt ? ` • último aprendizado ${lastLearningAt}` : ""}`;
    }),
    memorySummaries: selectedMemories.map((row) => {
      const displayName = getUserDisplayName({
        display_name: row.display_name,
        first_name: row.first_name,
        last_name: row.last_name,
        email: row.email,
      });
      return `[${formatWorkspaceAssistantDate(row.created_at) ?? "sem data"}] ${displayName}: ${truncateInsight(row.memory_text, 280)}`;
    }),
    caseSummaries: selectedCases.map((row) => {
      const clientName =
        row.client_preferred_name?.trim() ||
        `${row.client_first_name ?? ""} ${row.client_last_name ?? ""}`.trim() ||
        "Cliente sem nome";
      return `${row.case_number} • ${row.title} • cliente ${clientName} • status ${row.status_code} • prioridade ${row.priority_code}${row.description ? ` • ${truncateInsight(row.description, 220)}` : ""}`;
    }),
    clientSummaries: selectedClients.map((row) => {
      const clientName =
        row.preferred_name?.trim() || `${row.first_name} ${row.last_name}`.trim() || "Cliente sem nome";
      return `${clientName}${row.email ? ` • ${row.email}` : ""}${row.phone ? ` • ${row.phone}` : ""}${row.immigration_status ? ` • status migratório ${row.immigration_status}` : ""}`;
    }),
    reviewSummaries: selectedReviews.map((row) => {
      const dueAt = formatWorkspaceAssistantDate(row.due_at);
      return `${row.document_name} • caso ${row.case_number} - ${row.case_title} • status ${row.current_status}${row.latest_decision_code ? ` • decisão ${row.latest_decision_code}` : ""}${dueAt ? ` • prazo ${dueAt}` : ""} • ${truncateInsight(row.objective, 220)}`;
    }),
    knowledgeDocumentSummaries,
  };
}

function buildGlobalWorkspaceSystemPrompt(input: {
  lawFirmName: string;
}) {
  return [
    `Você é o assistente global do workspace ${input.lawFirmName}.`,
    "Responda sempre em português do Brasil.",
    "Você deve consolidar conhecimento de agentes, chats, revisões documentais, casos, clientes e documentos da base de conhecimento do workspace.",
    "Baseie sua resposta apenas no contexto do workspace fornecido e no histórico desta conversa.",
    "Quando a base fornecida for insuficiente, diga isso claramente em vez de inventar.",
    "Prefira respostas objetivas, práticas e orientadas à operação jurídica do workspace.",
    "Se houver sinais conflitantes entre agentes ou registros, explicite o conflito e a sua melhor leitura.",
  ].join(" ");
}

async function loadPendingInvitationsForUser(input: {
  userId: string;
  userEmail: string;
  currentLawFirmId: string;
}) {
  const normalizedEmail = normalizeEmail(input.userEmail);
  const rows = await prisma.$queryRaw<
    Array<{
      id: string;
      invitee_email: string;
      created_at: Date;
      team_id: string;
      team_name: string;
      team_description: string | null;
      law_firm_name: string;
      invited_display_name: string | null;
      invited_first_name: string | null;
      invited_last_name: string | null;
      invited_email: string | null;
    }>
  >`
    SELECT
      ti.id,
      ti.invitee_email,
      ti.created_at,
      t.id AS team_id,
      t.name AS team_name,
      t.description AS team_description,
      lf.name AS law_firm_name,
      iu.display_name AS invited_display_name,
      iu.first_name AS invited_first_name,
      iu.last_name AS invited_last_name,
      iu.email AS invited_email
    FROM team_invitations ti
    INNER JOIN teams t ON t.id = ti.team_id
    INNER JOIN law_firms lf ON lf.id = ti.law_firm_id
    LEFT JOIN users iu ON iu.id = ti.invited_by_user_id
    WHERE ti.status_code = 'pending'
      AND ti.law_firm_id = ${input.currentLawFirmId}
      AND (
        ti.invitee_user_id = ${input.userId}
        OR LOWER(ti.invitee_email) = ${normalizedEmail}
      )
    ORDER BY ti.created_at ASC
  `;

  return rows.map((row) => ({
    id: row.id,
    email: row.invitee_email,
    invitedAt: row.created_at,
    teamId: row.team_id,
    teamName: row.team_name,
    teamDescription: row.team_description,
    lawFirmName: row.law_firm_name,
    invitedBy: getUserDisplayName({
      display_name: row.invited_display_name,
      first_name: row.invited_first_name,
      last_name: row.invited_last_name,
      email: row.invited_email,
    }),
  }));
}

async function loadMyTeamsForUser(input: {
  userId: string;
  currentLawFirmId: string;
}) {
  const rows = await prisma.$queryRaw<
    Array<{
      team_id: string;
      team_name: string;
      team_description: string | null;
      law_firm_id: string;
      law_firm_name: string;
      membership_role: string;
      joined_at: Date;
      member_count: number;
      pending_invitation_count: number;
    }>
  >`
    SELECT
      tm.team_id,
      t.name AS team_name,
      t.description AS team_description,
      tm.law_firm_id,
      lf.name AS law_firm_name,
      tm.membership_role,
      tm.joined_at,
      (
        SELECT COUNT(*)
        FROM team_memberships tm2
        WHERE tm2.team_id = tm.team_id
          AND tm2.law_firm_id = tm.law_firm_id
      ) AS member_count,
      (
        SELECT COUNT(*)
        FROM team_invitations ti
        WHERE ti.team_id = tm.team_id
          AND ti.law_firm_id = tm.law_firm_id
          AND ti.status_code = 'pending'
      ) AS pending_invitation_count
    FROM team_memberships tm
    INNER JOIN teams t ON t.id = tm.team_id
    INNER JOIN law_firms lf ON lf.id = tm.law_firm_id
    WHERE tm.user_id = ${input.userId}
    ORDER BY
      CASE WHEN tm.law_firm_id = ${input.currentLawFirmId} THEN 0 ELSE 1 END,
      tm.joined_at DESC
  `;

  return rows.map((row) => ({
    teamId: row.team_id,
    teamName: row.team_name,
    teamDescription: row.team_description,
    lawFirmId: row.law_firm_id,
    lawFirmName: row.law_firm_name,
    membershipRole: row.membership_role,
    joinedAt: row.joined_at,
    memberCount: Number(row.member_count ?? 0),
    pendingInvitationsCount: Number(row.pending_invitation_count ?? 0),
    isCurrentWorkspace: row.law_firm_id === input.currentLawFirmId,
  }));
}

async function loadTeamWorkspace(input: {
  lawFirmId: string;
  userId: string;
  userEmail: string;
  roleCodes: string[];
  lawFirmName: string;
  selectedTeamId?: string | null;
}) {
  const pendingInvitationsForMe = await loadPendingInvitationsForUser({
    userId: input.userId,
    userEmail: input.userEmail,
    currentLawFirmId: input.lawFirmId,
  });
  const myTeams = await loadMyTeamsForUser({
    userId: input.userId,
    currentLawFirmId: input.lawFirmId,
  });
  const workspaceTeams = myTeams.filter((team) => team.lawFirmId === input.lawFirmId);
  const resolvedSelectedTeamId =
    input.selectedTeamId && workspaceTeams.some((team) => team.teamId === input.selectedTeamId)
      ? input.selectedTeamId
      : workspaceTeams[0]?.teamId ?? null;

  if (!resolvedSelectedTeamId) {
    return {
      lawFirmName: input.lawFirmName,
      selectedTeamId: null,
      team: null,
      myTeams,
      pendingInvitationsForMe,
    };
  }

  const team = await getTeamContext({
    lawFirmId: input.lawFirmId,
    userId: input.userId,
    teamId: resolvedSelectedTeamId,
  });

  if (!team) {
    return {
      lawFirmName: input.lawFirmName,
      selectedTeamId: null,
      team: null,
      myTeams,
      pendingInvitationsForMe,
    };
  }

  const canManageTeam =
    team.myRole === "owner" || input.roleCodes.includes("admin");

  const [memberRows, invitationRows] = await Promise.all([
    prisma.$queryRaw<
      Array<{
        user_id: string;
        email: string;
        display_name: string | null;
        first_name: string | null;
        last_name: string | null;
        avatar_url: string | null;
        membership_role: string;
        joined_at: Date;
        agent_id: string | null;
        agent_name: string | null;
        learning_summary: string | null;
        memory_count: number | null;
        last_learning_at: Date | null;
      }>
    >`
      SELECT
        tm.user_id,
        u.email,
        u.display_name,
        u.first_name,
        u.last_name,
        u.avatar_url,
        tm.membership_role,
        tm.joined_at,
        ta.id AS agent_id,
        ta.agent_name,
        ta.learning_summary,
        (
          SELECT COUNT(*)
          FROM team_agent_memories tam
          INNER JOIN team_agents ta2 ON ta2.id = tam.agent_id
          WHERE tam.law_firm_id = ${input.lawFirmId}
            AND ta2.user_id = tm.user_id
        ) AS memory_count,
        (
          SELECT MAX(tam.created_at)
          FROM team_agent_memories tam
          INNER JOIN team_agents ta2 ON ta2.id = tam.agent_id
          WHERE tam.law_firm_id = ${input.lawFirmId}
            AND ta2.user_id = tm.user_id
        ) AS last_learning_at
      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 = ${team.teamId}
      ORDER BY
        CASE WHEN tm.membership_role = 'owner' THEN 0 ELSE 1 END,
        COALESCE(u.display_name, CONCAT(u.first_name, ' ', u.last_name), u.email) ASC
    `,
    canManageTeam
      ? prisma.$queryRaw<
          Array<{
            id: string;
            invitee_email: string;
            status_code: string;
            responded_at: Date | null;
            created_at: Date;
            invited_display_name: string | null;
            invited_first_name: string | null;
            invited_last_name: string | null;
            invited_email: string | null;
            invitee_display_name: string | null;
            invitee_first_name: string | null;
            invitee_last_name: string | null;
            invitee_user_email: string | null;
          }>
        >`
          SELECT
            ti.id,
            ti.invitee_email,
            ti.status_code,
            ti.responded_at,
            ti.created_at,
            iu.display_name AS invited_display_name,
            iu.first_name AS invited_first_name,
            iu.last_name AS invited_last_name,
            iu.email AS invited_email,
            tu.display_name AS invitee_display_name,
            tu.first_name AS invitee_first_name,
            tu.last_name AS invitee_last_name,
            tu.email AS invitee_user_email
          FROM team_invitations ti
          LEFT JOIN users iu ON iu.id = ti.invited_by_user_id
          LEFT JOIN users tu ON tu.id = ti.invitee_user_id
          WHERE ti.law_firm_id = ${input.lawFirmId}
            AND ti.team_id = ${team.teamId}
          ORDER BY ti.created_at DESC
        `
      : Promise.resolve<
          Array<{
            id: string;
            invitee_email: string;
            status_code: string;
            responded_at: Date | null;
            created_at: Date;
            invited_display_name: string | null;
            invited_first_name: string | null;
            invited_last_name: string | null;
            invited_email: string | null;
            invitee_display_name: string | null;
            invitee_first_name: string | null;
            invitee_last_name: string | null;
            invitee_user_email: string | null;
          }>
        >([]),
  ]);

  return {
    lawFirmName: input.lawFirmName,
    selectedTeamId: team.teamId,
    team: {
      id: team.teamId,
      name: team.name,
      description: team.description,
      createdAt: team.createdAt,
      createdBy: team.createdBy,
      myRole: team.myRole,
      canManage: canManageTeam,
      members: memberRows.map((row) => ({
        userId: row.user_id,
        email: row.email,
        displayName: getUserDisplayName({
          display_name: row.display_name,
          first_name: row.first_name,
          last_name: row.last_name,
          email: row.email,
        }),
        avatarUrl: row.avatar_url,
        membershipRole: row.membership_role,
        joinedAt: row.joined_at,
        isCurrentUser: row.user_id === input.userId,
        agent: row.agent_id
          ? {
              id: row.agent_id,
              agentName: row.agent_name ?? "Agente sem nome",
              learningSummary: row.learning_summary,
              memoryCount: Number(row.memory_count ?? 0),
              lastLearningAt: row.last_learning_at,
            }
          : null,
      })),
      invitations: invitationRows.map((row) => ({
        id: row.id,
        email: row.invitee_email,
        status: row.status_code,
        invitedAt: row.created_at,
        respondedAt: row.responded_at,
        invitedBy: getUserDisplayName({
          display_name: row.invited_display_name,
          first_name: row.invited_first_name,
          last_name: row.invited_last_name,
          email: row.invited_email,
        }),
        inviteeDisplayName:
          row.invitee_display_name || row.invitee_first_name || row.invitee_user_email
            ? getUserDisplayName({
                display_name: row.invitee_display_name,
                first_name: row.invitee_first_name,
                last_name: row.invitee_last_name,
                email: row.invitee_user_email,
              })
            : null,
      })),
    },
    myTeams,
    pendingInvitationsForMe,
  };
}

async function requireManageTeamAccess(input: {
  lawFirmId: string;
  userId: string;
  teamId: string;
  roleCodes: string[];
}) {
  const team = await getTeamContext({
    lawFirmId: input.lawFirmId,
    userId: input.userId,
    teamId: input.teamId,
  });

  if (!team) {
    throw new Error("Team not found");
  }

  const canManage = team.myRole === "owner" || input.roleCodes.includes("admin");
  if (!canManage) {
    throw new Error("forbidden");
  }

  return team;
}

export async function registerTeamRoutes(app: FastifyInstance) {
  app.get("/agents", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

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

    const agents = await listAccessibleAgents({
      lawFirmId: profile.lawFirm.id,
      userId: profile.user.id,
    });

    return [
      buildGlobalAssistantDirectoryItem({
        lawFirmName: profile.lawFirm.name,
        memoryCount: agents.reduce((sum, agent) => sum + Number(agent.memoryCount ?? 0), 0),
        lastLearningAt:
          agents
            .map((agent) => (agent.lastLearningAt ? new Date(agent.lastLearningAt) : null))
            .filter((value): value is Date => Boolean(value))
            .sort((left, right) => right.getTime() - left.getTime())[0] ?? null,
      }),
      ...agents,
    ];
  });

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

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

    let aiRunId: string | null = null;

    try {
      const aiRun = await createAiRun({
        lawFirmId: profile.lawFirm.id,
        runType: payload.agentId === "global" ? "workspace_global_assistant" : "team_agent_assistant",
        status: "running",
      });
      aiRunId = aiRun.id;
      if (payload.agentId === "global") {
        const globalContext = await getGlobalWorkspaceAssistantContext({
          lawFirmId: profile.lawFirm.id,
          lawFirmName: profile.lawFirm.name,
          question: payload.question.trim(),
        });

        const completion = await runTextChatCompletion({
          lawFirmId: profile.lawFirm.id,
          systemPrompt: buildGlobalWorkspaceSystemPrompt({
            lawFirmName: profile.lawFirm.name,
          }),
          messages: [
            {
              role: "user",
              content: JSON.stringify(
                {
                  workspaceProfile: {
                    lawFirmName: profile.lawFirm.name,
                    overview: globalContext.overview,
                  },
                  agentSummaries: globalContext.agentSummaries,
                  memorySummaries: globalContext.memorySummaries,
                  caseSummaries: globalContext.caseSummaries,
                  clientSummaries: globalContext.clientSummaries,
                  reviewSummaries: globalContext.reviewSummaries,
                  knowledgeDocumentSummaries: globalContext.knowledgeDocumentSummaries,
                  conversationHistory: payload.history.slice(-12),
                  currentQuestion: payload.question.trim(),
                  responseRules: [
                    "responda em texto puro",
                    "se o contexto do workspace for insuficiente, explicite essa limitação",
                    "não use markdown em excesso",
                  ],
                },
                null,
                2,
              ),
            },
          ],
          maxCompletionTokens: 1400,
        });

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

        await writeAuditLog({
          lawFirmId: profile.lawFirm.id,
          officeId: profile.user.primaryOfficeId ?? null,
          actorUserId: profile.user.id,
          entityType: "law_firm",
          entityId: profile.lawFirm.id,
          action: "workspace.global.ask",
          afterJson: {
            aiRunId: aiRun.id,
            questionLength: payload.question.trim().length,
            contextItemsUsed:
              globalContext.agentSummaries.length +
              globalContext.memorySummaries.length +
              globalContext.caseSummaries.length +
              globalContext.clientSummaries.length +
              globalContext.reviewSummaries.length +
              globalContext.knowledgeDocumentSummaries.length,
            model: completion.model,
          },
          request,
        });

        return {
          agent: globalContext.directoryItem,
          answer: completion.text,
          model: completion.model,
          usage: completion.usage,
          memoryCountUsed: globalContext.memorySummaries.length,
        };
      }

      const agent = await getAccessibleAgentContext({
        lawFirmId: profile.lawFirm.id,
        userId: profile.user.id,
        agentId: payload.agentId,
      });

      if (!agent) {
        throw reply.notFound("Agente não encontrado para os teams aos quais você pertence.");
      }

      const knowledgeDocumentSummaries = summarizeWorkspaceKnowledgeDocuments({
        rows: await loadWorkspaceKnowledgeDocumentRows(profile.lawFirm.id),
        question: payload.question.trim(),
        limit: 8,
      });

      const completion = await runTextChatCompletion({
        lawFirmId: profile.lawFirm.id,
        systemPrompt: buildAskAgentSystemPrompt({
          userDisplayName: agent.userDisplayName,
          agentName: agent.agentName,
          membershipRole: agent.membershipRole,
          systemPrompt: agent.systemPrompt,
        }),
        messages: [
          {
            role: "user",
            content: JSON.stringify(
              {
                agentProfile: {
                  agentId: agent.agentId,
                  agentName: agent.agentName,
                  representedUserName: agent.userDisplayName,
                  representedUserEmail: agent.userEmail,
                  memoryCount: agent.memoryCount,
                  learningSummary: agent.learningSummary,
                  recentMemories: agent.recentMemories,
                },
                workspaceKnowledgeDocuments: knowledgeDocumentSummaries,
                conversationHistory: payload.history.slice(-12),
                currentQuestion: payload.question.trim(),
                responseRules: [
                  "responda em texto puro",
                  "se a memória do agente for insuficiente, use a base de conhecimento do workspace quando ela ajudar",
                  "se a base ainda for insuficiente, explicite essa limitação",
                  "não use markdown em excesso",
                ],
              },
              null,
              2,
            ),
          },
        ],
        maxCompletionTokens: 1200,
      });

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

      await writeAuditLog({
        lawFirmId: profile.lawFirm.id,
        officeId: profile.user.primaryOfficeId ?? null,
        actorUserId: profile.user.id,
        entityType: "team_agent",
        entityId: agent.agentId,
        action: "team.agent.ask",
        afterJson: {
          teamId: agent.teamId,
          aiRunId: aiRun.id,
          questionLength: payload.question.trim().length,
          memoryCountUsed: agent.recentMemories.length,
          knowledgeDocumentsUsed: knowledgeDocumentSummaries.length,
          model: completion.model,
        },
        request,
      });

      return {
        agent: {
          agentId: agent.agentId,
          agentName: agent.agentName,
          teamId: agent.teamId,
          teamName: agent.teamName,
          userId: agent.userId,
          userDisplayName: agent.userDisplayName,
          userEmail: agent.userEmail,
          avatarUrl: agent.avatarUrl,
          membershipRole: agent.membershipRole,
          memoryCount: agent.memoryCount,
          lastLearningAt: agent.lastLearningAt,
          isCurrentUser: agent.userId === profile.user.id,
        },
        answer: completion.text,
        model: completion.model,
        usage: completion.usage,
        memoryCountUsed: agent.recentMemories.length,
      };
    } catch (error) {
      if (aiRunId) {
        await finishAiRun({
          aiRunId,
          status: "failed",
          errorMessage: error instanceof Error ? error.message : "Falha ao consultar o agente.",
        });
      }

      throw reply.badRequest(
        error instanceof Error ? error.message : "Falha ao consultar o agente.",
      );
    }
  });

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

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

    return loadTeamWorkspace({
      lawFirmId: profile.lawFirm.id,
      userId: profile.user.id,
      userEmail: profile.user.email,
      roleCodes: profile.user.roleCodes,
      lawFirmName: profile.lawFirm.name,
      selectedTeamId: query.teamId ?? null,
    });
  });

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

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

    const teamId = createId();
    const membershipId = createId();
    const teamName = payload.name.trim();
    const description = String(payload.description ?? "").trim() || null;

    await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        INSERT INTO teams (
          id, law_firm_id, name, description, created_by_user_id, created_at, updated_at
        ) VALUES (
          ${teamId},
          ${profile.lawFirm.id},
          ${teamName},
          ${description},
          ${profile.user.id},
          CURRENT_TIMESTAMP,
          CURRENT_TIMESTAMP
        )
      `;

      await tx.$executeRaw`
        INSERT INTO team_memberships (
          id, law_firm_id, team_id, user_id, membership_role, invited_by_user_id, joined_at
        ) VALUES (
          ${membershipId},
          ${profile.lawFirm.id},
          ${teamId},
          ${profile.user.id},
          'owner',
          ${profile.user.id},
          CURRENT_TIMESTAMP
        )
      `;
    });

    await ensureTeamAgent({
      lawFirmId: profile.lawFirm.id,
      teamId,
      userId: profile.user.id,
      displayName: profile.user.displayName,
      email: profile.user.email,
      membershipRole: "owner",
    });

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "team",
      entityId: teamId,
      action: "team.create",
      afterJson: {
        name: teamName,
        description,
      },
      request,
    });

    return reply.code(201).send(
      await loadTeamWorkspace({
        lawFirmId: profile.lawFirm.id,
        userId: profile.user.id,
        userEmail: profile.user.email,
        roleCodes: profile.user.roleCodes,
        lawFirmName: profile.lawFirm.name,
        selectedTeamId: teamId,
      }),
    );
  });

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

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

    let team;
    try {
      team = await requireManageTeamAccess({
        lawFirmId: profile.lawFirm.id,
        userId: profile.user.id,
        teamId: payload.teamId,
        roleCodes: profile.user.roleCodes,
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : "forbidden";
      if (message === "Team not found") {
        throw reply.notFound("Crie o team antes de convidar pessoas.");
      }

      throw reply.forbidden("Apenas owner ou admin podem convidar pessoas para o team.");
    }

    const email = normalizeEmail(payload.email);
    const [existingMember] = await prisma.$queryRaw<
      Array<{
        user_id: string;
      }>
    >`
      SELECT tm.user_id
      FROM team_memberships tm
      INNER JOIN users u ON u.id = tm.user_id
      WHERE tm.law_firm_id = ${profile.lawFirm.id}
        AND tm.team_id = ${team.teamId}
        AND LOWER(u.email) = ${email}
      LIMIT 1
    `;

    if (existingMember) {
      throw reply.conflict("Esse e-mail já faz parte do team.");
    }

    const [inviteeWorkspaceUser] = await prisma.$queryRaw<
      Array<{
        id: string;
      }>
    >`
      SELECT u.id
      FROM users u
      INNER JOIN workspace_memberships wm ON wm.user_id = u.id
      WHERE LOWER(u.email) = ${email}
        AND u.deleted_at IS NULL
        AND wm.law_firm_id = ${profile.lawFirm.id}
      LIMIT 1
    `;

    if (!inviteeWorkspaceUser) {
      const anyUserWithEmail = await prisma.user.findFirst({
        where: {
          email,
          deleted_at: null,
        },
        select: {
          id: true,
        },
      });

      if (anyUserWithEmail) {
        throw reply.badRequest(
          "Esse e-mail já existe, mas ainda não participa deste workspace. Adicione o usuário ao workspace antes de enviar o convite para o team.",
        );
      }

      throw reply.badRequest(
        "Só é possível convidar usuários já cadastrados neste workspace. Crie o usuário primeiro e depois envie o convite.",
      );
    }

    const inviteeUser = inviteeWorkspaceUser;

    const [existingInvitation] = await prisma.$queryRaw<
      Array<{
        id: string;
        status_code: string;
      }>
    >`
      SELECT id, status_code
      FROM team_invitations
      WHERE law_firm_id = ${profile.lawFirm.id}
        AND team_id = ${team.teamId}
        AND invitee_email = ${email}
      LIMIT 1
    `;

    const invitationId = existingInvitation?.id ?? createId();

    if (existingInvitation?.status_code === "pending") {
      throw reply.conflict("Já existe um convite pendente para esse e-mail.");
    }

    if (existingInvitation) {
      await prisma.$executeRaw`
        UPDATE team_invitations
        SET
          invitee_user_id = ${inviteeUser?.id ?? null},
          invited_by_user_id = ${profile.user.id},
          status_code = 'pending',
          responded_at = NULL,
          created_at = CURRENT_TIMESTAMP
        WHERE id = ${invitationId}
      `;
    } else {
      await prisma.$executeRaw`
        INSERT INTO team_invitations (
          id, law_firm_id, team_id, invitee_email, invitee_user_id, invited_by_user_id,
          status_code, responded_at, created_at
        ) VALUES (
          ${invitationId},
          ${profile.lawFirm.id},
          ${team.teamId},
          ${email},
          ${inviteeUser?.id ?? null},
          ${profile.user.id},
          'pending',
          NULL,
          CURRENT_TIMESTAMP
        )
      `;
    }

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "team_invitation",
      entityId: invitationId,
      action: "team.invite",
      afterJson: {
        teamId: team.teamId,
        inviteeEmail: email,
      },
      request,
    });

    return reply.code(201).send(
      await loadTeamWorkspace({
        lawFirmId: profile.lawFirm.id,
        userId: profile.user.id,
        userEmail: profile.user.email,
        roleCodes: profile.user.roleCodes,
        lawFirmName: profile.lawFirm.name,
        selectedTeamId: team.teamId,
      }),
    );
  });

  app.post("/invitations/:invitationId/respond", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { invitationId } = invitationParamsSchema.parse(request.params);
    const payload = respondInvitationSchema.parse(request.body);

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

    const [invitation] = await prisma.$queryRaw<
      Array<{
        id: string;
        law_firm_id: string;
        team_id: string;
        invitee_email: string;
        invitee_user_id: string | null;
        invited_by_user_id: string | null;
        status_code: string;
      }>
    >`
      SELECT id, law_firm_id, team_id, invitee_email, invitee_user_id, invited_by_user_id, status_code
      FROM team_invitations
      WHERE id = ${invitationId}
      LIMIT 1
    `;

    if (!invitation) {
      throw reply.notFound("Convite de team não encontrado.");
    }

    if (invitation.status_code !== "pending") {
      throw reply.badRequest("Esse convite já foi respondido.");
    }

    const inviteMatchesUser =
      invitation.invitee_user_id === profile.user.id ||
      normalizeEmail(invitation.invitee_email) === normalizeEmail(profile.user.email);

    if (!inviteMatchesUser) {
      throw reply.forbidden("Esse convite não pertence ao usuário autenticado.");
    }

    if (invitation.law_firm_id !== profile.lawFirm.id) {
      throw reply.forbidden(
        "Esse convite pertence a outro workspace. Entre com um usuário deste workspace para aceitar o convite.",
      );
    }

    if (payload.decision === "accepted") {
      const [existingMembership] = await prisma.$queryRaw<
        Array<{
          id: string;
        }>
      >`
        SELECT id
        FROM team_memberships
        WHERE law_firm_id = ${invitation.law_firm_id}
          AND team_id = ${invitation.team_id}
          AND user_id = ${profile.user.id}
        LIMIT 1
      `;

      if (!existingMembership) {
        await prisma.$executeRaw`
          INSERT INTO team_memberships (
            id, law_firm_id, team_id, user_id, membership_role, invited_by_user_id, joined_at
          ) VALUES (
            ${createId()},
            ${invitation.law_firm_id},
            ${invitation.team_id},
            ${profile.user.id},
            'member',
            ${invitation.invited_by_user_id ?? null},
            CURRENT_TIMESTAMP
          )
        `;
      }

      await ensureTeamAgent({
        lawFirmId: invitation.law_firm_id,
        teamId: invitation.team_id,
        userId: profile.user.id,
        displayName: profile.user.displayName,
        email: profile.user.email,
        membershipRole: existingMembership ? "member" : "member",
      });
    }

    await prisma.$executeRaw`
      UPDATE team_invitations
      SET
        invitee_user_id = ${profile.user.id},
        status_code = ${payload.decision},
        responded_at = NOW()
      WHERE id = ${invitationId}
    `;

    await writeAuditLog({
      lawFirmId: invitation.law_firm_id,
      officeId: null,
      actorUserId: profile.user.id,
      entityType: "team_invitation",
      entityId: invitationId,
      action:
        payload.decision === "accepted" ? "team.invitation.accept" : "team.invitation.reject",
      afterJson: {
        teamId: invitation.team_id,
        decision: payload.decision,
      },
      request,
    });

    return loadTeamWorkspace({
      lawFirmId: profile.lawFirm.id,
      userId: profile.user.id,
      userEmail: profile.user.email,
      roleCodes: profile.user.roleCodes,
      lawFirmName: profile.lawFirm.name,
      selectedTeamId:
        payload.decision === "accepted"
          ? invitation.team_id
          : null,
    });
  });
}
