/**
 * Back office — ficha do cliente.
 *
 * Uma página com separadores (ver docs/padroes-interface.md), e cada separador
 * diz a verdade sobre o que sabe:
 *
 *   Conta        real — identificação, utilizadores, lojas, anunciantes
 *   Actividade   real — o `audit_log` desta conta
 *   Brand kit    real desde 24 Set — o mesmo ecrã de identidade que o cliente vê
 *   Conhecimento real desde 22 Set — as fontes, e a porta para a revisão de factos
 *
 * A versão anterior deste ecrã mostrava cores, tom de voz e fontes de
 * conhecimento de um cliente inventado como se fossem da conta. Com dados de
 * verdade por baixo, isso passava a ser uma mentira dentro de um ecrã
 * verdadeiro, e saiu. Os dois separadores que ainda não têm tabela dizem o que
 * lá vai ficar — que é, aliás, o que se decidiu a 21 Set em
 * docs/materiais-aceites.md.
 *
 * O caso das agências, que era o aviso vermelho deste ecrã, ficou resolvido
 * pelo `parent_agency_id`: cada anunciante é uma conta com a sua própria ficha.
 * Deixou de haver "selector sem onde guardar" — há ligações entre contas.
 */

const ROTULO_ESTADO_CONTA = {
  activo: { texto: 'Activa', variante: 'sucesso' },
  por_activar: { texto: 'Por activar', variante: 'primario' },
  suspenso: { texto: 'Suspensa', variante: 'erro' },
  cancelado: { texto: 'Cancelada', variante: 'contorno' },
};

// Os mesmos nomes do convite (modal_convidar.jsx): a ficha dizia «Dono» a quem o
// convite tinha chamado «Responsável» (ensaio da demo de 12 Out, P10).
const ROTULO_PERFIL = { owner: 'Responsável', editor: 'Editor', viewer: 'Leitura' };

/**
 * As mudanças de estado que fazem sentido a partir de cada estado.
 *
 * A chave é o vocabulário do ecrã (o `comoEcra` da API traduz `active` para
 * `activo`); o `accao` é o do enum da base, que é o que a API recebe de volta.
 * As duas línguas ficam aqui, lado a lado, em vez de espalhadas pelo JSX — foi
 * a confundi-las que mostrei "Cancelar conta" numa conta já cancelada.
 *
 * O servidor recusa na mesma o que não pode: isto é o ecrã a não oferecer um
 * botão que só serve para dar erro.
 */
const ACCOES_POR_ESTADO = {
  activo: [
    { accao: 'suspended', rotulo: 'Suspender' },
    { accao: 'cancelled', rotulo: 'Cancelar conta' },
  ],
  suspenso: [
    { accao: 'active', rotulo: 'Reactivar' },
    { accao: 'cancelled', rotulo: 'Cancelar conta' },
  ],
  por_activar: [{ accao: 'cancelled', rotulo: 'Cancelar conta' }],
  // De cancelada não há nada a fazer aqui.
  cancelado: [],
};

const SEPARADORES_FICHA = [
  // Primeiro o trabalho: o que a equipa quer saber de um cliente é de quem é a vez (Ricardo, 4 Nov).
  { chave: 'trabalho', rotulo: 'Campanhas e vídeos' },
  { chave: 'conta', rotulo: 'Conta' },
  { chave: 'actividade', rotulo: 'Actividade' },
  { chave: 'brandkit', rotulo: 'Brand kit' },
  { chave: 'conhecimento', rotulo: 'Conhecimento' },
];

/**
 * Os sete passos do onboarding, pela ordem do assistente do FC.
 *
 * `verifica` diz como saber, com o que a base tem hoje, se um passo marcado como
 * feito tem mesmo algo por baixo; `semBase` é o que se diz quando não tem. Um passo
 * sem `verifica` não se consegue confirmar: o ecrã mostra o que o
 * `onboarding_progress` diz e nada mais. `porFazer` é a frase de um passo que ainda
 * não chegou — dita em linguagem de produto, sem nomes de integrações (demo de 12 Out:
 * «DocuSign», «Moloni» e «Brevo» eram perguntas à espera de ser feitas).
 *
 * O contrato e a facturação tratados fora da plataforma (`onboarding.manual`) contam
 * como feitos: a API diz quem os marcou e porquê, e é isso que a linha mostra.
 */
const PASSOS_ONBOARDING = [
  { chave: 'data', titulo: 'Dados' },
  {
    chave: 'contract', titulo: 'Contrato',
    verifica: (c) => c.onboarding.tem_contrato || !!c.onboarding.manual?.contract,
    semBase: 'não há nenhum contrato registado',
  },
  {
    chave: 'kb', titulo: 'Base de conhecimento',
    verifica: (c) => c.onboarding.tem_kb, semBase: 'não há nenhuma fonte lida na base de conhecimento',
    // A autoria certa (Codex, 7 Out): só se diz «pelo cliente» do que o cliente confirmou.
    feitoComo: (c) => {
      const cli = c.onboarding.factos_do_cliente || 0;
      const ver = c.onboarding.factos_verificados || 0;
      if (cli > 0) return `Feito — ${cli} ${cli === 1 ? 'dado da marca confirmado' : 'dados da marca confirmados'} pelo cliente`;
      if (ver > 0) return `Feito — ${ver} ${ver === 1 ? 'dado da marca verificado' : 'dados da marca verificados'}`;
      return 'Feito — o material da marca já foi lido';
    },
    porFazer: () => 'Ainda não — o cliente carrega os ficheiros em A minha marca',
  },
  {
    // O agente configura-se pela voz da marca (lib/voz.js) desde 24 Set: sem
    // ela aceite, o Agente PM escreve num tom neutro e trata por tu.
    chave: 'agent', titulo: 'Voz da marca',
    verifica: (c) => c.onboarding.tem_agente, semBase: 'a voz da marca ainda não foi aceite',
    feitoComo: (c) => (c.onboarding.voz_aceite_por === 'cliente' ? 'Feito — confirmada pelo cliente'
      : c.onboarding.voz_aceite_por === 'netscreen' ? 'Feito — aceite pela equipa NetScreen' : 'Feito — aceite'),
    porFazer: () => 'Ainda não — o cliente confirma-a em A minha marca',
  },
  {
    chave: 'billing', titulo: 'Facturação',
    verifica: (c) => !!c.onboarding.manual?.billing, semBase: 'não há facturação registada',
  },
  { chave: 'hardware', titulo: 'Hardware', porFazer: () => 'Ainda sem painéis atribuídos' },
  {
    chave: 'invite', titulo: 'Convite',
    // Num anunciante quem entra é a gente da agência (migração 064): não precisa de gente sua.
    verifica: (c) => c.utilizadores.length > 0 || !!c.agencia, semBase: 'não há ninguém na conta',
    feitoComo: (c) => (c.agencia && !c.utilizadores.length ? 'Feito: entram as pessoas da agência' : 'Feito — a pessoa já definiu a password'),
    porFazer: (c) => {
      if (c.agencia) return { texto: 'À espera que alguém da agência defina a password (o convite é na ficha da agência)' };
      // Um link gerado é meio caminho: diz-se isso, e não que o passo está por fazer.
      const pendentes = c.utilizadores.filter((u) => !u.activated_at && u.invite_expires_at);
      if (pendentes.some((u) => diasAte(u.invite_expires_at) >= 0)) {
        return { texto: 'Link gerado, à espera que a pessoa defina a password' };
      }
      if (pendentes.length) return { texto: 'O link caducou — gera outro em Utilizadores', aviso: true };
      return { texto: 'Por fazer — gera o link em Utilizadores' };
    },
  },
];

/** «Tratado fora da plataforma por Ana, a 07/10/2026: contrato em papel.» */
function fraseManual(m) {
  const quem = m.por ? ` por ${m.por}` : '';
  const quando = m.em ? `, a ${formatData(m.em)}` : '';
  // Sem repetir o «tratados fora» que um motivo antigo já trazia (percurso de raiz, 9 Out).
  const motivo = m.motivo ? m.motivo.replace(/^tratad[oa]s? fora da plataforma[:.,]?\s*/i, '') : '';
  return `Tratado fora da plataforma${quem}${quando}${motivo ? `: ${motivo.charAt(0).toUpperCase()}${motivo.slice(1)}` : ''}`;
}

/**
 * O contrato ou a facturação tratados fora da plataforma (6 Out). Sem a assinatura e a
 * facturação ligadas, nenhum gesto os marcava, e uma conta criada do zero nunca se
 * activava. Pede o motivo, que fica na auditoria; quem não é Admin recebe a recusa.
 */
function MarcarForaDaPlataforma({ slug, passo, onFeito }) {
  const [aberto, setAberto] = React.useState(false);
  const [motivo, setMotivo] = React.useState('');
  const [ocupado, setOcupado] = React.useState(false);
  const [erro, setErro] = React.useState(null);
  const marcar = async () => {
    setOcupado(true);
    setErro(null);
    try {
      await api.post(`/api/admin/accounts/${slug}/onboarding/${passo}`, { motivo: motivo.trim() });
      setAberto(false);
      onFeito();
    } catch (e) {
      setErro(e.status === 403 ? 'Só um Admin da NetScreen pode marcar isto.' : e.message);
    } finally {
      setOcupado(false);
    }
  };
  if (!aberto) {
    return (
      <button type="button" onClick={() => setAberto(true)} className="mt-1 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline">
        Tratado fora da plataforma
      </button>
    );
  }
  return (
    <div className="mt-2 space-y-2">
      <label htmlFor={`fora-${passo}`} className="block text-xs text-muted-foreground">Como foi tratado (fica no registo)</label>
      <input
        id={`fora-${passo}`}
        value={motivo}
        onChange={(e) => setMotivo(e.target.value)}
        placeholder={passo === 'contract' ? 'Contrato assinado em papel a…' : 'Facturas emitidas pela contabilidade a partir de…'}
        className="w-full border border-border bg-background px-3 py-1.5 text-xs text-foreground outline-none focus-visible:border-primary"
      />
      {erro && <p className="text-xs text-perigo-texto" role="alert">{erro}</p>}
      <div className="flex gap-2">
        <button type="button" disabled={ocupado || !motivo.trim()} onClick={marcar} className="bg-primary px-3 py-1 text-xs text-primary-foreground hover:bg-primary/90 disabled:opacity-50">
          {ocupado ? 'A marcar…' : 'Marcar como feito'}
        </button>
        <button type="button" onClick={() => setAberto(false)} className="border border-border px-3 py-1 text-xs hover:bg-secondary">Cancelar</button>
      </div>
      {/* O botão fica desligado até haver motivo, e dizia-se nada (teste de raiz, 9 Out). */}
      {!motivo.trim() && <p className="text-xs text-muted-foreground">Escreve como foi tratado para poderes marcar.</p>}
    </div>
  );
}

/**
 * O que falta para a conta se activar, à vista no topo da ficha, com o contrato e a facturação
 * marcados de uma vez (teste de raiz do Ricardo, 9 Out: a conta ficava «Por activar» e a ligação
 * «Tratado fora da plataforma», pequena e a meio da página, não se via; não havia por onde activar).
 */
function ActivarConta({ conta, slug, onFeito, ehAdmin = true }) {
  // Uma conta antiga, sem registo de onboarding: marcar o contrato e a facturação cria-o (9 Out; dava
  // 404 e só se resolvia por SQL). Até lá não se sabe do convite, e não se diz que falta.
  const semRegisto = !conta.onboarding;
  const passos = conta.onboarding ? conta.onboarding.passos : { data: true, invite: true };
  // Num anunciante entra a gente da agência: o convite fecha-se lá (migração 064).
  const convite = conta.agencia ? 'alguém da agência definir a password (o convite)' : 'a pessoa definir a password (o convite)';
  const falta = [['data', 'os dados'], ['contract', 'o contrato'], ['billing', 'a facturação'], ['invite', convite]]
    .filter(([k]) => !passos[k]);
  const porMarcar = ['contract', 'billing'].filter((k) => !passos[k]);
  const [aberto, setAberto] = React.useState(false);
  const [motivo, setMotivo] = React.useState('');
  const [ocupado, setOcupado] = React.useState(false);
  const [erro, setErro] = React.useState(null);
  if (conta.estado !== 'por_activar') return null;
  if (!falta.length) return null;
  const nomePasso = { contract: 'o contrato', billing: 'a facturação' };
  const marcar = async () => {
    setOcupado(true);
    setErro(null);
    const feitos = [];
    try {
      for (const passo of porMarcar) {
        await api.post(`/api/admin/accounts/${slug}/onboarding/${passo}`, { motivo: motivo.trim() });
        feitos.push(passo);
      }
      setAberto(false);
    } catch (e) {
      // Os dois passos são independentes: o que ficou marcado fica, e diz-se (Codex, 9 Out).
      const falhou = porMarcar.find((p) => !feitos.includes(p));
      const motivoErro = e.status === 403 ? 'só um Admin da NetScreen pode marcar isto' : e.message;
      setErro(feitos.length
        ? `Ficou marcado ${feitos.map((p) => nomePasso[p]).join(' e ')}; ${nomePasso[falhou]} não (${motivoErro}). Tenta outra vez: só se marca o que falta.`
        : `Não ficou nada marcado: ${motivoErro}.`);
    } finally {
      setOcupado(false);
      onFeito();
    }
  };
  const lista = falta.map(([, t]) => t);
  return (
    <div className="space-y-3 rounded-md border border-[hsl(var(--primary)/0.4)] bg-[hsl(var(--primary)/0.06)] p-4 text-sm">
      <p className="font-medium text-foreground">Esta conta está por activar</p>
      {/* «Quando estiverem feitos o convite» lia-se partido (teste de raiz, 9 Out). */}
      <p className="text-muted-foreground">{`Activa-se sozinha. Falta ${lista.length > 1 ? `${lista.slice(0, -1).join(', ')} e ${lista[lista.length - 1]}` : lista[0]}.`}</p>
      {semRegisto && (
        <p className="text-xs text-muted-foreground">É uma conta anterior ao registo de onboarding. Ao marcar o contrato e a facturação, o registo cria-se, e a ficha passa a mostrar o resto.</p>
      )}
      {porMarcar.length > 0 && !ehAdmin && (
        <p className="text-xs text-muted-foreground">{`Marcar ${porMarcar.map((p) => nomePasso[p]).join(' e ')} como tratados fora da plataforma é com um Admin da NetScreen.`}</p>
      )}
      {erro && !aberto && <p className="text-xs text-perigo-texto" role="alert">{erro}</p>}
      {porMarcar.length > 0 && ehAdmin && !aberto && (
        <button type="button" onClick={() => setAberto(true)} className="ns-btn ns-btn-sm ns-btn-primario">
          {porMarcar.length === 2 ? 'Marcar contrato e facturação como tratados fora' : `Marcar ${porMarcar[0] === 'contract' ? 'o contrato' : 'a facturação'} como tratado fora`}
        </button>
      )}
      {aberto && (
        <div className="space-y-2">
          <label htmlFor="activar-motivo" className="block text-xs text-muted-foreground">Como foi tratado (fica no registo)</label>
          <input
            id="activar-motivo"
            value={motivo}
            onChange={(e) => setMotivo(e.target.value)}
            placeholder="Contrato assinado em papel; facturas pela contabilidade"
            className="w-full border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:border-primary"
          />
          {erro && <p className="text-xs text-perigo-texto" role="alert">{erro}</p>}
          <div className="flex gap-2">
            <button type="button" disabled={ocupado || !motivo.trim()} onClick={marcar} className="ns-btn ns-btn-sm ns-btn-primario">
              {ocupado ? 'A marcar…' : 'Marcar como feito'}
            </button>
            <button type="button" onClick={() => setAberto(false)} className="ns-btn ns-btn-sm ns-btn-secundario">Cancelar</button>
          </div>
          {!motivo.trim() && <p className="text-xs text-muted-foreground">Escreve como foram tratados para poderes marcar.</p>}
        </div>
      )}
    </div>
  );
}

/**
 * O plano e os créditos de IA do mês, com os dois gestos que os mudam (9 Out, becos sem saída): o
 * cliente lia «fala com a NetScreen para mudar de plano» e «até a NetScreen os alargar», e a ficha
 * não tinha por onde. Só o Admin muda (as rotas recusam os outros); o Operator vê com quem é.
 */
/**
 * Um anunciante novo numa agência que já existe (percurso de raiz, 9 Out: só se criavam ao criar a agência,
 * e a ficha dizia «ainda não tem anunciantes» sem botão). A rota é a de criar uma conta; só o Admin.
 */
function NovoAnunciante({ conta, slug, onCriado }) {
  const [aberto, setAberto] = React.useState(false);
  const [nome, setNome] = React.useState('');
  const [fora, setFora] = React.useState(true);
  const [motivo, setMotivo] = React.useState('A assinatura e a facturação ainda não estão ligadas à plataforma.');
  const [ocupado, setOcupado] = React.useState(false);
  const [erro, setErro] = React.useState(null);
  const slugNovo = `${slug}-${nome.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
    .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')}`.slice(0, 60).replace(/-+$/, '');
  const pode = nome.trim() && (!fora || motivo.trim()) && !ocupado;
  const criar = async () => {
    setOcupado(true);
    setErro(null);
    try {
      await api.post('/api/admin/accounts', {
        slug: slugNovo, nome: nome.trim(), tipo: 'sub_cliente', agencia: slug, plano: conta.plano_codigo,
        ...(fora ? { tratados_fora: motivo.trim() } : {}),
      });
      setAberto(false);
      setNome('');
      onCriado(slugNovo);
    } catch (e) {
      setErro(e.message || 'Não foi possível criar o anunciante.');
    } finally {
      setOcupado(false);
    }
  };
  if (!aberto) {
    return (
      <div className="border-t border-border px-5 py-3">
        <button type="button" onClick={() => setAberto(true)} className="ns-btn ns-btn-sm ns-btn-secundario text-xs">Acrescentar anunciante</button>
      </div>
    );
  }
  return (
    <div className="space-y-3 border-t border-border px-5 py-4 text-sm">
      <label className="block">
        <span className="ns-rotulo">Nome do anunciante</span>
        <input value={nome} onChange={(e) => setNome(e.target.value)} className="mt-1 w-full border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:border-primary" autoFocus />
      </label>
      <label className="flex items-start gap-2">
        <input type="checkbox" checked={fora} onChange={(e) => setFora(e.target.checked)} className="mt-0.5 accent-primary" />
        <span>Contrato e facturação tratados fora da plataforma</span>
      </label>
      {fora && (
        <label className="block">
          <span className="ns-rotulo">Como foram tratados (fica no registo)</span>
          <input value={motivo} onChange={(e) => setMotivo(e.target.value)} className="mt-1 w-full border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:border-primary" />
        </label>
      )}
      <p className="text-xs text-muted-foreground">
        {fora
          ? 'Entra a gente da agência: activa-se quando alguém da agência tiver definido a password.'
          : 'Depois, marca o contrato e a facturação na ficha do anunciante.'}
        {fora && !motivo.trim() ? ' Escreve como foram tratados para poderes criar.' : ''}
      </p>
      {erro && <p role="alert" className="text-xs text-perigo-texto">{erro}</p>}
      <div className="flex gap-2">
        <button type="button" disabled={!pode} onClick={criar} className="ns-btn ns-btn-sm ns-btn-primario text-xs">
          {ocupado ? 'A criar…' : 'Criar anunciante'}
        </button>
        <button type="button" onClick={() => setAberto(false)} className="ns-btn ns-btn-sm ns-btn-secundario text-xs">Cancelar</button>
      </div>
    </div>
  );
}

function PlanoECreditos({ conta, slug, onFeito, ehAdmin }) {
  const [aberto, setAberto] = React.useState(null); // 'plano' | 'creditos' | null
  const [planos, setPlanos] = React.useState(null);
  const [plano, setPlano] = React.useState('');
  const [valor, setValor] = React.useState('');
  const [motivo, setMotivo] = React.useState('');
  const [ocupado, setOcupado] = React.useState(false);
  const [erro, setErro] = React.useState(null);
  const [feito, setFeito] = React.useState(null);
  const c = conta.creditos || {};
  const eur = (n) => formatEuros(n);

  const abrir = (qual) => {
    setAberto(qual);
    setErro(null);
    setFeito(null);
    setMotivo('');
    if (qual === 'creditos') setValor(c.dados == null ? '' : String(c.dados));
    if (qual === 'plano') {
      setPlano('');
      // O tecto actual à partida, para um sob consulta não o perder ao mudar (agentes, 20 Out).
      setValor(c.dados == null ? '' : String(c.dados));
      if (!planos) api.get('/api/admin/plans').then((r) => setPlanos(r.planos)).catch((e) => setErro(e.message));
    }
  };
  const guardar = async () => {
    setOcupado(true);
    setErro(null);
    try {
      if (aberto === 'plano') {
        const r = await api.patch(`/api/admin/accounts/${slug}/plano`, {
          plano, motivo: motivo.trim(), ...(semTecto ? { creditos: valorNumero } : {}),
        });
        setFeito(`O plano passou a ${r.plano_nome}.${r.contrato_noutro_plano ? ` O contrato assinado continua no plano ${r.contrato_noutro_plano.plano}: falta o aditamento.` : ''}`);
      } else {
        const r = await api.patch(`/api/admin/accounts/${slug}/creditos`, { creditos: valorNumero, motivo: motivo.trim() });
        setFeito(r.bloqueada
          ? `Os créditos do mês passaram a ${eur(r.creditos)}, mas já se gastou ${eur(r.gastos)}: continuam esgotados.`
          : `Os créditos do mês passaram a ${eur(r.creditos)}.${c.bloqueada ? ' O cliente pode voltar a escrever.' : ''}`);
      }
      setAberto(null);
      onFeito();
    } catch (e) {
      setErro(e.status === 403 ? 'Só um Admin da NetScreen pode mudar isto.' : e.message);
    } finally {
      setOcupado(false);
    }
  };

  // Um plano sob consulta não traz créditos de IA (o Multi-loja): o tecto deste mês pede-se ao mudar,
  // senão o mês ficava sem tecto (agentes, 20 Out). A mesma regra da rota.
  const destino = aberto === 'plano' && planos ? planos.find((p) => p.codigo === plano) : null;
  const semTecto = Boolean(destino && !destino.publico && !destino.creditos_ia);
  const pedeValor = aberto === 'creditos' || semTecto;
  const valorNumero = Number(String(valor).replace(',', '.'));
  const valorValido = !pedeValor || (String(valor).trim() !== '' && Number.isFinite(valorNumero) && valorNumero >= 0);
  const podeGuardar = !ocupado && motivo.trim() && (aberto === 'plano' ? plano && valorValido : valorValido);
  // O «Guardar» cinzento sem dizer porquê, também com um valor negativo (agentes, 20 Out).
  const valorPorque = pedeValor && !valorValido
    ? (String(valor).trim() === '' ? 'Indica os créditos deste mês, em euros.' : 'Os créditos são um valor em euros, de 0 para cima.')
    : null;
  const cancelada = conta.estado === 'cancelado';

  return (
    <BlocoFicha titulo="Plano e créditos de IA">
      <div className="space-y-4 p-5 text-sm">
        <dl className="grid grid-cols-1 gap-5 sm:grid-cols-2">
          <DadoFicha rotulo="Plano" valor={conta.plano || 'sem plano'} />
          <div>
            <dt className="text-xs uppercase tracking-wider text-muted-foreground">Créditos de IA deste mês</dt>
            <dd className={`mt-1 text-sm ${c.bloqueada ? 'text-perigo-texto' : 'text-foreground'}`}>
              {c.dados == null
                ? `${eur(c.gastos || 0)} gastos, sem tecto`
                : `${eur(c.gastos || 0)} de ${eur(c.dados)}${c.bloqueada ? ' — esgotados: o cliente não consegue escrever' : ''}`}
            </dd>
          </div>
        </dl>
        {/* Roll-up da agência (spec 21 Set, L14): os anunciantes gastam, a agência é que é facturada (A6, 4 Nov). */}
        {Array.isArray(conta.anunciantes_creditos) && conta.anunciantes_creditos.length > 0 && (
          <div className="border-t border-border pt-4">
            <p className="text-xs uppercase tracking-wider text-muted-foreground">Créditos de IA dos anunciantes, este mês</p>
            <ul className="mt-2 space-y-1">
              {conta.anunciantes_creditos.map((a) => (
                <li key={a.slug} className="flex justify-between gap-3"><span>{a.nome}</span><span className="tabular-nums">{eur(a.gastos)}</span></li>
              ))}
              <li className="flex justify-between gap-3 border-t border-border pt-1 font-medium">
                <span>Total facturado à agência</span>
                <span className="tabular-nums">{eur((c.gastos || 0) + conta.anunciantes_creditos.reduce((t, a) => t + (a.gastos || 0), 0))}</span>
              </li>
            </ul>
          </div>
        )}
        {conta.agencia && (
          <p className="text-xs text-muted-foreground">O que este anunciante gasta é facturado à agência {conta.agencia.nome}.</p>
        )}
        {feito && <p className="text-xs text-muted-foreground" role="status">{feito}</p>}
        {!ehAdmin && !cancelada && (
          <p className="text-xs text-muted-foreground">Mudar o plano ou alargar os créditos é com um Admin da NetScreen.</p>
        )}
        {ehAdmin && !cancelada && !aberto && (
          <div className="flex flex-wrap gap-2">
            <button type="button" onClick={() => abrir('creditos')} className={`ns-btn ns-btn-sm ${c.bloqueada ? 'ns-btn-primario' : 'ns-btn-secundario'}`}>
              {c.bloqueada ? 'Alargar os créditos do mês' : 'Acertar os créditos do mês'}
            </button>
            <button type="button" onClick={() => abrir('plano')} className="ns-btn ns-btn-sm ns-btn-secundario">Mudar de plano</button>
          </div>
        )}
        {aberto && (
          <div className="space-y-2">
            {aberto === 'plano' ? (
              <>
                <label htmlFor="ficha-plano" className="block text-xs text-muted-foreground">Plano novo</label>
                {!planos && !erro ? (
                  <p className="text-xs text-muted-foreground" role="status">A ler os planos…</p>
                ) : (
                  <select
                    id="ficha-plano"
                    value={plano}
                    onChange={(e) => setPlano(e.target.value)}
                    className="w-full border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:border-primary"
                  >
                    <option value="" className="bg-background">Escolhe…</option>
                    {(planos || []).filter((p) => p.codigo !== conta.plano_codigo).map((p) => (
                      <option key={p.codigo} value={p.codigo} className="bg-background">
                        {p.nome}{p.mensalidade == null ? ' (sob consulta)' : ` (${eur(p.mensalidade)}/mês)`}
                      </option>
                    ))}
                  </select>
                )}
                {conta.contrato_assinado && (
                  <p className="text-xs text-foreground" role="note">
                    {`Esta conta tem um contrato assinado no plano ${conta.contrato_assinado.plano}, a ${eur(conta.contrato_assinado.mensalidade)}/mês. Mudar o plano aqui não muda o contrato: a mensalidade continua a assinada até haver um aditamento.`}
                  </p>
                )}
                {semTecto ? (
                  <>
                    <label htmlFor="ficha-plano-creditos" className="block text-xs text-muted-foreground">
                      {`O ${destino.nome} é sob consulta e não traz créditos de IA. Créditos deste mês, em euros (o que foi combinado)`}
                    </label>
                    <input
                      id="ficha-plano-creditos"
                      inputMode="decimal"
                      value={valor}
                      onChange={(e) => setValor(e.target.value)}
                      className="w-40 border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:border-primary"
                    />
                    {/* A mensalidade de um sob consulta vive no contrato assinado: a ficha não a guarda. */}
                    <p className="text-xs text-muted-foreground">
                      A mensalidade combinada fica no contrato: até haver um assinado neste plano, a ficha diz «Sob consulta». No mês seguinte o plano não traz créditos: acertam-se outra vez aqui.
                    </p>
                  </>
                ) : (
                  <p className="text-xs text-muted-foreground">Os limites novos valem logo. Os créditos deste mês sobem se o plano novo der mais, e não descem até ao mês seguinte; para os baixar já, acerta-os à parte.</p>
                )}
              </>
            ) : (
              <>
                <label htmlFor="ficha-creditos" className="block text-xs text-muted-foreground">Créditos deste mês, em euros (o total, não o que se acrescenta)</label>
                <input
                  id="ficha-creditos"
                  inputMode="decimal"
                  value={valor}
                  onChange={(e) => setValor(e.target.value)}
                  className="w-40 border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:border-primary"
                />
                <p className="text-xs text-muted-foreground">{`Já se gastou ${eur(c.gastos || 0)}. No mês seguinte volta aos créditos do plano.`}</p>
              </>
            )}
            <label htmlFor="ficha-plano-motivo" className="block text-xs text-muted-foreground">Porquê (fica no registo)</label>
            <input
              id="ficha-plano-motivo"
              value={motivo}
              onChange={(e) => setMotivo(e.target.value)}
              placeholder={aberto === 'plano' ? 'Pedido do cliente por email a…' : 'Campanha de Natal, combinado com…'}
              className="w-full border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:border-primary"
            />
            {/* O «Guardar» cinzento sem dizer porquê (percurso de raiz, 9 Out). */}
            {valorPorque && <p className="text-xs text-muted-foreground">{valorPorque}</p>}
            {!motivo.trim() && <p className="text-xs text-muted-foreground">Escreve porquê para poderes guardar.</p>}
            {erro && <p className="text-xs text-perigo-texto" role="alert">{erro}</p>}
            <div className="flex gap-2">
              <button type="button" disabled={!podeGuardar} onClick={guardar} className="ns-btn ns-btn-sm ns-btn-primario">
                {ocupado ? 'A guardar…' : 'Guardar'}
              </button>
              <button type="button" onClick={() => setAberto(null)} className="ns-btn ns-btn-sm ns-btn-secundario">Cancelar</button>
            </div>
          </div>
        )}
      </div>
    </BlocoFicha>
  );
}

/** Frases completas por evento — "acção + entidade" soletradas dá português torto. */
const FRASE_AUDITORIA = {
  'create:account': 'Criou a conta',
  'allocate:hardware': 'Atribuiu um painel',
  'impersonate:account': 'Entrou como cliente',
  'auth.login:sessao': 'Entrou',
};
/**
 * Pela acção só, quando a entidade não muda o sentido. Cobre o que o código
 * regista hoje — ver `grep -r "action: '" api lib`. Uma acção nova sem frase
 * continua a aparecer, em bruto: esconder um evento era pior do que o
 * mostrar feio.
 */
const FRASE_ACCAO = {
  create: 'Criou',
  allocate: 'Atribuiu um painel',
  impersonate: 'Entrou como cliente',
  'auth.login': 'Entrou',
  'accept:invite': 'Aceitou o convite',
  'create:invite': 'Convidou uma pessoa',
  'resend:invite': 'Reenviou um convite',
  'suspend:account': 'Suspendeu a conta',
  'reactivate:account': 'Reactivou a conta',
  'cancel:account': 'Cancelou a conta',
  'request:reset': 'Pediu para repor a password',
  'reset:password': 'Repôs a password',
  'reset:mfa': 'Repôs a MFA',
  'onboarding.manual': 'Marcou um passo do onboarding como tratado fora da plataforma',
  'promote:netscreen_user': 'Passou a Admin',
  'demote:netscreen_user': 'Passou a Operador',
  'disable:netscreen_user': 'Tirou o acesso à equipa',
  'enable:netscreen_user': 'Devolveu o acesso à equipa',
  'update:user_role': 'Mudou o perfil de uma pessoa',
  'remove:user': 'Retirou uma pessoa da conta',
  'update:plan': 'Alterou um plano',
  'account.spots': 'Mudou os spots negociados',
  'account.creditos': 'Acertou os créditos de IA do mês',
  'account.plano': 'Mudou o plano da conta',
  'account.dados': 'Alterou a razão social, o NIF ou a morada',
  'hardware.medida': 'Registou a medida de um painel',
  'loja.create': 'Criou uma loja',
  'loja.update': 'Alterou uma loja',
  'loja.delete': 'Apagou uma loja',
  'loja.painel': 'Pôs um painel numa loja',
  'material.upload': 'Carregou um ficheiro',
  'material.delete': 'Apagou um ficheiro',
  'kb_source.create': 'Acrescentou uma fonte de conhecimento',
  'kb_source.reread': 'Mandou ler outra vez uma fonte',
  'kb_source.resume': 'Mandou continuar uma leitura a meio',
  'kb_source.approve': 'Aprovou uma fonte',
  'kb_source.delete': 'Removeu uma fonte',
  'brand_fact.decide': 'Decidiu um facto',
  'brand_fact.sinalizar': 'Sinalizou um facto como errado',
  'brand_dossier.generate': 'Gerou o dossiê de marca',
  'brand_dossier.curate': 'Editou o dossiê de marca',
  'campanha.create': 'Criou uma campanha',
  'campanha.estado': 'Mudou o estado de uma campanha',
  'campanha.lojas': 'Escolheu as lojas de uma campanha',
  'campanha.materiais': 'Escolheu os materiais de uma campanha',
  'campanha.gerar': 'Pediu criativos ao Agente PM',
  'campanha.delete': 'Apagou uma campanha',
  'criativo.aprovar': 'Aprovou um criativo',
  'criativo.desaprovar': 'Retirou a aprovação de um criativo',
  // As que faltavam e apareciam em bruto, «brand_fact.retirar · brand_fact» (ensaio da
  // demo de 12 Out). Uma acção nova sem frase cai no genérico de baixo, não no código.
  'campanha.aprovar': 'Aprovou e publicou uma campanha',
  'campanha.storyboard': 'Pediu ao Agente PM as cenas do vídeo',
  'campanha.repetir': 'Repetiu uma campanha',
  'campanha.cartazes_revistos': 'Pediu para rever os criativos dos painéis',
  'storyboard.aprovar': 'Aprovou as cenas do vídeo',
  'criativo.editar': 'Corrigiu o texto de um criativo',
  'video.tentar_outra_vez': 'Mandou compor o vídeo outra vez',
  'excecao.pedir': 'Pediu uma excepção a uma regra',
  'excecao.aceitar': 'Aceitou um pedido de excepção',
  'excecao.recusar': 'Recusou um pedido de excepção',
  'entrega.marcar': 'Actualizou uma entrega',
  'material.ler': 'Mandou ler um ficheiro',
  'material.papel': 'Mudou o que é um ficheiro',
  'material.ambito': 'Mudou onde um ficheiro se usa',
  'brand_fact.confirmar': 'Confirmou um dado da marca',
  'brand_fact.retirar': 'Retirou um dado da marca',
  'brand_fact.repor': 'Repôs um dado da marca',
  'brand_fact.prova': 'Juntou uma prova a um dado da marca',
  'brand_fact.desconfirmar': 'Voltou atrás num dado da marca',
  'brand_kit.confirmar': 'Confirmou as cores, a letra e o logótipo',
  'brand_voz.propor': 'Pediu uma proposta de como a marca fala',
  'brand_voz.aceitar': 'Confirmou como a marca fala',
  'brand_regra.criar': 'Acrescentou uma regra da marca',
  'brand_regra.retirar': 'Retirou uma regra da marca',
  // As que apareciam em código nos filtros da Auditoria, «criacao.interpretar», «hardware.registar»,
  // e o registo de um painel como «Fez uma alteração» (agentes, 20 Out).
  'hardware.registar': 'Registou um painel no stock',
  'hardware.devolver': 'Devolveu um painel ao stock',
  'hardware.mudar_loja': 'Mudou um painel de loja',
  'hardware.manutencao': 'Pôs ou tirou um painel de manutenção',
  'hardware.virado': 'Disse para onde está virado um painel',
  'fila.responsavel': 'Escolheu quem trata uma linha do «A tratar»',
  'vnnox.publicar': 'Enviou um vídeo para um painel',
  'vnnox.publicar.resultado': 'Registou o resultado de um envio para um painel',
  'update:redes_sociais': 'Mudou as redes sociais da conta',
  'kb_pages.analisar_outra_vez': 'Mandou analisar outra vez as páginas lidas',
  'kb_source.inspeccionar_ambito': 'Verificou se um site é da marca',
  'kb_source.ambito_recusado': 'Um site não foi lido por não ser da marca',
  'brand_site.observar': 'Leu as cores e o logótipo do site',
  'brand_site.observar_cache': 'Leu as cores e o logótipo do site',
  'brand_site.ambito_recusado': 'Um site não foi usado por não ser da marca',
  'brand_site.importar_logo': 'Trouxe um logótipo do site',
  'brand_material.logotipo_do_manual': 'Tirou um logótipo do manual da marca',
  'brand_kit.manter_cores': 'Manteve as cores da marca',
  'campanha.storyboard_actualizado': 'Actualizou as cenas do vídeo',
  'criacao.fonte': 'Leu uma referência para um vídeo',
  'criacao.material.ler': 'Leu um ficheiro para um vídeo',
  'criacao.interpretar': 'Pediu a leitura do pedido de vídeo',
  'criacao.orientacao': 'Recebeu a proposta para um vídeo',
  'criacao.confirmar': 'Confirmou a proposta de um vídeo',
  'criacao.aprovar-materiais': 'Aprovou os textos e as imagens de um vídeo',
  'criacao.ajuste.preparar': 'Pediu um ajuste a um vídeo',
  'criacao.video.pedir': 'Pediu um vídeo',
  'criacao.video.repetir': 'Pediu o vídeo outra vez',
  'criacao.video.retirar': 'Retirou um vídeo da fila',
  'criacao.video.resultado': 'Um vídeo ficou pronto ou falhou',
  'criacao.video.aprovar': 'Aprovou um vídeo',
  'criacao.campanha.rascunho': 'Criou uma campanha a partir de um vídeo',
};

const CANAL_EVENTO = { social: 'das redes sociais', dooh: 'dos painéis', video: 'do vídeo', pdv: 'do assistente na loja' };
const ESTADO_CAMPANHA_EVENTO = {
  draft: 'voltou a rascunho', em_revisao: 'voltou a revisão', aprovada: 'foi aprovada', ativa: 'ficou no ar', encerrada: 'foi encerrada',
};
const ESTADO_ENTREGA_EVENTO = { por_enviar: 'por enviar', enviado: 'enviada', confirmado: 'confirmada', falhou: 'falhou' };

/** «A broa da nossa padaria», sem o ponto final; nada se não houver texto. */
const aspasEvento = (t) => (typeof t === 'string' && t.trim() ? ` «${t.trim().replace(/[.!]$/, '')}»` : '');

/**
 * Uma frase por evento, com o nome da coisa quando o registo o tem (demo de 12 Out: a
 * coluna «Como ficou» mostrava o JSON cru, com ids e hashes). O que não tem frase
 * própria usa a da acção, e o que nem essa tem diz só que houve uma alteração — o
 * código interno fica no pormenor recolhido, nunca na frase.
 */
function descreverEvento(e) {
  const a = e.after || {};
  const b = e.before || {};
  const titulo = a.titulo || (typeof a.campanha === 'string' && !/^[0-9a-f-]{36}$/i.test(a.campanha) ? a.campanha : null);
  const daCampanha = titulo ? ` da campanha${aspasEvento(titulo)}` : '';
  const afirmacao = aspasEvento(a.afirmacao || b.afirmacao);
  const facto = afirmacao ? `:${afirmacao}` : '';
  const canal = CANAL_EVENTO[a.canal] ? ` ${CANAL_EVENTO[a.canal]}` : '';
  switch (e.action) {
    case 'campanha.create': return `Criou a campanha${aspasEvento(a.titulo)}`;
    case 'campanha.aprovar': return `Aprovou e publicou a campanha${aspasEvento(a.titulo)}`;
    case 'campanha.repetir': return `Repetiu a campanha${aspasEvento(a.titulo)}`;
    case 'campanha.estado':
      return `A campanha${aspasEvento(a.titulo)} ${ESTADO_CAMPANHA_EVENTO[a.estado] || 'mudou de estado'}`;
    case 'campanha.storyboard': return `Pediu ao Agente PM as cenas do vídeo${daCampanha}`;
    case 'storyboard.aprovar': return `Aprovou as cenas do vídeo${daCampanha}`;
    case 'criativo.aprovar': return `Aprovou o criativo${canal}${daCampanha}`;
    case 'criativo.desaprovar': return `Retirou a aprovação do criativo${canal}${daCampanha}`;
    case 'criativo.editar': return `Corrigiu o texto do criativo${canal}${daCampanha}`;
    case 'entrega.marcar':
      return `Entrega ${a.destino ? `para ${a.destino} ` : ''}marcada como ${ESTADO_ENTREGA_EVENTO[a.estado] || 'actualizada'}`;
    case 'brand_fact.confirmar':
      return a.estado === 'proibido' ? `Marcou como coisa a nunca dizer${facto}` : `Confirmou um dado da marca${facto}`;
    case 'brand_fact.retirar': return `Retirou um dado da marca${facto}`;
    case 'brand_fact.sinalizar': return `Disse que um dado da marca está errado${facto}`;
    case 'brand_fact.repor': return `Repôs um dado da marca${facto}`;
    case 'brand_fact.prova': return `Juntou uma prova a um dado da marca${facto}`;
    case 'brand_fact.desconfirmar': return `Voltou atrás num dado da marca${facto}`;
    case 'brand_fact.decide': return `Decidiu um dado da marca${facto}`;
    case 'material.upload': return `Carregou o ficheiro${aspasEvento(a.nome)}`;
    case 'material.delete': return `Apagou o ficheiro${aspasEvento(b.nome)}`;
    case 'loja.create': return `Criou a loja${aspasEvento(a.name)}`;
    case 'loja.painel': return 'Pôs um painel numa loja';
    case 'hardware.registar': return a.serial ? `Registou o painel ${a.serial} no stock` : 'Registou um painel no stock';
    case 'onboarding.manual':
      return a.billing && !a.contract
        ? 'Marcou a facturação como tratada fora da plataforma'
        : `Marcou ${a.contract ? 'o contrato' : 'um passo do onboarding'} como tratado fora da plataforma`;
    default:
      return FRASE_AUDITORIA[`${e.action}:${e.entity}`] || FRASE_ACCAO[e.action] || 'Fez uma alteração na conta';
  }
}

/** O porquê, quando o registo o tem: o motivo, a nota, o texto do cliente. */
function notaEvento(e) {
  const a = e.after || {};
  const t = a.motivo || a.nota || a.texto;
  return typeof t === 'string' && t.trim() ? t.trim() : null;
}

/** «cliente» ou «NetScreen» por baixo do email: a lista mistura as duas populações. */
const QUEM_EVENTO = { user: 'cliente', netscreen_user: 'NetScreen', system: 'sistema', api: 'sistema' };

/** A coluna «Quem», partilhada com a auditoria geral. */
function quemEvento(e) {
  if (!e.actor_email) return <span className="text-muted-foreground">sistema</span>;
  return (
    <span>
      <span className="break-all">{e.actor_email}</span>
      {QUEM_EVENTO[e.actor_type] && (
        <span className="block text-xs text-muted-foreground">{QUEM_EVENTO[e.actor_type]}</span>
      )}
    </span>
  );
}

/** Um valor do registo em texto: listas por vírgulas, o vazio como traço, o resto curto. */
function valorDoRegisto(v) {
  if (v === null || v === undefined || v === '') return '—';
  if (typeof v === 'boolean') return v ? 'sim' : 'não';
  if (Array.isArray(v)) return v.length ? v.map(valorDoRegisto).join(', ') : '—';
  if (typeof v === 'object') return JSON.stringify(v);
  return String(v);
}

/**
 * O pormenor de um registo, campo a campo, «antes → depois» (agentes, 20 Out: era o JSON cru). Os
 * nomes dos campos ficam os do registo, com espaços: são o que se procura num incidente.
 */
function PormenorEvento({ antes, depois }) {
  const a = antes || {};
  const d = depois || {};
  const chaves = [...new Set([...Object.keys(a), ...Object.keys(d)])];
  return (
    <dl className="mt-1 max-w-[40rem] space-y-0.5 text-[11px] text-muted-foreground">
      {chaves.map((k) => (
        <div key={k} className="flex flex-wrap gap-x-2">
          <dt className="text-foreground">{k.replace(/_/g, ' ')}</dt>
          <dd className="min-w-0 break-words">
            {k in a && k in d ? `${valorDoRegisto(a[k])} → ${valorDoRegisto(d[k])}` : valorDoRegisto(k in d ? d[k] : a[k])}
          </dd>
        </div>
      ))}
    </dl>
  );
}

/** A coluna «O quê»: a frase, o porquê por baixo, e o registo recolhido. */
function oQueEvento(e) {
  const nota = notaEvento(e);
  return (
    <span className="block">
      <span>{descreverEvento(e)}</span>
      {nota && <span className="mt-0.5 block text-xs text-muted-foreground">{nota}</span>}
      {(e.after || e.before) && (
        <details className="mt-1 text-xs" onClick={(ev) => ev.stopPropagation()}>
          <summary className="cursor-pointer text-muted-foreground hover:text-foreground">Ver pormenor</summary>
          <PormenorEvento antes={e.before} depois={e.after} />
        </details>
      )}
    </span>
  );
}

const formatarMomento = (iso) =>
  new Intl.DateTimeFormat('pt-PT', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(iso));

/** Um par rótulo/valor da secção de identificação. Sem valor, é um traço. */
function DadoFicha({ rotulo, valor }) {
  return (
    <div>
      <dt className="text-xs uppercase tracking-wider text-muted-foreground">{rotulo}</dt>
      <dd className="mt-1 text-sm text-foreground">
        {valor || <span className="text-muted-foreground">—</span>}
      </dd>
    </div>
  );
}

/**
 * A razão social, o NIF e a morada, editáveis (agentes, 20 Out): apareciam a «—» sem por onde os
 * pôr, porque o «Criar cliente» não os pede. A rota regista o antes e o depois na auditoria.
 */
function EditarDados({ conta, slug, onFeito }) {
  const [aberto, setAberto] = React.useState(false);
  const [v, setV] = React.useState({});
  const [ocupado, setOcupado] = React.useState(false);
  const [erro, setErro] = React.useState(null);
  const abrir = () => {
    setV({ legal_name: conta.legal_name || '', nif: conta.nif || '', morada: conta.morada || '' });
    setErro(null);
    setAberto(true);
  };
  const guardar = async () => {
    setOcupado(true);
    setErro(null);
    try {
      await api.patch(`/api/admin/accounts/${slug}/dados`, v);
      setAberto(false);
      onFeito();
    } catch (e) {
      setErro(e.message || 'Não foi possível guardar os dados.');
    } finally {
      setOcupado(false);
    }
  };
  if (!aberto) {
    return (
      <div className="border-t border-border px-5 py-3">
        <button type="button" onClick={abrir} className="ns-btn ns-btn-sm ns-btn-secundario text-xs">Editar dados</button>
      </div>
    );
  }
  const campo = (chave, rotulo) => (
    <label className="block">
      <span className="ns-rotulo">{rotulo}</span>
      <input value={v[chave]} onChange={(e) => setV((x) => ({ ...x, [chave]: e.target.value }))} className="mt-1 w-full border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:border-primary" />
    </label>
  );
  return (
    <div className="space-y-3 border-t border-border px-5 py-4 text-sm">
      {campo('legal_name', 'Razão social')}
      {campo('nif', 'NIF')}
      {campo('morada', 'Morada')}
      {!v.legal_name.trim() && <p className="text-xs text-muted-foreground">A razão social não pode ficar vazia.</p>}
      {erro && <p role="alert" className="text-xs text-perigo-texto">{erro}</p>}
      <div className="flex gap-2">
        <button type="button" disabled={ocupado || !v.legal_name.trim()} onClick={guardar} className="ns-btn ns-btn-sm ns-btn-primario text-xs">
          {ocupado ? 'A guardar…' : 'Guardar'}
        </button>
        <button type="button" onClick={() => setAberto(false)} className="ns-btn ns-btn-sm ns-btn-secundario text-xs">Cancelar</button>
      </div>
    </div>
  );
}

/** Um bloco de uma ficha: título, e o conteúdo num cartão. */
/**
 * Em que pé está cada pessoa da conta.
 *
 * Três estados que se confundem se ficarem todos como "nunca entrou": quem
 * tem password e ainda não entrou, quem tem um convite à espera, e quem tem um
 * convite que já caducou. Só o último é que pede acção de alguém.
 */
function estadoConvite(u) {
  if (u.activated_at) {
    return { texto: u.last_login_at ? `entrou ${desde(u.last_login_at).texto}` : 'nunca entrou' };
  }
  if (!u.invite_expires_at) return { texto: 'sem convite', alerta: true };

  const dias = diasAte(u.invite_expires_at);
  if (dias < 0) return { texto: 'convite caducado', alerta: true };
  if (dias === 0) return { texto: 'convite caduca hoje', alerta: true };
  return { texto: `convite caduca em ${dias} ${dias === 1 ? 'dia' : 'dias'}` };
}

function BlocoFicha({ titulo, nota, children }) {
  return (
    <section>
      <h2 className="label-eyebrow">{titulo}</h2>
      {nota && <p className="mt-1 text-xs text-muted-foreground">{nota}</p>}
      <div className="mt-3 border border-border bg-card">{children}</div>
    </section>
  );
}

/**
 * A identidade da conta, no mesmo ecrã que o cliente vê (screen_marca.jsx e
 * marca_voz.jsx). Até 24 Set era uma cópia à mão, e já estava atrasada: não
 * tinha a cor do texto nem a marca numa frase (advogado da simplicidade). O
 * que a equipa guarda aqui fica com o nome dela, como "NetScreen".
 */
function AdminIdentidade({ slug, onNavegar }) {
  return (
    <div className="space-y-10">
      <p className="max-w-prose text-xs text-muted-foreground">
        É o que o cliente vê em A minha marca → Identidade. O que mudares aqui fica registado com o teu nome.
      </p>
      <section>
        <h2 className="mb-3 font-display text-2xl tracking-wide">Como aparece</h2>
        <MarcaIdentidade slug={slug} podeMexer onNavegar={onNavegar} daEquipa />
      </section>
      <section className="border-t border-border pt-8">
        <h2 className="mb-3 font-display text-2xl tracking-wide">Quem é e como fala</h2>
        <MarcaVoz slug={slug} podeMexer daEquipa />
      </section>
    </div>
  );
}

function ScreenAdminMarca({ slug, me, onNavegar, onEntrarComo }) {
  // Só um Admin marca o que foi tratado fora da plataforma (a rota recusa os outros com 403): o
  // Operator não vê o botão, vê quem trata disto (parecer do Codex, 9 Out).
  const ehAdmin = !me || !me.user || me.user.role === 'admin';
  const [conta, setConta] = React.useState(null);
  const [erro, setErro] = React.useState(null);
  const [eventos, setEventos] = React.useState(null);
  const [aConvidar, setAConvidar] = React.useState(null);
  const [mudancaEstado, setMudancaEstado] = React.useState(null);
  const [aReporPassword, setAReporPassword] = React.useState(null);

  /**
   * O assistente de criação acabou de perguntar quem é o responsável e mandou-o
   * para aqui, para o convite se gerar com os campos já preenchidos.
   *
   * Lê-se uma vez e apaga-se: recarregar a página não deve reabrir o modal, e
   * o rascunho tem um email lá dentro — não fica na `sessionStorage` mais tempo
   * do que precisa.
   */
  React.useEffect(() => {
    let rascunho = null;
    try {
      const guardado = sessionStorage.getItem('conviteRascunho');
      if (guardado) {
        rascunho = JSON.parse(guardado);
        sessionStorage.removeItem('conviteRascunho');
      }
    } catch { /* sem storage: convida-se à mão */ }
    if (rascunho && rascunho.slug === slug) {
      setAConvidar({ pessoa: null, sugestao: { nome: rascunho.nome, email: rascunho.email } });
    }
  }, [slug]);
  const [toast, setToast] = React.useState(null);
  const fecharToast = React.useCallback(() => setToast(null), []);

  const inicial = () => {
    try {
      const s = new URLSearchParams(window.location.search).get('separador');
      return SEPARADORES_FICHA.some((x) => x.chave === s) ? s : 'trabalho';
    } catch {
      return 'trabalho';
    }
  };
  const [separador, setSeparador] = React.useState(inicial);

  // O separador vive no URL, para uma ficha poder ser enviada já aberta no sítio certo.
  // `replaceState` e não uma navegação: mudar de separador não é mudar de página.
  function mudarSeparador(chave) {
    setSeparador(chave);
    try {
      window.history.replaceState({}, '', `${window.location.pathname}?separador=${chave}`);
    } catch { /* sem history: o separador muda na mesma, só não fica no URL */ }
  }

  // Em `useCallback` para o modal de convite poder pedir a ficha outra vez
  // depois de convidar — o estado do convite mudou na lista.
  const carregar = React.useCallback(() => {
    setErro(null);
    return api
      .get(`/api/admin/accounts/${slug}`)
      .then(setConta)
      .catch((e) => setErro(e.status === 404 ? 'nao-existe' : e.message || 'Não foi possível ler esta conta.'));
  }, [slug]);

  React.useEffect(() => {
    setConta(null);
    setEventos(null);
    carregar();
  }, [slug, carregar]);
  // O que o cliente faz do lado dele aparece aqui sem recarregar (sessão de
  // 24 Set). Pára com um diálogo aberto.
  useActualizar(carregar, { activo: !aConvidar && !mudancaEstado && !aReporPassword });

  // A auditoria só se pede quando se abre o separador.
  React.useEffect(() => {
    if (separador !== 'actividade' || eventos) return;
    api
      .get(`/api/admin/accounts/${slug}/audit`)
      .then((r) => setEventos(r.eventos))
      .catch(() => setEventos([]));
  }, [separador, slug, eventos]);

  const migalhas = (nome) => (
    <nav aria-label="Localização" className="flex items-center gap-2 text-xs text-muted-foreground">
      <a
        href="/admin/clientes"
        onClick={(e) => {
          e.preventDefault();
          onNavegar('/admin/clientes');
        }}
        className="uppercase tracking-wider outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-accent"
      >
        Clientes
      </a>
      <span aria-hidden="true">/</span>
      <span className="uppercase tracking-wider text-foreground" aria-current="page">{nome}</span>
    </nav>
  );

  if (erro === 'nao-existe') {
    return (
      <div className="space-y-6">
        <header>{migalhas('Não encontrado')}</header>
        <div className="border border-border bg-card px-6 py-12 text-center">
          <p className="font-display text-xl tracking-wide">Esta conta não existe</p>
          <p className="mt-2 text-sm text-muted-foreground">
            Não há nenhum cliente com o identificador &ldquo;{slug}&rdquo;.
          </p>
        </div>
      </div>
    );
  }

  if (erro) {
    return (
      <div className="space-y-6">
        <header>{migalhas('Erro')}</header>
        <div className="border border-border bg-card px-6 py-12 text-center">
          <p className="font-display text-xl tracking-wide">Não deu para ler a conta</p>
          <p className="mt-2 text-sm text-muted-foreground">{erro}</p>
        </div>
      </div>
    );
  }

  if (!conta) {
    return (
      <div className="space-y-6">
        <Skeleton className="h-4 w-40" />
        <Skeleton className="h-10 w-64" />
        <SkeletonTabela linhas={4} />
      </div>
    );
  }

  const estado = ROTULO_ESTADO_CONTA[conta.estado] || { texto: conta.estado, variante: 'neutro' };
  const ehAgencia = conta.tipo === 'agencia';
  const tipoTexto = conta.agencia ? 'Anunciante' : ROTULO_TIPO[conta.tipo] || conta.tipo;

  const colunasEventos = [
    { chave: 'at', rotulo: 'Quando', valorOrdenacao: (e) => new Date(e.at).getTime(), render: (e) => formatarMomento(e.at) },
    { chave: 'actor_email', rotulo: 'Quem', render: quemEvento },
    // Uma frase por linha; o registo em bruto fica recolhido em «Ver pormenor» (demo de 12 Out).
    { chave: 'action', rotulo: 'O quê', principal: true, render: oQueEvento },
  ];

  return (
    <div className="space-y-6">
      <header className="flex flex-wrap items-end justify-between gap-4">
        <div>
          {migalhas(conta.nome)}
          <h1 className="mt-3 font-display text-4xl uppercase leading-none tracking-wide">{conta.nome}</h1>
          <p className="mt-2 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
            {tipoTexto}
            {conta.agencia && (
              <>
                {' de '}
                <a
                  href={`/admin/clientes/${conta.agencia.slug}`}
                  onClick={(e) => {
                    e.preventDefault();
                    onNavegar(`/admin/clientes/${conta.agencia.slug}`);
                  }}
                  className="text-foreground underline-offset-2 outline-none hover:text-primary hover:underline focus-visible:ring-1 focus-visible:ring-accent"
                >
                  {conta.agencia.nome}
                </a>
              </>
            )}
            <span aria-hidden="true">·</span>
            {conta.plano || 'sem plano'}
            <Badge variante={estado.variante}>{estado.texto}</Badge>
          </p>
          {/* O porquê e o quando de uma suspensão ou cancelamento, da auditoria (agentes, 20 Out). */}
          {conta.mudanca_estado && (
            <p className="mt-1 text-xs text-muted-foreground">
              {`${estado.texto} a ${formatData(conta.mudanca_estado.em)}${conta.mudanca_estado.por ? ` por ${conta.mudanca_estado.por}` : ''}`}
              {conta.mudanca_estado.motivo ? `: ${conta.mudanca_estado.motivo}` : '.'}
            </p>
          )}
        </div>
        <div className="flex shrink-0 flex-wrap gap-2">
          {/* «Ver como cliente» primeiro; suspender e cancelar depois (no telemóvel eram os primeiros, A3). */}
          <button
            type="button"
            onClick={() => onEntrarComo({ slug: conta.slug, nome: conta.nome })}
            className="ns-btn ns-btn-sm ns-btn-secundario"
          >
            Ver como cliente
          </button>
          {/* Cancelar é só do Admin (a rota recusa os outros com 403, D9 do spec): ao Operator não se
              oferece um botão que só dá erro (9 Out, becos sem saída). */}
          {(ACCOES_POR_ESTADO[conta.estado] || []).filter((a) => ehAdmin || a.accao !== 'cancelled').map((a) => (
            <button
              key={a.accao}
              type="button"
              onClick={() => setMudancaEstado(a.accao)}
              // Mudar o estado da conta pesa no cliente: suspender e cancelar
              // ficam com a moldura vermelha, e não com a cara de "Ver como cliente" (28 Set).
              className={`ns-btn ns-btn-sm ${a.accao === 'active' ? 'ns-btn-secundario' : 'ns-btn-perigo'}`}
            >
              {a.rotulo}
            </button>
          ))}
        </div>
      </header>

      <Separadores rotulo="Secções da ficha" separadores={SEPARADORES_FICHA} activo={separador} aoMudar={mudarSeparador} />

      <PainelSeparador chave="trabalho" activo={separador}>
        {separador === 'trabalho' && <AdminTrabalhoCliente slug={slug} conta={conta} onEntrarComo={onEntrarComo} onNavegar={onNavegar} />}
      </PainelSeparador>

      <PainelSeparador chave="conta" activo={separador}>
        <div className="space-y-8">
          <ActivarConta conta={conta} slug={slug} onFeito={carregar} ehAdmin={ehAdmin} />
          <BlocoFicha titulo="Identificação">
            <dl className="grid grid-cols-1 gap-5 p-5 sm:grid-cols-2 lg:grid-cols-3">
              <DadoFicha rotulo="Razão social" valor={conta.legal_name} />
              <DadoFicha rotulo="NIF" valor={conta.nif} />
              <DadoFicha rotulo="Morada" valor={conta.morada} />
              {/* Só o Admin vê a mensalidade, como a receita (agentes, 20 Out): ao Operator a API não a manda. */}
              {conta.receita_visivel !== false && (
                <DadoFicha rotulo="Mensalidade" valor={conta.agencia ? `Facturada à agência ${conta.agencia.nome}` : conta.mensalidade == null ? 'Sob consulta' : euros(conta.mensalidade)} />
              )}
              <DadoFicha rotulo="Criada em" valor={conta.criada_em && formatData(conta.criada_em)} />
              <DadoFicha rotulo="Activada em" valor={conta.activada_em && formatData(conta.activada_em)} />
            </dl>
            {conta.estado !== 'cancelado' && <EditarDados conta={conta} slug={slug} onFeito={carregar} />}
          </BlocoFicha>

          <PlanoECreditos conta={conta} slug={slug} onFeito={carregar} ehAdmin={ehAdmin} />

          {conta.onboarding && (
            <BlocoFicha
              titulo="Onboarding"
              nota={(() => {
                // Um passo marcado sem nada por baixo não conta como feito: dizia «7 de 7 passos» com
                // avisos de contrato, facturação e convite (agentes, 20 Out).
                const marcados = PASSOS_ONBOARDING.filter((p) => conta.onboarding.passos[p.chave]);
                const comAviso = marcados.filter((p) => p.verifica && !p.verifica(conta)).length;
                const feitos = marcados.length - comAviso;
                return `${feitos} de ${PASSOS_ONBOARDING.length} passos feitos`
                  + `${comAviso ? `, ${comAviso} ${comAviso === 1 ? 'marcado sem confirmação' : 'marcados sem confirmação'}` : ''}. `
                  + 'A conta activa-se sozinha quando dados, contrato, facturação e convite estão feitos.';
              })()}
            >
              <div className="px-5">
                {PASSOS_ONBOARDING.map((p) => {
                  const feito = conta.onboarding.passos[p.chave];
                  // Marcado como feito, mas a base não confirma: diz-se, em vez de repetir a coluna.
                  const semSuporte = feito && !!p.verifica && !p.verifica(conta);
                  const manual = conta.onboarding.manual?.[p.chave];
                  // Um passo que ainda não chegou fica neutro, não a vermelho: não é uma falha.
                  const pendente = !feito && p.porFazer ? p.porFazer(conta) : null;
                  const pendenteTexto = typeof pendente === 'string' ? pendente : pendente?.texto;
                  return (
                    <LinhaChecklist
                      key={p.chave}
                      feito={feito && !semSuporte}
                      aviso={semSuporte || !!pendente?.aviso}
                      titulo={p.titulo}
                      extra={ehAdmin && !feito && ['por_activar', 'activo'].includes(conta.estado) && ['contract', 'billing'].includes(p.chave)
                        ? <MarcarForaDaPlataforma slug={slug} passo={p.chave} onFeito={carregar} />
                        // A ficha dizia «sem painéis» sem por onde os atribuir (agentes, 20 Out): leva aos
                        // painéis por atribuir, com este cliente já escolhido.
                        : p.chave === 'hardware' && !feito && conta.estado !== 'cancelado'
                        ? (
                          <button type="button" onClick={() => onNavegar(`/admin/hardware?para=${encodeURIComponent(slug)}`)} className="mt-1 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline">
                            Atribuir painéis
                          </button>
                        )
                        : null}
                      detalhe={
                        feito
                          ? semSuporte
                            ? `Registado como feito, mas ${p.semBase}`
                            : manual
                            ? fraseManual(manual)
                            : p.feitoComo
                            ? p.feitoComo(conta)
                            : 'Feito'
                          : pendenteTexto || 'Por fazer'
                      }
                    />
                  );
                })}
              </div>
            </BlocoFicha>
          )}

          {ehAgencia && (
            <BlocoFicha
              titulo="Anunciantes"
              nota="Cada anunciante é uma conta própria, com a sua ficha, o seu brand kit e a sua base de conhecimento."
            >
              {conta.marcas.length === 0 ? (
                <p className="px-5 py-6 text-sm text-muted-foreground">Esta agência ainda não tem anunciantes.</p>
              ) : (
                <ul>
                  {conta.marcas.map((m) => (
                    <li key={m.slug} className="border-t border-border first:border-t-0">
                      <a
                        href={`/admin/clientes/${m.slug}`}
                        onClick={(e) => {
                          e.preventDefault();
                          onNavegar(`/admin/clientes/${m.slug}`);
                        }}
                        className="flex items-center justify-between gap-3 px-5 py-3 text-sm text-foreground outline-none hover:bg-secondary focus-visible:ring-1 focus-visible:ring-accent"
                      >
                        {m.nome}
                        <span className="text-xs text-muted-foreground">abrir ficha →</span>
                      </a>
                    </li>
                  ))}
                </ul>
              )}
              {ehAdmin && <NovoAnunciante conta={conta} slug={slug} onCriado={(s) => onNavegar(`/admin/clientes/${s}`)} />}
            </BlocoFicha>
          )}

          <div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
            <BlocoFicha titulo="Utilizadores">
              {conta.utilizadores.length === 0 ? (
                <p className="px-5 py-6 text-sm text-muted-foreground">
                  {conta.agencia
                    ? 'Ninguém próprio: entram as pessoas da agência. Só é preciso convidar alguém que não seja da agência.'
                    : 'Ainda não há ninguém nesta conta.'}
                </p>
              ) : (
                <ul>
                  {conta.utilizadores.map((u) => (
                    <li key={u.id} className="flex flex-wrap items-baseline justify-between gap-3 border-t border-border px-5 py-3 first:border-t-0">
                      <div className="min-w-0">
                        <p className="text-sm text-foreground">{u.nome}</p>
                        <p className="mt-0.5 truncate text-xs text-muted-foreground">{u.email}</p>
                      </div>
                      <div className="flex items-baseline gap-3 text-right text-xs text-muted-foreground">
                        <div>
                          <p>{ROTULO_PERFIL[u.role] || u.role}</p>
                          <p className={estadoConvite(u).alerta ? 'text-primary' : undefined}>
                            {estadoConvite(u).texto}
                          </p>
                        </div>
                        <button
                          type="button"
                          onClick={() =>
                            u.activated_at ? setAReporPassword(u) : setAConvidar({ pessoa: u })
                          }
                          className="shrink-0 border border-border px-2 py-1 text-xs text-muted-foreground transition-colors hover:border-primary hover:text-foreground"
                        >
                          {u.activated_at ? 'Repor password' : 'Reenviar'}
                        </button>
                      </div>
                    </li>
                  ))}
                </ul>
              )}
              <div className="border-t border-border px-5 py-3">
                <button
                  type="button"
                  onClick={() => setAConvidar({ pessoa: null })}
                  className="text-xs uppercase tracking-wider text-primary hover:underline"
                >
                  Convidar alguém
                </button>
              </div>
            </BlocoFicha>

            <BlocoFicha titulo="Lojas">
              {conta.lojas.length === 0 ? (
                <div className="space-y-2 px-5 py-6 text-sm text-muted-foreground">
                  <p>Esta conta ainda não tem lojas. Cria-as o cliente em Lojas e painéis, ou a equipa por ele.</p>
                  {/* Sem botão, o administrador virgem não sabia onde criar a loja (4 Nov). */}
                  <button type="button" onClick={() => onEntrarComo({ slug: conta.slug, nome: conta.nome, destino: `/${conta.slug}/paineis-pdv` })}
                    className="ns-btn ns-btn-sm ns-btn-suave">Criar a loja como o cliente</button>
                </div>
              ) : (
                <ul>
                  {conta.lojas.map((l) => (
                    <li key={l.id} className="flex flex-wrap items-baseline justify-between gap-3 border-t border-border px-5 py-3 first:border-t-0">
                      <div className="min-w-0">
                        <p className="text-sm text-foreground">{l.nome}</p>
                        <p className="mt-0.5 truncate text-xs text-muted-foreground">{l.morada || 'sem morada'}</p>
                      </div>
                      <p className="text-xs text-muted-foreground">
                        {l.n_paineis} {l.n_paineis === 1 ? 'painel' : 'painéis'}
                      </p>
                    </li>
                  ))}
                </ul>
              )}
            </BlocoFicha>
          </div>
        </div>
      </PainelSeparador>

      <PainelSeparador chave="actividade" activo={separador}>
        <p className="mb-4 text-sm text-muted-foreground">
          O que o cliente e a equipa NetScreen fizeram nesta conta, do mais recente para o mais antigo.
        </p>
        <Tabela
          colunas={colunasEventos}
          linhas={eventos || []}
          carregando={!eventos}
          chaveLinha={(e) => e.id}
          ordemInicial={{ chave: 'at', sentido: 'desc' }}
          contagem={false}
          vazio={{
            titulo: 'Ainda sem actividade',
            descricao: 'Ainda não há nada registado para esta conta.',
          }}
        />
      </PainelSeparador>

      <PainelSeparador chave="brandkit" activo={separador}>
        {/* O kit e a voz guardados, os mesmos que o cliente vê (cliente
            simulado, 24 Set: aqui dizia "por ligar" com o kit confirmado). */}
        {separador === 'brandkit' && <AdminIdentidade slug={slug} onNavegar={onNavegar} />}
      </PainelSeparador>

      <PainelSeparador chave="conhecimento" activo={separador}>
        {/* Deixou de ser uma promessa a 22 de Setembro: a base de conhecimento,
            a biblioteca e o dossiê existem e têm ecrã. O que fica aqui é a
            porta para o trabalho que é **nosso** — a curadoria. */}
        <Painel className="mb-6 grid grid-cols-1 gap-px bg-[var(--border)] sm:grid-cols-3">
          {[
            ['Fontes lidas', `${conta.fontes.lidas} de ${conta.fontes.total}`],
            ['Fontes com erro', conta.fontes.erro],
            ['Campanhas', `${conta.campanhas} · ${conta.campanhas_activas} no ar`],
          ].map(([rotulo, valor]) => (
            <div key={rotulo} className="bg-background p-5">
              <p className="text-xs text-muted-foreground">{rotulo}</p>
              <p className={`mt-1 font-display text-3xl leading-none tracking-wide tabular-nums ${
                rotulo === 'Fontes com erro' && valor > 0 ? 'text-accent' : ''}`}>{valor}</p>
            </div>
          ))}
        </Painel>

        <div className="border border-border bg-card p-5">
          <h3 className="font-display text-xl tracking-wide">Factos da marca</h3>
          <p className="mt-2 max-w-prose text-sm leading-relaxed text-muted-foreground">
            O que saiu do material desta conta. É o passo humano: o que aqui for verificado é o
            que o agente vai poder afirmar sobre esta marca, e o que for proibido é o que ele nunca
            vai dizer. O tom e quem a marca é estão na voz, no separador "Brand kit".
          </p>
          {conta.sinalizacoes_abertas > 0 && (
            <p className="mt-3 border-l-2 border-accent pl-3 text-sm text-foreground">
              {conta.sinalizacoes_abertas} {conta.sinalizacoes_abertas === 1 ? 'sinalização do cliente' : 'sinalizações do cliente'} por
              responder. Até decidires, esses factos não vão ao Agente PM.
            </p>
          )}
          <button
            type="button"
            onClick={() => onNavegar(`/admin/dossie/${slug}`)}
            className="mt-4 bg-primary px-4 py-2 text-xs uppercase tracking-wider text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-1 focus-visible:ring-accent"
          >
            Rever os factos
          </button>
        </div>

        <div className="mt-4 border border-border bg-card p-5">
          <p className="label-eyebrow">Do lado do cliente</p>
          <p className="mt-2 max-w-prose text-sm leading-relaxed text-muted-foreground">
            O cliente carrega tudo em <strong>A minha marca → Ficheiros e fontes</strong>: vários
            ficheiros de uma vez, cada um com o que é (logótipo, manual, imagem, documento) e o estado
            da leitura na mesma linha. Os documentos lêem-se; as imagens só se alguém pedir.
          </p>
        </div>
      </PainelSeparador>

      <ModalEstadoConta
        aberto={mudancaEstado !== null}
        slug={slug}
        nome={conta.nome}
        accao={mudancaEstado}
        onFechar={() => setMudancaEstado(null)}
        onConcluido={(mensagem) => {
          setMudancaEstado(null);
          setToast({ id: Date.now(), mensagem });
          carregar();
        }}
      />

      <ModalReporPassword
        aberto={aReporPassword !== null}
        pessoa={aReporPassword}
        slug={slug}
        onFechar={() => setAReporPassword(null)}
        onConcluido={(mensagem) => {
          setAReporPassword(null);
          setToast({ id: Date.now(), mensagem });
        }}
      />

      <ModalConvidar
        aberto={aConvidar !== null}
        slug={slug}
        marca={conta.nome}
        pessoa={aConvidar?.pessoa}
        sugestao={aConvidar?.sugestao}
        onFechar={() => setAConvidar(null)}
        onConvidado={(mensagem) => {
          setAConvidar(null);
          setToast({ id: Date.now(), mensagem });
          carregar(); // o estado do convite mudou na lista
        }}
      />
      <Toast toast={toast} onFechar={fecharToast} />
    </div>
  );
}
