/**
 * PATCH /api/v1/contacts/[id]/carteira — põe/tira o cliente na carteira de um
 * vendedor (issue #2591, regra 1/5).
 *
 * ─── Por que existe uma rota em vez de um PATCH em `/contacts/[id]` ─────────
 *
 * Gravar `contacts.carteira_user_id` direto é o defeito, não o fix: a policy de
 * UPDATE de `contacts` não olha papel (medido pelo autor da issue), então
 * qualquer autenticado se põe como dono pela REST e leva o negócio seguinte.
 * O gatilho `trg_contacts_carteira_so_pelo_servidor` recusa a SESSÃO (42501),
 * e esta porta é a única que passa — chamando a
 * `fn_definir_carteira_do_cliente` (só do service_role, com o ator passado
 * depois do gate de papel), que revalida o ator, valida o dono
 * candidato (membro ativo da MESMA org, papel que conta) e adota os negócios
 * abertos sem dono na mesma transação.
 *
 * ─── Quem passa: `agent`+ COM ESCOPO DE POSSE (#3746) ──────────────────────
 *
 * Até a v1.94 só `manager`+ mudava a carteira, e o efeito era o Atendente
 * pedindo a um gerente para assumir o cliente que ele já atende. Hoje o piso é
 * `agent`, e o escopo é a POSSE, conferida no servidor por
 * `lib/carteira/posse.ts` (a mesma régua do POST de enrollment): o Atendente
 * muda a carteira do cliente que já está na carteira dele (e pode passá-lo
 * adiante) ou põe na carteira o cliente SEM dono cuja conversa está atribuída a
 * ele. Fora disso, 403 `fora_da_posse` com o motivo escrito. Tirar o cliente da
 * carteira de OUTRO vendedor segue sendo de `manager`+ (403
 * `carteira_permissao_negada`, doc 250, escolha 1-B). `manager`+ segue podendo tudo.
 *
 * `viewer` não entra: enxerga sem poder. Os dois `.eq` com o `organization_id`
 * da sessão são obrigatórios — o usuário não lê contato de outra org nem por
 * acidente.
 */
import { randomUUID } from "node:crypto";

import type { NextRequest } from "next/server";
import type { SupabaseClient } from "@supabase/supabase-js";
import { z } from "zod";

import { ok, fail } from "@/lib/api/wrappers";
import type { Actor } from "@/lib/api/handlers/types";
import { audit } from "@/lib/audit";
import { requireRole } from "@/lib/auth/require-role";
import { donoQualificado } from "@/lib/carteira/dono";
import { conferirPosse, MENSAGEM_FORA_DA_POSSE, respostaDaPosse } from "@/lib/carteira/posse";
import { registrarTrocaDeComando } from "@/lib/inbox/atividade-de-comando";
import { traduzir } from "@/lib/i18n/dicionario";
import { requireSupportWrite } from "@/lib/impersonate/support";
import { createAdminClient } from "@/lib/supabase/admin";
import { createClient } from "@/lib/supabase/server";

export const dynamic = "force-dynamic";

const corpoSchema = z.object({
  /** `null` tira o cliente da carteira (volta ao fluxo normal, sem dono). */
  dono_user_id: z.string().uuid().nullable(),
  origem: z.enum(["manual", "importacao", "atribuicao_automatica"]).optional(),
});

type Ctx = { params: Promise<{ id: string }> };

interface Recusa {
  status: number;
  code: string;
  /** Mensagem canônica em pt-BR; `traduzir` cuida do idioma da sessão. */
  mensagem: string;
}

/** Mensagens em pt-BR fixas, traduzidas na resposta — nunca em inglês. */
function recusaDaFuncao(erro: { code?: string; message?: string } | null): Recusa {
  const bruto = erro?.message ?? "";
  if (bruto.includes("carteira_permissao_negada")) {
    // O papel não opera carteira (o `viewer`; a rota já cobra `agent`+, então
    // isto é a segunda rede). O Atendente NÃO cai aqui: ele cai na régua de
    // posse, abaixo e na rota — o banco repete a checagem (#3746).
    return {
      status: 403,
      code: "carteira_permissao_negada",
      mensagem: "Seu papel não permite mudar a carteira do cliente.",
    };
  }
  if (bruto.includes("carteira_fora_da_posse")) {
    // A função do banco conferiu a posse e recusou — mesma frase da recusa da
    // rota, para o Atendente não receber duas explicações diferentes para a
    // mesma coisa conforme quem respondeu primeiro.
    return {
      status: 403,
      code: "fora_da_posse",
      mensagem: MENSAGEM_FORA_DA_POSSE,
    };
  }
  if (bruto.includes("carteira_dono_invalido")) {
    return {
      status: 422,
      code: "carteira_dono_invalido",
      mensagem:
        "O dono precisa ser membro ativo desta empresa, com papel de atendente, gerente ou administrador. Quem só lê, e quem saiu da equipe, não é dono de carteira.",
    };
  }
  if (bruto.includes("carteira_contato_nao_encontrado") || bruto.includes("carteira_contato_de_outra")) {
    return {
      status: 404,
      code: "contact_not_found",
      mensagem: "Contato não encontrado nesta empresa.",
    };
  }
  if (bruto.includes("carteira_origem_invalida") || bruto.includes("carteira_parametro_invalido")) {
    return {
      status: 422,
      code: "validation_failed",
      mensagem: "A origem da carteira não é válida.",
    };
  }
  return {
    status: 500,
    code: "carteira_nao_salva",
    mensagem: "Não foi possível salvar a carteira do cliente.",
  };
}

export async function PATCH(req: NextRequest, ctx: Ctx): Promise<Response> {
  const requestId = randomUUID();

  const supportDenied = await requireSupportWrite();
  if (supportDenied) return supportDenied;

  // Piso de papel: `agent`+. Quem de fato pode mudar ESTE cliente é a régua de
  // posse abaixo — o papel sozinho não distingue "meu cliente" de "cliente do
  // colega", e era essa a diferença que travava o atendimento (#3746).
  const authz = await requireRole("agent", { requestId, resource: "contacts" });
  if (!authz.ok) return authz.response;
  const orgId = authz.org.orgId;
  const { id: contactId } = await ctx.params;
  const t = (texto: string) => traduzir(texto, authz.user.idioma);

  const lido = corpoSchema.safeParse(await req.json().catch(() => ({})));
  if (!lido.success) {
    return fail("validation_failed", lido.error.issues[0]?.message ?? "corpo inválido", 422, {
      requestId,
    });
  }
  const dono = lido.data.dono_user_id;
  const origem = lido.data.origem ?? "manual";

  const supabase = await createClient();

  // Antes/para o audit (e para dizer se mudou): lido com a sessão, org junto.
  const { data: contato, error: erroDeLeitura } = await supabase
    .from("contacts")
    .select("id, carteira_user_id")
    .eq("id", contactId)
    .eq("organization_id", orgId)
    .maybeSingle();
  if (erroDeLeitura) {
    return fail("contact_not_found", t("Contato não encontrado nesta empresa."), 404, { requestId });
  }
  const antes = (contato as { carteira_user_id?: string | null } | null)?.carteira_user_id ?? null;

  // #3746 — A POSSE, aqui e não na tela. `agent` muda a carteira do cliente que
  // já está na carteira dele, ou a do cliente SEM dono cuja conversa está com
  // ele (doc 250, 1-B: a carteira de um colega é do gerente); `manager`+
  // passa direto. A recusa (403/404/500) vem da régua, com o motivo escrito —
  // nada é lido para a foto nem gravado antes desta decisão.
  const admin = createAdminClient();
  const posse = await conferirPosse({
    acao: "carteira",
    db: admin,
    organizationId: orgId,
    atorId: authz.user.id,
    papel: authz.org.role,
    contactId,
  });
  if (!posse.pode) return respostaDaPosse(posse, { traduzir: t, requestId });

  // #3732: quais conversas deste contato estavam em qual número ANTES da troca.
  // A carteira passa as conversas abertas pela `fn_conversation_assign`, que
  // pode trocar o número de cada uma; a auditoria do contato não é a linha do
  // tempo da conversa, então o "continua pelo número de {nome}" tem de ser
  // escrito por conversa, depois, comparando com esta foto. Falha de leitura
  // não impede a troca de carteira — só deixa a linha do tempo sem o registro.
  const numerosAntes = await conversasDoContato(admin, orgId, contactId);

  const { error } = await admin.rpc("fn_definir_carteira_do_cliente", {
    p_org: orgId,
    p_actor: authz.user.id,
    p_contact: contactId,
    p_dono: dono,
    p_origem: origem,
  });
  if (error) {
    const recusa = recusaDaFuncao(error);
    return fail(recusa.code, t(recusa.mensagem), recusa.status, { requestId });
  }

  if (dono !== null) {
    await registrarNumerosQueMudaram({
      supabase,
      admin,
      organizationId: orgId,
      contactId,
      antes: numerosAntes,
      ator: { type: "user", id: authz.user.id, role: authz.org.role },
    });
  }

  await audit({
    action: "contact.carteira_changed",
    resourceType: "contact",
    resourceId: contactId,
    requestId,
    metadata: {
      antes: antes ?? null,
      depois: dono ?? null,
      origem: dono === null ? null : origem,
      adotados: dono !== null, // a RPC adota, na mesma transação, os negócios abertos sem dono
    },
  });

  return ok({ contact_id: contactId, carteira_user_id: dono ?? null }, { requestId });
}

/**
 * GET /api/v1/contacts/[id]/carteira — quem é o dono HOJE, para a TELA (#2591).
 *
 * ─── Por que isto vive na mesma porta do PATCH, e não numa rota nova ────────
 *
 * A tela da carteira é uma feature, um recurso, um caminho: quem grava e quem lê
 * o mesmo dado moram no mesmo arquivo. Uma rota `.../carteira/leitura` só
 * duplicaria o `requireRole` e o filtro de organização para dizer a mesma coisa.
 *
 * ─── O que ela devolve, e o que ela NÃO devolve ─────────────────────────────
 *
 * Devolve `dono_user_id` e `qualificado` — a MESMA régua do motor
 * (`lib/carteira/dono.ts`), aplicada aqui sobre o vínculo de hoje: dono que saiu
 * da equipe ou que só lê NÃO conta (regra 5), e a tela precisa saber disso para
 * não prometer o que o motor não honra.
 *
 * NÃO devolve o NOME do dono: quem tem os nomes é a lista de membros
 * (`/team/assignable`), que a tela já carrega para trocar o dono. Resolver o nome
 * aqui criaria uma segunda fonte de nomes para o mesmo id.
 *
 * Devolve também `pode_alterar` (#3746): a MESMA régua de posse do PATCH,
 * aplicada a este contato e a este usuário. A tela não escolhe por papel — ela
 * lê esta resposta e desenha o seletor só quando o servidor aceitaria a
 * gravação. Duas réguas (uma na tela, outra na rota) divergiriam na primeira
 * mudança, e a tela prometeria o que a rota recusa.
 *
 * `agent`+ lê (a regra 2 é para quem atende). O recorte de `viewer` — "vê só se
 * for da própria carteira, sem o nome dos outros" — fica FORA deste PR, como o
 * #2667 já declarou: exigiria visão por coluna, que o Postgres/RLS não faz.
 */
export async function GET(_req: NextRequest, ctx: Ctx): Promise<Response> {
  const requestId = randomUUID();

  const authz = await requireRole("agent", { requestId, resource: "contacts" });
  if (!authz.ok) return authz.response;
  const orgId = authz.org.orgId;
  const { id: contactId } = await ctx.params;
  const t = (texto: string) => traduzir(texto, authz.user.idioma);

  const supabase = await createClient();
  // Os dois `.eq` (id + organization_id da sessão) são obrigatórios: a RLS de
  // contacts deixa passar as orgs do usuário, e um membro de duas leria a
  // carteira do contato da org que não está selecionada.
  const { data: contato, error } = await supabase
    .from("contacts")
    .select("carteira_user_id")
    .eq("id", contactId)
    .eq("organization_id", orgId)
    .maybeSingle();
  if (error || !contato) {
    return fail("contact_not_found", t("Contato não encontrado nesta empresa."), 404, { requestId });
  }

  const dono = (contato as { carteira_user_id?: string | null }).carteira_user_id ?? null;
  // Falha de leitura do vínculo degrada para "não conta" (a faixa/cartão trata
  // como dono inativo): nunca é a leitura de tela que estoura a ficha inteira.
  const admin = createAdminClient();
  let qualificado = false;
  if (dono) {
    try {
      qualificado = await donoQualificado(admin, orgId, dono);
    } catch {
      qualificado = false;
    }
  }

  // #3746 — a capacidade, respondida pelo SERVIDOR com a régua do PATCH. Falha
  // de leitura devolve `false` (a tela esconde o seletor) em vez de estourar a
  // ficha: o botão a menos é um incômodo, a ficha em branco é um defeito.
  const posse = await conferirPosse({
    acao: "carteira",
    db: admin,
    organizationId: orgId,
    atorId: authz.user.id,
    papel: authz.org.role,
    contactId,
  });

  return ok(
    { contact_id: contactId, dono_user_id: dono, qualificado, pode_alterar: posse.pode },
    { requestId },
  );
}

/** O número em que cada conversa do contato está — a foto que a troca compara. */
interface NumeroDaConversa {
  id: string;
  channel_session_id: string | null;
  assigned_to_user_name: string | null;
}

/**
 * Leitura pelo cliente ADMIN, filtrada pela organização do cookie: a foto é do
 * contato inteiro, e conversa com outra pessoa não é visível pela RLS de quem
 * troca a carteira. Falha de leitura devolve lista vazia — a troca de carteira
 * não depende dela.
 */
async function conversasDoContato(
  admin: SupabaseClient,
  orgId: string,
  contactId: string,
): Promise<NumeroDaConversa[]> {
  const { data, error } = await admin
    .from("conversations")
    .select("id, channel_session_id, assigned_to_user_name")
    .eq("organization_id", orgId)
    .eq("contact_id", contactId);
  if (error || !data) return [];
  return data as NumeroDaConversa[];
}

/**
 * #3732 — o número que mudou na troca de carteira vira linha na linha do tempo.
 *
 * A `fn_definir_carteira_do_cliente` passa as conversas abertas pela
 * `fn_conversation_assign`, que já troca o número junto com o dono; o que faltava
 * era o registro na conversa — a auditoria do CONTATO (`contact.carteira_changed`)
 * conta a troca de dono, não que o cliente passou a ser atendido por outro
 * WhatsApp. A frase é a mesma da transferência ("Continua pelo número de
 * {nome}"), porque é a mesma coisa que aconteceu: a conversa mudou de número.
 *
 * Fire-and-forget por dentro de `registrarTrocaDeComando`: a carteira já foi
 * gravada, e a linha do tempo não pode derrubar a operação que ela descreve.
 */
async function registrarNumerosQueMudaram(p: {
  supabase: SupabaseClient;
  admin: SupabaseClient;
  organizationId: string;
  contactId: string;
  antes: NumeroDaConversa[];
  ator: Actor;
}): Promise<void> {
  const depois = await conversasDoContato(p.admin, p.organizationId, p.contactId);
  if (depois.length === 0) return;
  const antesPorId = new Map(p.antes.map((c) => [c.id, c.channel_session_id]));

  for (const conversa of depois) {
    if (!antesPorId.has(conversa.id)) continue;
    const anterior = antesPorId.get(conversa.id) ?? null;
    if (anterior === conversa.channel_session_id) continue;
    await registrarTrocaDeComando({
      supabase: p.supabase,
      organizationId: p.organizationId,
      conversationId: conversa.id,
      contactId: p.contactId,
      tipo: "conversation_transferred",
      actor: p.ator,
      motivo: `Continua pelo número de ${conversa.assigned_to_user_name?.trim() || "outro atendente"}`,
      payload: { numero_anterior: anterior, numero_novo: conversa.channel_session_id },
    });
  }
}
