"use client";
import { ehAFila } from "@/lib/inbox/comando-da-conversa";
import { useEffect, useMemo, useCallback } from "react";
import { useT } from "@/hooks/i18n/useT";
import type { InfiniteData, UseInfiniteQueryResult } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Check } from "@/lib/ui/icons";
import { useChannelSessions } from "@/hooks/channels/useChannelSessions";
import { useConversasComMencao } from "@/hooks/inbox/useConversasComMencao";
import { desmarcarConversaMencionada } from "@/lib/notifications/mencao-na-lista";

import { useAutomaticoAtivo } from "@/hooks/ai/useAutomaticoAtivo";
import { useMarcarComoExemplo } from "@/hooks/inbox/useMarcarComoExemplo";

import { ConversationListItem } from "./ConversationListItem";
import { EmptyInbox } from "@/components/empty";
import { EmptyPorFiltro } from "./EmptyPorFiltro";
import { filtrosAuxiliaresAtivos } from "@/lib/inbox/filtros-ativos";
import type {
  ConversationsFilters,
  ConversationWithContact,
} from "@/hooks/inbox/useConversationsRealtime";

interface ListResponse {
  data: ConversationWithContact[];
  meta?: { cursor?: string | null; has_more?: boolean };
}

interface Props {
  /** Query já montada no pai — evita duplicar subscription Realtime + refetch. */
  listQuery: UseInfiniteQueryResult<InfiniteData<ListResponse>, Error>;
  filters: ConversationsFilters;
  selectedId: string | null;
  onSelect: (id: string) => void;
  /** Desliga os filtros auxiliares. Sem ele, o vazio por filtro nao oferece o botao. */
  onLimparFiltros?: () => void;
  /** Notifies parent when the visible list changes (used by keyboard nav). */
  onVisibleChange?: (ids: string[]) => void;
  /**
   * Seleção múltipla (#2484, F1). Quando `onAlternarSelecao` vem, cada linha
   * ganha a caixa de marcação e a lista oferece o "selecionar todas" — sem
   * ela, a lista é exatamente a que era.
   */
  selecionados?: Set<string>;
  onAlternarSelecao?: (id: string) => void;
  onSelecionarTodosVisiveis?: () => void;
  /**
   * Quem a rota `usable-for-rag` aceita (atendente para cima, ou admin de
   * plataforma fora do suporte). Sem isto a linha mostra o selo e não o
   * controle: o leitor veria um botão que só devolve 403.
   */
  podeMarcarExemplo?: boolean;
}

export function ConversationList({
  listQuery: q,
  filters,
  selectedId,
  onSelect,
  onVisibleChange,
  onLimparFiltros,
  selecionados,
  onAlternarSelecao,
  onSelecionarTodosVisiveis,
  podeMarcarExemplo = false,
}: Props) {
  const t = useT();
  /**
   * As conversas com menção pendente (#2372).
   *
   * Uma assinatura só para a lista inteira (não um hook por linha): a marca
   * nasce do listener de notas do AppShell e vale para todas as linhas. Abrir a
   * conversa é o que a CONSOME — por isso o clique passa por `abrirConversa`.
   */
  const mencionadas = useConversasComMencao();
  const abrirConversa = useCallback(
    (id: string) => {
      desmarcarConversaMencionada(id);
      onSelect(id);
    },
    [onSelect],
  );
  // Só mostra POR ONDE a conversa entrou quando há mais de um número. Com um
  // só, o rótulo seria a mesma palavra em toda linha — ruído que ensina o olho
  // a ignorar a área onde vivem os avisos que importam.
  //
  // `?? []` e não `undefined`: enquanto a lista de canais carrega, o certo é
  // NÃO mostrar. Mostrar e sumir depois é pior que aparecer um instante tarde.
  const canais = useChannelSessions().data ?? [];
  const maisDeUmCanal = canais.length > 1;

  // Fila (G5-03): a lista já vem ordenada por tempo de espera (server), então a
  // posição é o índice na lista visível. Só mostramos posição/espera nessa visão.
  // A Fila deixou de mandar `assigned_to=unassigned` (agora pede `comando`), e
  // sem esta linha a numeração "1º, 2º…" e o tempo de espera sumiriam da única
  // visão em que servem para alguma coisa — sem erro nenhum, só sumiriam.
  const isQueue = ehAFila(filters);
  // Uma leitura por lista, compartilhada por todas as linhas (react-query dedupa
  // com o cabeçalho, que faz a mesma pergunta).
  const automaticoDaOrg = useAutomaticoAtivo();

  /**
   * O "exemplo para a IA" (#3006) mora AQUI, uma vez por lista — e não em cada
   * linha. A mutação é a mesma para todas as linhas, e a linha é renderizada
   * sem `QueryClientProvider` em vários testes do repo: um hook por linha
   * derrubaria todos com "No QueryClient set". Mesma razão do `automaticoDaOrg`
   * logo acima.
   */
  const { alternar: alternarExemplo } = useMarcarComoExemplo();

  // Sem filtro de cliente: TODO filtro é parâmetro do schema e roda no banco.
  // `clientFilter` era o mecanismo que permitia um filtro existir fora do contrato
  // — e foi por ele que "Não lidos" virou ilha, fora da cerca que vigia os demais.
  const items = useMemo(
    () => (q.data?.pages.flatMap((p) => p.data) ?? []) as ConversationWithContact[],
    [q.data],
  );

  // Notify parent of currently-visible IDs (for j/k nav). Must use effect
  // (not render-time call) — invoking onVisibleChange during render triggers
  // setState in InboxLayout from inside ConversationList's render phase,
  // which React 19 forbids.

  /**
   * O badge de atendente só entra quando DISCRIMINA — mesma regra do badge de
   * canal, e pelo mesmo motivo escrito lá: rótulo que se repete em toda linha
   * ensina o olho a ignorar a área onde vivem os avisos que importam.
   *
   * Medido nas abas: "Fila" pede `comando=aguardando` (nenhuma linha tem dono —
   * a régua põe quem tem dono em `humano`), "Minhas" filtra `assigned_to=me`
   * (todas têm o MESMO) e "Automático" pede `comando=automatico` (também sem
   * dono, pela mesma razão). Sobram "Todas" e "Fechadas" — e mesmo nelas, só
   * vale se a página realmente tiver mais de um dono distinto.
   *
   * O `filters.comando` entrou junto com as abas novas: sem ele, a Fila voltaria
   * a repetir o mesmo selo de atendente em cada uma das linhas.
   */
  const mostrarAtendente = useMemo(() => {
    if (filters.assigned_to) return false;
    if (filters.comando && !filters.comando.includes("humano")) return false;
    const donos = new Set(
      items.map((i) => i.assigned_to_user_id).filter((id): id is string => Boolean(id)),
    );
    return donos.size > 1;
  }, [filters.assigned_to, filters.comando, items]);

  /**
   * O ícone de robô, mesma regra dos dois badges acima: só entra quando
   * DISCRIMINA. A aba "Automático" pede `comando=["automatico"]` — toda linha
   * já é robô, e repetir o ícone em cada uma vira ruído. Nas outras abas a
   * lista é mista (ou pode ser), então o ícone segue dizendo algo.
   */
  const mostrarAutomatico =
    !(filters.comando?.length === 1 && filters.comando[0] === "automatico");

  useEffect(() => {
    if (onVisibleChange) onVisibleChange(items.map((i) => i.id));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [items]);

  // O SKELETON É DA PRIMEIRA CARGA (#2366) — nunca do refetch.
  //
  // `isLoading` só é verdadeiro sem dado nenhum na tela: o refetch em voo
  // mantém a resposta anterior, e a troca de chave (quando o automatico-ativo
  // responde) herda a lista anterior pelo `placeholderData` do hook — só essa
  // troca: aba ou busca nova é outra lista, e volta ao skeleton. `items.length === 0` declara a regra em palavras — o preço de
  // reconstruí-la aqui é o defeito inteiro desta issue.
  //
  // `isPending`, e não `isLoading` (#2365): a consulta SEGURADA da Fila — a que
  // espera o fato org-wide antes de sair, para não pedir a mesma lista duas
  // vezes — está pendente SEM estar buscando, e `isLoading` seria falso ali. A
  // diferença não é cosmética: com `isLoading`, a espera de ~200ms do fato
  // caía no vazio por AUSÊNCIA e a tela dizia "Sem conversas por aqui" — caixa
  // vazia em vez de "está vindo". `isPending` continua falso no refetch (há
  // dado), então a regra do #2366 segue de pé: sem dado é primeira carga.
  if (q.isPending && items.length === 0) {
    return (
      <div className="space-y-3 p-3">
        {[1, 2, 3, 4, 5].map((i) => (
          <Skeleton key={i} className="h-16 w-full" />
        ))}
      </div>
    );
  }

  // O erro só fala quando NÃO HÁ O QUE PRESERVAR (#2366).
  //
  // Um refetch que falha com a lista na tela deixa a lista onde está: trocar
  // uma tela cheia por "Erro ao carregar conversas" é trocar um problema de
  // rede por uma caixa de entrada vazia — e o `queryFn` do hook já disparou o
  // toast com o motivo cru. Sem dado nenhum (primeira carga), o erro continua
  // sendo tudo o que existe para mostrar, com o botão de tentar de novo.
  if (q.isError && q.data === undefined) {
    return (
      <div className="p-4 text-center text-sm text-muted-foreground">
        <p>{t("Erro ao carregar conversas.")}</p>
        <Button
          size="sm"
          variant="outline"
          className="mt-2"
          onClick={() => q.refetch()}
        >
          Tentar novamente
        </Button>
      </div>
    );
  }

  // Vazio por AUSENCIA: a caixa esta mesmo vazia, e o texto pode prometer que
  // mensagens vao aparecer. Este e o unico caso que ainda sai por `return`
  // precoce, porque aqui nao ha pagina seguinte a alcancar.
  const filtrosAtivos = filtrosAuxiliaresAtivos(filters);
  if (items.length === 0 && filtrosAtivos.length === 0) {
    return (
      <div className="flex h-full items-center justify-center p-6">
        <EmptyInbox />
      </div>
    );
  }

  return (
    <div className="flex h-full flex-col">
      <div className="flex-1 overflow-y-auto">
        {/* Vazio por FILTRO: fica DENTRO do return, nunca como `return` precoce —
            e por isso o bloco do `hasNextPage` abaixo continua sendo alcancado. */}
        {items.length === 0 && filtrosAtivos.length > 0 && (
          <EmptyPorFiltro filtros={filtrosAtivos} onLimpar={onLimparFiltros} />
        )}
        {/* Grupos tem uma regra que o vazio sozinho não conta: conversa de grupo
            só nasce na primeira mensagem recebida depois de o grupo ser ligado em
            Conexões › Grupos (spec: "Só entram os grupos escolhidos. O padrão é
            desligado"). A frase diz a regra, não o diagnóstico: o filtro Grupos
            soma com Fila e Busca, e a lista pode vir vazia com grupos ligados.
            Sem esta linha o operador só ouve "Nenhuma conversa com esses
            filtros" e acha o botão quebrado (issue #2103). */}
        {items.length === 0 && filters.is_group && (
          <p className="px-6 pb-6 text-center text-xs text-text-subtle">
            {t("Só entram na inbox os grupos ligados em Conexões › Grupos, e cada um aparece aqui a partir da primeira mensagem recebida depois de ligar (o histórico anterior não entra).")}
          </p>
        )}
        {/* Cabeça da seleção em lote (#2484, F1): marca de uma vez tudo o que
            está na tela. Some com a lista vazia — oferecer "selecionar todas"
            quando não há nenhuma é botão de mentira. */}
        {onAlternarSelecao && items.length > 0 && (
          <div className="flex items-center gap-2 border-b border-border/70 px-3 py-1.5">
            <button
              type="button"
              role="checkbox"
              aria-checked={
                selecionados
                  ? items.every((c) => selecionados.has(c.id))
                  : false
              }
              aria-label={t("Selecionar todas as conversas visíveis")}
              title={t("Selecionar todas")}
              data-testid="selecionar-todas-visiveis"
              onClick={onSelecionarTodosVisiveis}
              className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border border-muted-foreground bg-background transition-colors hover:bg-surface-elevated focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent"
            >
              {selecionados && items.every((c) => selecionados.has(c.id)) && (
                <Check size={12} weight="bold" aria-hidden />
              )}
            </button>
            <span className="text-xs text-text-muted">{t("Selecionar todas")}</span>
          </div>
        )}
        {items.map((c, i) => (
          <ConversationListItem
            key={c.id}
            conversation={c}
            isSelected={c.id === selectedId}
            onSelect={abrirConversa}
            mencionado={mencionadas.has(c.id)}
            queuePosition={isQueue ? i + 1 : undefined}
            mostrarCanal={maisDeUmCanal}
            mostrarAtendente={mostrarAtendente}
            mostrarAutomatico={mostrarAutomatico}
            automaticoDaOrg={automaticoDaOrg.data}
            selecionado={selecionados?.has(c.id)}
            onAlternarSelecao={onAlternarSelecao}
            onAlternarExemplo={podeMarcarExemplo ? alternarExemplo : undefined}
          />
        ))}
        {q.hasNextPage && (
          <div className="flex justify-center p-3">
            <Button
              size="sm"
              variant="outline"
              onClick={() => q.fetchNextPage()}
              disabled={q.isFetchingNextPage}
            >
              {q.isFetchingNextPage ? t("Carregando…") : t("Carregar mais")}
            </Button>
          </div>
        )}
      </div>
    </div>
  );
}
