import type { InterfaceSettings } from "@/lib/navigation/interface";
import type { Idioma } from "@/lib/i18n/idiomas";
import type { ModuloOpcional } from "@/lib/instalacao/modulos";
import type { CapacidadeDaOrganizacao } from "@/lib/organizacao/capacidades";

/**
 * Papéis dentro do tenant.
 *
 * `ai_operator` é o papel do AGENTE PUBLICADO, e existe SÓ no escopo do token
 * efêmero — nunca em `user_organizations`. Nenhuma pessoa o recebe, e o CHECK
 * daquela tabela segue com os quatro papéis humanos de propósito: é isso que
 * garante que ninguém ganhe autonomia de máquina por acidente de configuração.
 *
 * Ele senta ENTRE `agent` e `manager` porque descreve exatamente a faixa que
 * faltava: capacidades que um atendente humano não tem (configurar a operação,
 * mexer na régua de retorno) mas que o agente precisa para cumprir o invariante
 * 4 da doutrina — nenhuma demanda sem próximo passo. Abrir essas capacidades
 * para `agent` daria a uma PESSOA um poder que o produto não lhe dá pela tela;
 * fechá-las em `manager` tira do agente o que ele existe para fazer.
 *
 * `fn_role_at_least` no banco NÃO conhece este papel, e está certo assim: ela
 * consulta `fn_user_role_in_org`, que lê `user_organizations`. O agente não é
 * usuário. A RLS segue intacta.
 */
export type Role = "viewer" | "agent" | "ai_operator" | "manager" | "admin";
export const ROLE_RANK: Record<Role, number> = {
  viewer: 1,
  agent: 2,
  ai_operator: 3,
  manager: 4,
  admin: 5,
};

/**
 * Compara um role (possivelmente vindo solto de uma consulta, não tipado)
 * contra um mínimo. NÃO é gate de rota — isso é `requireRole()`
 * (`lib/auth/require-role.ts`), o único lugar que decide 403 e aplica o gate
 * de MFA. Este helper existe para os usos legítimos que sobram depois de uma
 * rota já ter passado por `requireRole()`: computar um campo informativo no
 * payload (ex.: `podeEditar`) ou uma regra de escopo adicional sobre o MESMO
 * role já resolvido (ex.: "autor OU manager+"). Em ambos a decisão de ACESSO
 * À ROTA já foi tomada; isto só lê o rank — nunca decide 401/403 sozinho.
 */
export function roleAtLeast(role: string | null | undefined, min: Role): boolean {
  const rank = role ? (ROLE_RANK[role as Role] ?? 0) : 0;
  return rank >= ROLE_RANK[min];
}

/**
 * O platform admin pode ESCREVER pulando o papel do tenant?
 *
 * `is_platform_admin` sozinho responde "tem a linha em platform_admins" — e o
 * `support_readonly` também tem. Todo atalho do tipo
 * `!user.is_platform_admin && ROLE_RANK[...] < ROLE_RANK.admin` deixava o
 * `support_readonly` que é membro comum de uma empresa escrever nela como se
 * administrasse. Só `scope === "full"` escreve; ausente = sem escrita.
 *
 * Mora aqui, e não em `requirePlatformAdmin.ts`, porque é puro: as server actions
 * que o usam já importam `ROLE_RANK` daqui e não ganham dependência de servidor.
 * MFA não entra: quem chama já confere `mfaEmDivida` como fazia antes.
 */
export function escreveComoPlatformAdmin(
  user: Pick<AuthUser, "is_platform_admin" | "platform_admin_scope">,
): boolean {
  return user.is_platform_admin && user.platform_admin_scope === "full";
}

/** Papéis que uma PESSOA pode ter. Espelha `user_organizations_role_check`. */
export const PAPEIS_HUMANOS: ReadonlyArray<Role> = ["viewer", "agent", "manager", "admin"];

/** Rótulo pt-BR para quem configura. `ai_operator` nunca aparece em seletor de time. */
export const ROTULO_DO_PAPEL: Record<Role, string> = {
  viewer: "Somente leitura",
  agent: "Atendente",
  ai_operator: "Assistente com autonomia de operação",
  manager: "Gerente",
  admin: "Administrador",
};

/**
 * Para papel vindo da API como `string` (listas de membros e convites). Papel
 * desconhecido devolve o próprio código: nunca some da tela.
 */
export function rotuloDoPapel(papel: string): string {
  return (ROTULO_DO_PAPEL as Record<string, string>)[papel] ?? papel;
}

/**
 * Escopo de visualização de conversas por atendente (G4-01, spec 13 §3.5;
 * quarto modo na #4022). Só restringe o papel `agent` (e o `ai_operator`, que cai
 * na mesma escada); viewer/manager/admin seguem org-wide.
 *
 * ESTA É A ÚNICA LISTA dos quatro modos. O tipo sai dela e o schema do PATCH
 * (`lib/schemas/routing.ts`) valida a partir dela — antes eram duas listas
 * literais em arquivos diferentes, mantidas iguais por um teste que comparava
 * array com array. Um modo a mais aqui passa a valer nos dois lugares sem
 * ninguém sincronizar nada.
 *
 * `own_and_sector` (#4022) é o único que olha para o SETOR: o atendente vê as
 * conversas cujo `sector_id` é de um setor em que ele está vinculado, inclusive
 * as que estão com um colega do setor. Quem o aplica é `conversations_select`
 * (via `fn_conversation_sector_scope`, migration 9214); nos LEADS o mesmo valor
 * cai no `else` de `fn_can_view_lead`, isto é, negócio continua só os seus.
 */
export const VISIBILITY_MODES = ["all", "own_and_unassigned", "own", "own_and_sector"] as const;
export type VisibilityMode = (typeof VISIBILITY_MODES)[number];
export const DEFAULT_VISIBILITY_MODE: VisibilityMode = "own_and_unassigned"; // G1-06a

/**
 * O Atendente é RESTRITO por este modo? (o predicado único, lido pela tela e
 * pelo catálogo de recursos)
 *
 * Duas coisas que o produto confunde com facilidade, e que este par separa:
 *
 *   • `visibilidadeRestritaAoAtendente` — o modo tira do Atendente a visão da
 *     organização inteira. É o que a aba "Todas" do Inbox e o recurso opcional
 *     "Atendente só vê os seus" querem dizer. `own_and_sector` também é
 *     restrito: o atendente vê o SETOR, não a organização.
 *   • `somenteOProprio` — o modo é o mais estreito (`own`), em que nem a conversa
 *     SEM DONO conta. É o modo da #3479: a conversa nasce sem dono, o Atendente
 *     não a enxerga, e é por isso que o fluxo que a abre precisa dar dono a ela
 *     (`app/api/v1/conversations/open-with-contact/route.ts`). `own_and_sector`
 *     NÃO entra aqui: o que está com um colega do setor conta.
 *
 * Antes deste par a mesma pergunta estava escrita à mão em três lugares
 * (`=== "own" || === "own_and_unassigned"`, `!== "all"` e o par do default), e
 * quem mexesse num deles deixava os outros mentindo em silêncio — o modo em que
 * a fila sem dono conta é justamente o que a #3479 mediu errado.
 *
 * Aceita `unknown` de propósito: quem lê vem de `settings` (jsonb) e pode ser
 * qualquer coisa. Valor que não é modo nenhum conta como NÃO restrito aqui — a
 * RLS é quem fecha em "não vê nada" (o `else` de `fn_can_view_conversation`),
 * e uma tela que adivinhasse "restrito" esconderia a aba de quem não devia.
 */
export function visibilidadeRestritaAoAtendente(modo: unknown): boolean {
  return modo === "own" || modo === "own_and_unassigned" || modo === "own_and_sector";
}

/** O modo mais estreito: o Atendente só enxerga o que já é dele. */
export function somenteOProprio(modo: unknown): boolean {
  return modo === "own";
}

export interface UserOrgMembership {
  interface_settings?: InterfaceSettings;
  organization_id: string;
  organization_name: string;
  role: Role;
  /**
   * Idioma padrão da organização (`organizations.locale`).
   *
   * Vem junto porque quem escolhe a organização ativa é a mesma função que
   * precisa decidir o idioma — buscá-lo depois seria uma segunda ida ao banco
   * para responder algo que a primeira já tinha em mãos.
   */
  locale?: string | null;
  /**
   * Fuso IANA da organização (`organizations.timezone`).
   *
   * Pela mesma razão do `locale` acima: quem escolhe a organização ativa é
   * quem precisa saber em que fuso a tela desenha o calendário, e buscá-lo
   * depois seria uma segunda ida ao banco para responder o que a primeira já
   * trouxe. Pode vir nulo ou inutilizável — nenhum escritor valida a coluna —,
   * então quem usa passa por `fusoValido` e cai em `FUSO_PADRAO`.
   */
  timezone?: string | null;
  /**
   * Moeda e país da organização (`organizations.currency` / `.country`).
   *
   * Mesma carona de `locale` e `timezone`, e pelo mesmo motivo: são as telas do
   * negócio e do contato que precisam deles — o rótulo do valor e o documento
   * do titular —, e sem esta carona cada diálogo cravaria `R$` e `CPF`, que foi
   * exatamente o defeito. `country` nulo significa Brasil (`PAIS_PADRAO`).
   */
  currency?: string | null;
  country?: string | null;
  /**
   * `organizations.status` da empresa. Quem decide se ela opera é `ehOperante`
   * (`lib/organizacao/operante.ts`); ausente ou nulo = NÃO operante.
   */
  org_status?: string | null;
  /** `organizations.suspended_kind` — só significa algo com status 'suspended'. */
  suspended_kind?: string | null;
}

export interface AuthUser {
  support?: import("@/lib/impersonate/support").SupportContext | null;
  id: string;
  email: string;
  full_name: string | null;
  avatar_url: string | null;
  is_platform_admin: boolean;
  /**
   * `platform_admins.scope` (`full` | `support_readonly`), nulo para quem não é
   * platform admin. Escrita de platform admin exige `=== "full"`; ausente = sem escrita.
   */
  platform_admin_scope?: string | null;
  /**
   * Idioma da interface, de `user_metadata.locale`.
   *
   * Vem no AuthUser e não numa consulta própria porque toda tela precisa dele
   * no primeiro render: buscá-lo depois faria a interface aparecer em português
   * e trocar para espanhol meio segundo depois, em toda navegação.
   */
  locale?: string | null;
  /**
   * O idioma que a interface REALMENTE usa nesta sessão — já resolvido.
   *
   * ─── Por que não bastava `locale` ──────────────────────────────────────
   *
   * `locale` é a PREFERÊNCIA de quem está logado, e ela costuma estar vazia:
   * ninguém abre o perfil antes de usar o produto. Quando está vazia, a
   * pergunta certa não é "português, então" — é "em que idioma esta empresa
   * trabalha", que é `organizations.locale`.
   *
   * Esse campo da organização existia, tinha seletor na tela de Configurações,
   * era gravado no banco… e NÃO ERA LIDO POR NINGUÉM. Medido por varredura: as
   * únicas referências eram a escrita (`app/actions/settings/updateTenant.ts`)
   * e a releitura para preencher o próprio formulário. Ou seja, exatamente o
   * defeito que originou o i18n — um seletor que não muda uma letra —, repetido
   * um andar acima e sem que ninguém percebesse.
   *
   * A ordem é: preferência da pessoa → idioma da organização → padrão do
   * produto. É ela que faz o idioma escolhido no instalador chegar a quem
   * entra: o `install.sh` grava na organização, e quem nunca abriu o perfil já
   * encontra o sistema no idioma certo.
   */
  idioma: Idioma;
  /**
   * Fuso de APRESENTAÇÃO, de `user_metadata.timezone`.
   *
   * Vem aqui pelo mesmo motivo do `locale` acima: a grade da Agenda precisa
   * dele no primeiro render, e buscá-lo depois desenharia o dia inteiro no
   * fuso errado para corrigir meio segundo depois — com os compromissos
   * pulando de posição na frente de quem está olhando.
   *
   * NÃO é o fuso da REGRA. Em que fuso as janelas de trabalho valem é
   * `attendant_availability.schedule.timezone`, e são perguntas diferentes:
   * quem está em Manaus vê a grade no horário de Manaus enquanto a jornada
   * continua valendo no fuso em que foi configurada.
   *
   * Até esta linha o campo era escrito pela tela de perfil e lido por NINGUÉM —
   * o anti-pattern "tela oferece o que o código ignora".
   */
  timezone?: string | null;
  organizations: UserOrgMembership[];
}

export interface ActiveOrg {
  interface_settings?: InterfaceSettings;
  /** Moeda da organização — o rótulo do valor do negócio sai dela. */
  currency?: string | null;
  /** País da organização (ISO-3166 alpha-2); nulo = Brasil. */
  country?: string | null;
  /** Status da org ativa (`orgAtivaSemPortao` sempre preenche). Ausente/nulo = não operante. */
  org_status?: string | null;
  /** Tipo da suspensão — só significa algo com status 'suspended'. */
  suspended_kind?: string | null;
  orgId: string;
  /** Fuso IANA da organização — ver `UserOrgMembership.timezone`. */
  timezone?: string | null;
  name: string;
  role: Role;
  /**
   * Escopo de visualização da org (G4-01). Opcional: só é preenchido no client
   * context (AppLayout) para a UI do inbox decidir visões visíveis. Não é fonte
   * de autorização — a RLS (fn_can_view_conversation) é quem garante o escopo.
   */
  visibility_mode?: VisibilityMode;
  /**
   * A regra "cliente pela agenda" está ligada nesta organização
   * (`organizations.settings.crm.cliente_pela_agenda`, migration 0262)?
   *
   * Opcional pelo mesmo motivo de `visibility_mode`: só o layout de `/app`
   * preenche, e ausente é desligado. NÃO é autorização nem é quem aplica a
   * regra — quem decide é o banco (o trigger lê a chave). Serve para a tela não
   * mostrar selo, data e funil de clientes de uma regra desligada, em que
   * `first_service_at` está congelada.
   */
  cliente_pela_agenda?: boolean;
  /**
   * A regra "cliente pela venda" está ligada nesta organização
   * (`organizations.settings.crm.cliente_pela_venda`, #3644)?
   *
   * Contato com negócio GANHO e nenhum aberto conta como cliente, e a conversa
   * nova nasce no funil de clientes em vez do de captação. Opcional pelo mesmo
   * motivo de `cliente_pela_agenda`: só o layout de `/app` preenche, e ausente é
   * desligado. Serve ao MESMO par de controles (marcar um funil como de
   * clientes e o selo do quadro), que passam a fazer sentido com qualquer uma
   * das duas regras.
   */
  cliente_pela_venda?: boolean;
  /**
   * Os módulos opcionais LIGADOS na instalação (`lib/instalacao/modulos.ts`).
   * É da instalação, não da organização — mora aqui porque este é o contexto
   * que o layout de `/app` entrega à casca. Ausente vale como nenhum: a porta
   * de módulo desligado não aparece no menu.
   */
  modulos_ligados?: readonly ModuloOpcional[];
  /**
   * Capacidades que ESTA organização ligou (`lib/organizacao/capacidades.ts`).
   * Só o layout de `/app` preenche; ausente vale como nenhuma no menu.
   */
  capacidades_ligadas?: readonly CapacidadeDaOrganizacao[];
  /**
   * O que ESTA organização definiu para si — CAMPO A CAMPO, e só o que ela
   * mesma definiu.
   *
   * Opcional pelo mesmo motivo de `visibility_mode`: só o layout de `/app`
   * preenche, e só para o campo cuja resolução aponta a camada da organização.
   * Campo ausente significa "vale o de cima" — o da instalação —, que é
   * exatamente o que o menu já mostrava. Por isso `nome` também é opcional: a
   * organização que definir só o logo não pode arrastar junto um `nome` que
   * ninguém escolheu ali.
   *
   * `logoUrl` TEM produtor desde a onda do upload: `camadaDaOrganizacao`
   * (`lib/branding/resolve.ts`) declara o logo a partir de
   * `settings.branding.logo_path`, gravado por `/api/v1/marca/logo`. O consumidor
   * (a barra lateral) entrou uma onda ANTES do produtor, de propósito e
   * declarado — foi o que permitiu que o upload fosse só a camada, sem reabrir a
   * casca inteira. Enquanto durou, não era campo decorativo pelo avesso: já
   * tinha teste de comportamento (`tests/unit/sidebar-nome-da-organizacao.test.tsx`).
   *
   * A rota é a que já existe: layout → `AuthProvider` → `useAuth()`. É como o
   * valor atravessa a fronteira servidor/navegador sem plumbing nova. A marca da
   * INSTALAÇÃO atravessa por outro caminho, e desde esta onda também vem do
   * banco: `app/layout.tsx` resolve a pilha e o `<PublicEnvScript/>` a injeta em
   * `window.__PUBLIC_ENV__`, de onde `branding()` a lê.
   */
  marca?: {
    readonly nome?: string;
    readonly logoUrl?: string | null;
    readonly logoDarkUrl?: string | null;
  };
}
