/**
 * Shapes canônicos das tabelas conversations e messages (Spec 03).
 * Espelha o schema do Postgres — atualizar aqui quando a migration mudar.
 */

export interface Conversation {
  id: string;
  organization_id: string;
  contact_id: string;
  channel_session_id: string;
  channel: string;
  status: string;
  status_changed_at: string;
  service_revision?: number;
  service_closed_at?: string | null;
  service_started_at?: string | null;
  current_demanda_id?: string | null;
  assigned_to_user_id: string | null;
  /**
   * A FILA da conversa (migration 9062): o setor para onde ela foi classificada.
   * `null` = ainda sem fila, que é o estado de toda conversa até alguém (ou a
   * triagem por IA) escolher. Opcional no tipo pela mesma razão dos outros
   * campos novos: o cache do react-query gravado antes deste deploy não tem a
   * chave, e a lista a lê antes da primeira resposta nova.
   */
  sector_id?: string | null;
  /**
   * Cópia desnormalizada do nome de quem atende (migration 0202), escrita por
   * `fn_conversation_assign` no mesmo UPDATE que grava `assigned_to_user_id`.
   * `null` quando não atribuída, ou quando o backfill/lookup não alcançou —
   * ver `lib/users/com-nome-do-atendente.ts` para o fallback desse caso raro.
   */
  assigned_to_user_name: string | null;
  assignee_kind: string | null;
  assigned_at: string | null;
  last_inbound_at: string | null;
  /**
   * A régua da Fila (migration 0267, issue #990): o instante da mensagem do
   * cliente MAIS ANTIGA que ninguém respondeu ainda — `min(sent_at)` dos inbound
   * posteriores a `last_outbound_at`. É o que ordena a aba Fila, o que a pílula
   * "Aguardando há…" mostra (`esperaDaConversa`) e o que a posição das ferramentas
   * de IA conta. Opcional cobrindo o intervalo entre o deploy deste código e a
   * migration aplicada; `null` quando o cliente nunca escreveu.
   */
  awaiting_since?: string | null;
  last_outbound_at: string | null;
  last_message_at: string | null;
  last_message_preview: string | null;
  unread_count_for_assignee: number;
  is_group: boolean;
  group_chat_id: string | null;
  tags: string[];
  /**
   * jsonb livre, por conversa. Entre outras marcas, mora aqui
   * `ai_com_dono: true` — o modo da #3967 em que a IA responde SEM soltar o dono
   * humano (`lib/ai/elegibilidade/ia-com-dono.ts`). A tela lê deste campo para
   * desenhar o interruptor; não é coluna nova e não precisa de leitura nova.
   */
  metadata: Record<string, unknown>;
  snooze_until: string | null;
  /**
   * Até quando o atendimento automático está desligado nesta conversa
   * (`'infinity'` depois de passar para uma pessoa). A tela precisa disto para
   * saber SE existe algo a devolver — sem o campo, o botão de retomar não teria
   * como aparecer só quando faz sentido, e a rota ficaria sem porta.
   */
  bot_silenced_until: string | null;
  /**
   * Campo CALCULADO pelo banco (migration 0203) — não é coluna, e por isso não vem
   * em `select=*`: quem o quiser tem de pedi-lo por nome. Opcional porque a
   * resposta de uma versão anterior, ainda em cache do react-query, não o tem.
   */
  comando_da_conversa?: string | null;
  /**
   * Há agente que vai responder ESTA conversa? (migration 9331, issue #4012) —
   * outro campo CALCULADO, e a segunda metade do mesmo fato: `comando_da_conversa`
   * já chega rebaixado a `aguardando` quando este é `false`, mas o cabeçalho e a
   * linha recalculam localmente (para o "devolver" simulado), e sem o fato
   * voltariam a dizer "Automático". Ausente = "não sei" (cache anterior).
   */
  automatico_da_conversa?: boolean | null;
  last_handoff_at: string | null;
  /**
   * Por que o automático está parado. Diferencia o handoff formal de uma pausa
   * por resposta no celular (`comandoDaConversa` escolhe o motivo da tela).
   */
  last_handoff_reason?: string | null;
  /**
   * A conversa está marcada como EXEMPLO para o agente aprender (#3006) — o
   * opt-in de LGPD (L-08) que a rota `POST .../usable-for-rag` liga e que o
   * cron `kb-conversations-batch` consome.
   *
   * Opcional de propósito, como `comando_da_conversa`: uma resposta em cache de
   * antes deste campo entrar no `SELECT_COLS` da lista não o tem. AUSENTE é
   * "não afirmo que está marcada", nunca "não está" — e o selo da lista só
   * aparece com `true`, porque afirmar o contrário seria pior.
   */
  usable_for_rag?: boolean;
  created_at: string;
  updated_at: string;
}

/**
 * Espelha o CHECK `messages_sent_via_check` do banco. O alias exportado existe
 * para o invariante banco×TypeScript ler a fonte real do vocabulário, sem criar
 * uma terceira lista manual só para o teste.
 */
export type SentVia = "user" | "ai" | "system" | "external_device" | "automation" | "crm";

export interface Message {
  id: string;
  organization_id: string;
  conversation_id: string;
  channel_session_id: string;
  contact_id: string;
  external_id: string | null;
  type: string;
  direction: "inbound" | "outbound";
  status: string;
  ack: number | null;
  error_code: string | null;
  error_message: string | null;
  body: string | null;
  media_url: string | null;
  media_mime: string | null;
  media_size_bytes: number | null;
  media_storage_path: string | null;
  /** Transcrição de mídia derivada (áudio→texto). Só disponível quando
   * `media_derived_status === 'ready'`. A realimentação chega aqui vinda das
   * colunas `messages.media_derived_text/status` (#2057). Elas já eram usadas
   * pela IA; agora o balão da inbox também as lê para o atendente que não
   * consegue ouvir. */
  media_derived_text?: string | null;
  media_derived_status?: string | null;
  // Espelha o CHECK do banco (messages_sent_via_check): 'crm', 'external_device',
  // 'automation', 'ai', 'user', 'system'. O tipo listava só três e o TypeScript
  // aceitava os demais só porque o dado vem do Supabase sem cast — a tela então
  // não conseguia nem NOMEAR o valor para exibi-lo (ver MessageBubble).
  sent_via: SentVia;
  sent_by_user_id: string | null;
  /**
   * O NOME de quem enviou (#4031) — e por que ele não é coluna.
   *
   * `sent_by_user_id` diz QUAL pessoa da equipe digitou, mas o balão só sabia
   * disso duas coisas: "Você" (para o autor) e "Atendente" (para todos os
   * outros). Com duas consultoras na mesma conversa, o gestor não tinha como
   * saber quem prometeu o quê — enquanto a IA já aparecia nomeada ("IA · Clara").
   *
   * O nome mora em `auth.users.raw_user_meta_data`, fora do alcance de uma
   * consulta por PostgREST — e o balão NÃO faz consulta. Por isso este campo é
   * de PAYLOAD: `GET /api/v1/conversations/[id]/messages` o preenche uma vez por
   * página (ids distintos, com `nomesDosAtendentes`), exatamente como o export
   * em PDF já fazia. Opcional porque uma resposta em cache de antes do campo
   * não o tem — e `null` é a declaração de "não resolvi o nome" (self-host sem
   * service role, linha antiga, usuário removido). Nunca é o nome de um cliente:
   * só aparece quando há `sent_by_user_id`.
   */
  sent_by_user_name?: string | null;
  /**
   * Quem DECIDIU o envio quando o que apertou foi um token (#1613, migration
   * 0416) — a pessoa "em nome de" quem a integração mandou. `null` em todo
   * envio direto, e ausente nas linhas anteriores à coluna. O balão lê este
   * campo junto de `metadata.sent_on_behalf` para dizer "Fulano · via {token}".
   */
  sent_on_behalf_of_user_id?: string | null;
  sent_at: string;
  delivered_at: string | null;
  read_at: string | null;
  metadata: Record<string, unknown>;
  /**
   * Quando o AUTOR editou no aplicativo (migration 0143). `body` já é a versão
   * nova; este campo existe para a tela poder DIZER que houve edição — ler um
   * combinado sem saber que ele mudou é como o erro começa.
   */
  edited_at: string | null;
  /** Quando o AUTOR apagou para todos. A linha fica; o texto não é mostrado. */
  revoked_at: string | null;
  /** A mensagem que esta responde (citação). `null` = envio solto. */
  reply_to_message_id: string | null;
  created_at: string;
  /**
   * Última escrita na linha (trigger `trg_messages_updated_at`). É o relógio do
   * reenvio (`lib/messaging/reenvio.ts`): linha `queued` parada há muito pode
   * ter perdido o processo que a enviava. Opcional porque linhas montadas fora
   * do `MSG_COLS` (otimistas antigas, fixtures) não o trazem.
   */
  updated_at?: string;
  /**
   * O modelo aprovado pelo qual a mensagem saiu (`type = 'template'`). Opcionais:
   * ausentes em linhas montadas fora do `MSG_COLS`. O balão os usa para dizer
   * que a mensagem foi por modelo; os botões moram em `metadata.template_buttons`.
   */
  template_name?: string | null;
  template_language?: string | null;
}

/** Nota interna de conversa (Onda 5.2) — nunca vai ao cliente, tabela separada de messages. */
export interface Note {
  id: string;
  conversation_id: string;
  body: string;
  created_by_user_id: string | null;
  created_by_name: string | null;
  created_at: string;
  /**
   * O anexo da nota (#1863, F3), em `internal-media` — nunca no
   * `whatsapp-media`, que é o bucket do canal do cliente. Os três nascem com
   * `null` e continuam `null` em toda nota só de texto: um quarto de nota antiga
   * não muda de forma, só ganha as colunas.
   *
   * O tipo de render não vem do banco: é derivado de `media_mime` por
   * `kindFromMime` (o mesmo corte que aceitou o arquivo no upload).
   */
  media_storage_path: string | null;
  media_mime: string | null;
  media_size_bytes: number | null;
  /**
   * O rastro da retenção por idade (#3756): quando o prazo da organização apaga
   * o anexo, o passo 2c de `fn_enfileirar_midia_vencida` zera os três ponteiros
   * acima e grava o "quando" e os "dias" aqui. `null` = nada foi apagado — é o
   * que separa uma nota que perdeu o anexo de uma nota que nunca teve, e é o
   * que o card lê para mostrar o aviso no lugar do arquivo.
   *
   * A redação por LGPD NÃO grava esta marca de propósito: ali o anexo não saiu
   * por política de retenção, e o aviso fala de prazo.
   */
  media_expired_at: string | null;
  media_retention_days: number | null;
}

/**
 * Mapeia o ack do WAHA para o status de `messages.status` quando o aviso MUDA o
 * status. Valores do WAHA: -1 erro, 0 pendente, 1 servidor, 2 aparelho
 * (entregue), 3 lido, 4 ouvido.
 *
 * Devolve `null` para "não muda o status": 0 (pendente, ainda não é "enviada"),
 * -1 (erro — quem trata é `handleAck`, que grava `failed`, nunca `sending`) e
 * ausente. Não há mais um `"sending"` de reserva: um aviso que não sabe o que
 * dizer não pode fazer a mensagem regredir.
 */
export function ackToStatus(ack: number | null | undefined): Message["status"] | null {
  if (ack == null) return null;
  if (ack >= 3) return "read";
  if (ack >= 2) return "delivered";
  if (ack >= 1) return "sent";
  return null;
}
