/*
  As métricas do link do orçamento: de cada abertura guardada até o que
  aparece na janela "Métricas do link".

  Tudo aqui é conta pura, sem banco e sem rede, para poder ser testado linha a
  linha. Quem busca as aberturas e o histórico é a rota
  /api/quotes/[id]/metricas-do-link.

  Três regras que mandam em tudo:

  1. Só conta o cliente. Abertura da equipe (navegador marcado pelo painel, ou
     marcado à mão com "fui eu") fica de fora de todos os números.
  2. Visita é o jeito de medir que a web inteira usa: aberturas seguidas do
     mesmo aparelho com menos de 30 minutos entre uma e outra são uma visita
     só. Recarregar a página não é voltar ao orçamento.
  3. A chance de fechar não tem peso inventado. É a proporção de orçamentos de
     verdade que, na mesma situação deste, viraram pedido — contada no banco
     por chance_de_fechar_orcamento — com o histórico da própria empresa
     puxando o número para o lado dela conforme ela tem casos (a média
     ponderada que a estatística chama de encolhimento bayesiano).
*/

export const FUSO = "America/Sao_Paulo";
export const INTERVALO_DE_VISITA_MS = 30 * 60 * 1000;
export const ABERTO_AGORA_MS = 45 * 1000;
/* Quantos casos a média geral vale na conta da empresa: com 20 orçamentos
   iguais no histórico dela, a empresa pesa metade. */
export const PESO_DA_MEDIA_GERAL = 20;
/* Abaixo disso, o grupo "já comprou antes / primeira compra" é pequeno demais
   para separar: vale a média de todos. */
export const MINIMO_PARA_SEPARAR = 30;
/* Quando houver esse tanto de orçamentos terminados com o registro detalhado,
   voltas ao link e aparelhos podem começar a ser medidos. */
export const MINIMO_PARA_MEDIR_SINAIS = 300;

export const SECOES_DA_FOLHA = ["itens", "total", "condicoes", "observacoes", "fim"] as const;
export const ACOES_DO_CLIENTE = ["pdf", "aprovar", "aprovou", "artes", "acompanhar"] as const;

export type SecaoDaFolha = (typeof SECOES_DA_FOLHA)[number];
export type AcaoDoCliente = (typeof ACOES_DO_CLIENTE)[number];
export type TipoDeAparelho = "celular" | "tablet" | "computador";

export type AberturaGuardada = {
  id: string;
  viewer_key: string;
  device_key: string | null;
  opened_at: string;
  last_seen_at: string;
  active_seconds: number | null;
  max_scroll: number | null;
  sections: string[] | null;
  actions: unknown;
  device_type: string | null;
  os_name: string | null;
  os_version: string | null;
  browser_name: string | null;
  device_model: string | null;
  in_app: string | null;
  referrer: string | null;
  city: string | null;
  region: string | null;
  country: string | null;
  is_team: boolean | null;
  counted: boolean | null;
};

export type EnvioGuardado = { channel: string; created_at: string };

export type OrcamentoDasMetricas = {
  numero: number | null;
  status: string | null;
  criadoEm: string;
  listaDePrecos: boolean;
  linkAtivo: boolean;
  linkVenceEm: string | null;
  validade: string | null;
  contadorAntigo: number;
  primeiraAntiga: string | null;
  ultimaAntiga: string | null;
  pedidoEm: string | null;
  clienteNome: string | null;
  clienteWhatsapp: string | null;
};

export type AcaoRegistrada = { tipo: AcaoDoCliente; em: string };

export type AparelhoDoLink = {
  id: string;
  tipo: TipoDeAparelho;
  nome: string;
  detalhe: string;
  aberturas: number;
  visitas: number;
  segundos: number;
  primeira: string;
  ultima: string;
  lugar: string | null;
  leitores: string[];
};

export type VisitaDoLink = {
  id: string;
  aparelhoId: string;
  inicio: string;
  fim: string;
  aberturas: number;
  segundos: number;
  rolagem: number;
  secoes: SecaoDaFolha[];
  acoes: AcaoRegistrada[];
  origem: string | null;
  lugar: string | null;
  depoisDoPedido: boolean;
};

/* ---------------------------------------------------------------- */
/* Leitura das linhas                                                */
/* ---------------------------------------------------------------- */

const ms = (valor: string | null | undefined) => {
  const tempo = valor ? new Date(valor).getTime() : NaN;
  return Number.isFinite(tempo) ? tempo : NaN;
};

const texto = (valor: unknown) => (typeof valor === "string" ? valor.trim() : "");

export function acoesDaAbertura(bruto: unknown): AcaoRegistrada[] {
  if (!Array.isArray(bruto)) return [];
  const validas = new Set<string>(ACOES_DO_CLIENTE);
  return bruto
    .map((item) => (item && typeof item === "object" ? (item as Record<string, unknown>) : {}))
    .filter((item) => validas.has(texto(item.tipo)) && Number.isFinite(ms(texto(item.em))))
    .map((item) => ({ tipo: texto(item.tipo) as AcaoDoCliente, em: texto(item.em) }));
}

function secoesDaAbertura(bruto: string[] | null | undefined): SecaoDaFolha[] {
  const validas = new Set<string>(SECOES_DA_FOLHA);
  return Array.isArray(bruto) ? (bruto.filter((secao) => validas.has(secao)) as SecaoDaFolha[]) : [];
}

function tipoDoAparelho(bruto: string | null | undefined): TipoDeAparelho {
  return bruto === "celular" || bruto === "tablet" ? bruto : "computador";
}

/** Separa o cliente da equipe: a marca do banco e os navegadores marcados à mão. */
export function separarDaEquipe(aberturas: AberturaGuardada[], leitoresDaEquipe: Iterable<string>) {
  const daEquipe = new Set(leitoresDaEquipe);
  const doCliente: AberturaGuardada[] = [];
  const equipe: AberturaGuardada[] = [];
  for (const abertura of aberturas) {
    if (abertura.is_team || daEquipe.has(abertura.viewer_key)) equipe.push(abertura);
    else doCliente.push(abertura);
  }
  return { doCliente, equipe };
}

/* ---------------------------------------------------------------- */
/* Aparelhos                                                         */
/* ---------------------------------------------------------------- */

export function chaveDoAparelho(abertura: Pick<AberturaGuardada, "device_key" | "viewer_key">) {
  return abertura.device_key ? `a:${abertura.device_key}` : `l:${abertura.viewer_key}`;
}

/** Como a pessoa reconhece o aparelho: o modelo quando se sabe; senão o sistema. */
export function nomeDoAparelho(abertura: Pick<AberturaGuardada, "device_model" | "os_name" | "device_type">) {
  const modelo = texto(abertura.device_model);
  if (modelo) return modelo;
  const tipo = tipoDoAparelho(abertura.device_type);
  switch (abertura.os_name) {
    case "Android":
      return tipo === "tablet" ? "Tablet Android" : "Celular Android";
    case "iOS":
      return "iPhone";
    case "iPadOS":
      return "iPad";
    case "Windows":
      return "Computador com Windows";
    case "macOS":
      return "Mac";
    case "ChromeOS":
      return "Chromebook";
    case "Linux":
      return "Computador com Linux";
    default:
      return tipo === "celular" ? "Celular" : tipo === "tablet" ? "Tablet" : "Computador";
  }
}

function juntarComE(partes: string[]) {
  const unicas = [...new Set(partes.filter(Boolean))];
  if (unicas.length <= 1) return unicas[0] ?? "";
  return `${unicas.slice(0, -1).join(", ")} e ${unicas[unicas.length - 1]}`;
}

function lugarDaAbertura(abertura: Pick<AberturaGuardada, "city" | "region" | "country">) {
  const cidade = texto(abertura.city);
  const estado = texto(abertura.region);
  const pais = texto(abertura.country).toUpperCase();
  if (cidade) return estado && estado !== cidade ? `${cidade}, ${estado}` : cidade;
  if (pais && pais !== "BR") return `Fora do Brasil (${pais})`;
  return null;
}

export function agruparAparelhos(aberturas: AberturaGuardada[], visitas: VisitaDoLink[] = []): AparelhoDoLink[] {
  const grupos = new Map<string, AberturaGuardada[]>();
  for (const abertura of aberturas) {
    const chave = chaveDoAparelho(abertura);
    grupos.set(chave, [...(grupos.get(chave) ?? []), abertura]);
  }
  const lista: AparelhoDoLink[] = [];
  for (const [id, doAparelho] of grupos) {
    const ordenadas = [...doAparelho].sort((a, b) => ms(a.opened_at) - ms(b.opened_at));
    const referencia = ordenadas[ordenadas.length - 1];
    const sistema = texto(referencia.os_name);
    const versao = texto(referencia.os_version);
    const ondeAbriu = juntarComE(
      ordenadas.map((abertura) => (abertura.in_app ? `dentro do ${abertura.in_app}` : texto(abertura.browser_name)))
    );
    const lugares = ordenadas.map(lugarDaAbertura).filter((lugar): lugar is string => Boolean(lugar));
    lista.push({
      id,
      tipo: tipoDoAparelho(referencia.device_type),
      nome: nomeDoAparelho(referencia),
      detalhe: [sistema ? `${sistema}${versao ? ` ${versao}` : ""}` : "", ondeAbriu].filter(Boolean).join(" · "),
      aberturas: ordenadas.length,
      visitas: visitas.filter((visita) => visita.aparelhoId === id).length,
      segundos: ordenadas.reduce((soma, abertura) => soma + Math.max(0, Number(abertura.active_seconds) || 0), 0),
      primeira: ordenadas[0].opened_at,
      ultima: ordenadas.reduce(
        (maior, abertura) => (ms(abertura.last_seen_at) > ms(maior) ? abertura.last_seen_at : maior),
        ordenadas[0].last_seen_at
      ),
      lugar: lugares.length ? lugares[lugares.length - 1] : null,
      leitores: [...new Set(ordenadas.map((abertura) => abertura.viewer_key))],
    });
  }
  /* O aparelho mais usado primeiro; empate, o mais recente. */
  return lista.sort((a, b) => b.aberturas - a.aberturas || ms(b.ultima) - ms(a.ultima));
}

/* ---------------------------------------------------------------- */
/* Visitas                                                           */
/* ---------------------------------------------------------------- */

export function montarVisitas(aberturas: AberturaGuardada[], pedidoEm: string | null = null): VisitaDoLink[] {
  const porAparelho = new Map<string, AberturaGuardada[]>();
  for (const abertura of aberturas) {
    const chave = chaveDoAparelho(abertura);
    porAparelho.set(chave, [...(porAparelho.get(chave) ?? []), abertura]);
  }
  const pedido = ms(pedidoEm);
  const visitas: VisitaDoLink[] = [];
  for (const [aparelhoId, doAparelho] of porAparelho) {
    const ordenadas = [...doAparelho].sort((a, b) => ms(a.opened_at) - ms(b.opened_at));
    let atual: VisitaDoLink | null = null;
    for (const abertura of ordenadas) {
      const inicio = ms(abertura.opened_at);
      const fim = Math.max(inicio, ms(abertura.last_seen_at) || inicio);
      if (!atual || inicio - ms(atual.fim) > INTERVALO_DE_VISITA_MS) {
        atual = {
          id: abertura.id,
          aparelhoId,
          inicio: abertura.opened_at,
          fim: new Date(fim).toISOString(),
          aberturas: 0,
          segundos: 0,
          rolagem: 0,
          secoes: [],
          acoes: [],
          origem: texto(abertura.referrer) || null,
          lugar: lugarDaAbertura(abertura),
          depoisDoPedido: Number.isFinite(pedido) && inicio > pedido,
        };
        visitas.push(atual);
      }
      atual.aberturas += 1;
      atual.segundos += Math.max(0, Number(abertura.active_seconds) || 0);
      atual.rolagem = Math.max(atual.rolagem, Math.min(100, Math.max(0, Number(abertura.max_scroll) || 0)));
      const vistas = new Set<SecaoDaFolha>([...atual.secoes, ...secoesDaAbertura(abertura.sections)]);
      atual.secoes = SECOES_DA_FOLHA.filter((secao) => vistas.has(secao));
      atual.acoes = [...atual.acoes, ...acoesDaAbertura(abertura.actions)].sort((a, b) => ms(a.em) - ms(b.em));
      if (fim > ms(atual.fim)) atual.fim = new Date(fim).toISOString();
      if (!atual.origem && abertura.referrer) atual.origem = texto(abertura.referrer);
      if (!atual.lugar) atual.lugar = lugarDaAbertura(abertura);
    }
  }
  /* A mais recente em cima, como numa conversa lida de trás para frente. */
  return visitas.sort((a, b) => ms(b.inicio) - ms(a.inicio));
}

/* ---------------------------------------------------------------- */
/* Horários                                                          */
/* ---------------------------------------------------------------- */

const formatoDaHora = new Intl.DateTimeFormat("en-GB", { timeZone: FUSO, hour: "2-digit", hourCycle: "h23" });

export function horaNoFuso(iso: string) {
  const hora = Number(formatoDaHora.format(new Date(iso)));
  return Number.isFinite(hora) ? hora % 24 : 0;
}

/** Quantas aberturas em cada hora do dia (0 a 23), no horário de Brasília. */
export function aberturasPorHora(aberturas: Array<Pick<AberturaGuardada, "opened_at">>) {
  const horas = Array.from({ length: 24 }, () => 0);
  for (const abertura of aberturas) {
    if (!Number.isFinite(ms(abertura.opened_at))) continue;
    horas[horaNoFuso(abertura.opened_at)] += 1;
  }
  return horas;
}

/**
 * A faixa de 3 horas em que o cliente mais abre, quando ela diz alguma coisa:
 * pelo menos 3 aberturas e metade delas dentro da faixa. Senão, null.
 */
export function faixaPreferida(horas: number[]) {
  const total = horas.reduce((soma, valor) => soma + valor, 0);
  if (total < 3) return null;
  let melhor = { inicio: 0, soma: -1 };
  for (let inicio = 0; inicio < 24; inicio += 1) {
    const soma = horas[inicio] + horas[(inicio + 1) % 24] + horas[(inicio + 2) % 24];
    if (soma > melhor.soma) melhor = { inicio, soma };
  }
  if (melhor.soma * 2 < total) return null;
  /* Encolhe as pontas vazias: "entre 19h e 21h" em vez de "entre 18h e 21h"
     quando às 18h não houve nada. */
  let inicio = melhor.inicio;
  let fim = (melhor.inicio + 2) % 24;
  if (horas[inicio] === 0) inicio = (inicio + 1) % 24;
  if (horas[fim] === 0) fim = (fim + 23) % 24;
  return { inicio, fim: (fim + 1) % 24, aberturas: melhor.soma, total };
}

/* ---------------------------------------------------------------- */
/* Chance de fechar                                                  */
/* ---------------------------------------------------------------- */

export type PontoDaCoorte = {
  dias: number;
  n: number;
  ganhos: number;
  n_estrato: number;
  ganhos_estrato: number;
  n_empresa: number;
  ganhos_empresa: number;
  n_empresa_estrato: number;
  ganhos_empresa_estrato: number;
};

export type Estimativa = {
  dias: number;
  chance: number;
  geral: { casos: number; fechados: number; taxa: number };
  empresa: { casos: number; fechados: number; taxa: number | null; peso: number };
  separouRecorrencia: boolean;
  precisao: "alta" | "media" | "baixa";
};

const numero = (valor: unknown) => {
  const convertido = Number(valor);
  return Number.isFinite(convertido) ? convertido : 0;
};

export function lerPontos(bruto: unknown): PontoDaCoorte[] {
  if (!Array.isArray(bruto)) return [];
  return bruto
    .map((item) => (item && typeof item === "object" ? (item as Record<string, unknown>) : {}))
    .map((item) => ({
      dias: numero(item.dias),
      n: numero(item.n),
      ganhos: numero(item.ganhos),
      n_estrato: numero(item.n_estrato),
      ganhos_estrato: numero(item.ganhos_estrato),
      n_empresa: numero(item.n_empresa),
      ganhos_empresa: numero(item.ganhos_empresa),
      n_empresa_estrato: numero(item.n_empresa_estrato),
      ganhos_empresa_estrato: numero(item.ganhos_empresa_estrato),
    }))
    .sort((a, b) => a.dias - b.dias);
}

/**
 * A chance de um ponto: a taxa geral dos orçamentos na mesma situação (do
 * mesmo grupo, já comprou / primeira compra, quando o grupo tem casos para
 * isso) puxada pela taxa da empresa, com peso crescendo com os casos dela:
 *
 *   chance = (fechados da empresa + 20 × taxa geral) / (casos da empresa + 20)
 */
export function estimarChance(ponto: PontoDaCoorte): Estimativa | null {
  const separouRecorrencia = ponto.n_estrato >= MINIMO_PARA_SEPARAR;
  const casos = separouRecorrencia ? ponto.n_estrato : ponto.n;
  const fechados = separouRecorrencia ? ponto.ganhos_estrato : ponto.ganhos;
  if (casos <= 0) return null;
  const taxa = fechados / casos;
  const casosDaEmpresa = separouRecorrencia ? ponto.n_empresa_estrato : ponto.n_empresa;
  const fechadosDaEmpresa = separouRecorrencia ? ponto.ganhos_empresa_estrato : ponto.ganhos_empresa;
  const chance = (fechadosDaEmpresa + PESO_DA_MEDIA_GERAL * taxa) / (casosDaEmpresa + PESO_DA_MEDIA_GERAL);
  return {
    dias: ponto.dias,
    chance,
    geral: { casos, fechados, taxa },
    empresa: {
      casos: casosDaEmpresa,
      fechados: fechadosDaEmpresa,
      taxa: casosDaEmpresa > 0 ? fechadosDaEmpresa / casosDaEmpresa : null,
      peso: casosDaEmpresa / (casosDaEmpresa + PESO_DA_MEDIA_GERAL),
    },
    separouRecorrencia,
    precisao: casos >= 200 ? "alta" : casos >= 60 ? "media" : "baixa",
  };
}

/** Os pontos que a conta pede ao banco: agora, amanhã, em 3 e em 7 dias (e a primeira abertura). */
export function pontosDaConsulta(diasAgora: number, aberto: boolean) {
  const agora = Math.max(0, Math.round(diasAgora * 1000) / 1000);
  const pontos = [agora, agora + 1, agora + 3, agora + 7];
  if (aberto) pontos.unshift(0);
  return [...new Set(pontos.map((dia) => Math.round(dia * 1000) / 1000))].sort((a, b) => a - b);
}

export type ChanceDoOrcamento =
  | { estado: "fechado"; em: string | null }
  /** Marcado como aprovado no painel, mas sem pedido gerado: só o pedido fecha de verdade. */
  | { estado: "aprovado" }
  | { estado: "recusado" }
  | { estado: "sem-conta"; motivo: string }
  | {
      estado: "aberto";
      aberto: boolean;
      diasDesde: number;
      recorrente: boolean;
      atual: Estimativa;
      naPrimeiraAbertura: Estimativa | null;
      daqui: Array<{ dias: number; rotulo: string; chance: number }>;
      recorrencia: { mesmoGrupo: number; outroGrupo: number | null; casos: number } | null;
      coorte: { orcamentos: number; desde: string | null; ate: string | null };
      comRegistroDetalhado: number;
    };

export function montarChance(input: {
  status: string | null;
  pedidoEm: string | null;
  listaDePrecos: boolean;
  aberto: boolean;
  diasDesde: number;
  resposta: unknown;
}): ChanceDoOrcamento {
  /* Só o pedido gerado fecha o orçamento de verdade (pelo botão Gerar pedido
     ou pela aprovação do cliente no link). O status "aprovado" marcado no
     painel é uma intenção: o pedido ainda não existe. */
  if (input.pedidoEm) return { estado: "fechado", em: input.pedidoEm };
  if (input.status === "rejected") return { estado: "recusado" };
  if (input.status === "approved" || input.status === "production" || input.status === "delivered") return { estado: "aprovado" };
  if (input.listaDePrecos) return { estado: "sem-conta", motivo: "Lista de preços não tem aprovação do cliente." };
  const resposta = input.resposta && typeof input.resposta === "object" ? (input.resposta as Record<string, unknown>) : {};
  const pontos = lerPontos(resposta.pontos);
  const perto = (dias: number) => pontos.reduce<PontoDaCoorte | null>((achado, ponto) => {
    if (!achado) return ponto;
    return Math.abs(ponto.dias - dias) < Math.abs(achado.dias - dias) ? ponto : achado;
  }, null);
  const agora = perto(input.diasDesde);
  const atual = agora ? estimarChance(agora) : null;
  if (!agora || !atual) {
    return { estado: "sem-conta", motivo: "Ainda não há orçamentos suficientes nessa situação para fazer a conta." };
  }

  const daqui = [
    { dias: 1, rotulo: "Amanhã" },
    { dias: 3, rotulo: "Em 3 dias" },
    { dias: 7, rotulo: "Em 7 dias" },
  ]
    .map(({ dias, rotulo }) => {
      const ponto = perto(input.diasDesde + dias);
      const estimativa = ponto && Math.abs(ponto.dias - (input.diasDesde + dias)) < 0.01 ? estimarChance(ponto) : null;
      return estimativa ? { dias, rotulo, chance: estimativa.chance } : null;
    })
    .filter((item): item is { dias: number; rotulo: string; chance: number } => Boolean(item));

  const primeiro = input.aberto ? pontos.find((ponto) => ponto.dias === 0) ?? null : null;
  const outroCasos = agora.n - agora.n_estrato;
  const outroFechados = agora.ganhos - agora.ganhos_estrato;
  const recorrencia = atual.separouRecorrencia
    ? {
        mesmoGrupo: atual.geral.taxa,
        outroGrupo: outroCasos >= MINIMO_PARA_SEPARAR ? outroFechados / outroCasos : null,
        casos: agora.n,
      }
    : null;

  return {
    estado: "aberto",
    aberto: input.aberto,
    diasDesde: input.diasDesde,
    recorrente: resposta.recorrente === true,
    atual,
    naPrimeiraAbertura: primeiro ? estimarChance(primeiro) : null,
    daqui,
    recorrencia,
    coorte: {
      orcamentos: numero(resposta.coorte),
      desde: typeof resposta.coorte_desde === "string" ? resposta.coorte_desde : null,
      ate: typeof resposta.coorte_ate === "string" ? resposta.coorte_ate : null,
    },
    comRegistroDetalhado: numero(resposta.com_registro_detalhado),
  };
}

/* ---------------------------------------------------------------- */
/* Resumo, sinais e próximos passos                                  */
/* ---------------------------------------------------------------- */

export type ResumoDoLink = {
  aberturas: number;
  aberturasDetalhadas: number;
  visitas: number;
  aparelhos: number;
  segundos: number;
  primeira: string | null;
  ultima: string | null;
  abertoAgora: boolean;
  rolagem: number;
  viu: SecaoDaFolha[];
  acoes: AcaoDoCliente[];
  legado: { aberturas: number; primeira: string | null; ultima: string | null } | null;
  daEquipe: number;
  horas: number[];
  faixa: ReturnType<typeof faixaPreferida>;
};

export function resumir(input: {
  doCliente: AberturaGuardada[];
  daEquipe: AberturaGuardada[];
  visitas: VisitaDoLink[];
  aparelhos: AparelhoDoLink[];
  orcamento: Pick<OrcamentoDasMetricas, "contadorAntigo" | "primeiraAntiga" | "ultimaAntiga">;
  agora: number;
}): ResumoDoLink {
  const { doCliente } = input;
  /* O contador antigo inclui as aberturas de antes do registro detalhado. As
     que o registro já contou saem da conta; o resto é o "antes". */
  const contadasAqui = [...doCliente, ...input.daEquipe].filter((abertura) => abertura.counted).length;
  const antes = Math.max(0, input.orcamento.contadorAntigo - contadasAqui);
  const legado =
    antes > 0
      ? {
          aberturas: antes,
          primeira: input.orcamento.primeiraAntiga,
          ultima: contadasAqui === 0 ? input.orcamento.ultimaAntiga : null,
        }
      : null;

  const inicios = doCliente.map((abertura) => ms(abertura.opened_at)).filter(Number.isFinite);
  const fins = doCliente.map((abertura) => ms(abertura.last_seen_at)).filter(Number.isFinite);
  if (legado?.primeira && Number.isFinite(ms(legado.primeira))) inicios.push(ms(legado.primeira));
  if (legado?.ultima && Number.isFinite(ms(legado.ultima))) fins.push(ms(legado.ultima));
  const primeira = inicios.length ? new Date(Math.min(...inicios)).toISOString() : null;
  const ultima = fins.length ? new Date(Math.max(...fins)).toISOString() : null;

  const viu = SECOES_DA_FOLHA.filter((secao) => doCliente.some((abertura) => secoesDaAbertura(abertura.sections).includes(secao)));
  const acoes = ACOES_DO_CLIENTE.filter((acao) => doCliente.some((abertura) => acoesDaAbertura(abertura.actions).some((item) => item.tipo === acao)));
  const horas = aberturasPorHora(doCliente);

  return {
    aberturas: doCliente.length + (legado?.aberturas ?? 0),
    aberturasDetalhadas: doCliente.length,
    visitas: input.visitas.length,
    aparelhos: input.aparelhos.length,
    segundos: doCliente.reduce((soma, abertura) => soma + Math.max(0, Number(abertura.active_seconds) || 0), 0),
    primeira,
    ultima,
    abertoAgora: doCliente.some((abertura) => input.agora - ms(abertura.last_seen_at) <= ABERTO_AGORA_MS),
    rolagem: doCliente.reduce((maior, abertura) => Math.max(maior, Math.min(100, Number(abertura.max_scroll) || 0)), 0),
    viu,
    acoes,
    legado,
    daEquipe: input.daEquipe.length,
    horas,
    faixa: faixaPreferida(horas),
  };
}

export type TomDoSinal = "bom" | "atencao" | "neutro";

export type Sinal = { id: string; tom: TomDoSinal; texto: string };

export type Passo = {
  id: string;
  tom: TomDoSinal;
  titulo: string;
  texto: string;
  botao: "whatsapp" | "reenviar" | "renovar" | null;
  mensagem: string | null;
};

export function duracaoLegivel(segundos: number) {
  const total = Math.max(0, Math.round(segundos));
  if (total < 60) return `${total} s`;
  const minutos = Math.round(total / 60);
  if (minutos < 60) return `${minutos} min`;
  const horas = Math.floor(minutos / 60);
  const resto = minutos % 60;
  return resto ? `${horas} h ${resto} min` : `${horas} h`;
}

export function haQuantoTempo(iso: string | null, agora: number) {
  const tempo = ms(iso);
  if (!Number.isFinite(tempo)) return "";
  const segundos = Math.max(0, Math.round((agora - tempo) / 1000));
  if (segundos < 60) return "agora há pouco";
  const minutos = Math.round(segundos / 60);
  if (minutos < 60) return `há ${minutos} min`;
  const horas = Math.round(minutos / 60);
  if (horas < 24) return `há ${horas} ${horas === 1 ? "hora" : "horas"}`;
  const dias = Math.round(horas / 24);
  if (dias < 60) return `há ${dias} ${dias === 1 ? "dia" : "dias"}`;
  /* "há 142 dias" ninguém lê de relance; "há 5 meses", sim. */
  const meses = Math.round(dias / 30.44);
  if (meses < 12) return `há ${meses} meses`;
  const anos = Math.round(dias / 365.25);
  return `há ${anos} ${anos === 1 ? "ano" : "anos"}`;
}

/** Porcentagem para ler: "38%", e "<1%" em vez de um "0%" que não é zero. */
export function porcentoLegivel(fracao: number) {
  if (!Number.isFinite(fracao) || fracao <= 0) return "0%";
  if (fracao < 0.005) return "<1%";
  return `${Math.round(fracao * 100)}%`;
}

export function primeiroNome(nome: string | null | undefined) {
  const limpo = texto(nome);
  if (!limpo) return "";
  const primeiro = limpo.split(/\s+/)[0];
  return primeiro.charAt(0).toUpperCase() + primeiro.slice(1).toLowerCase();
}

function saudacao(nome: string | null) {
  const primeiro = primeiroNome(nome);
  return primeiro ? `Oi, ${primeiro}!` : "Oi!";
}

export function sinaisDoLink(resumo: ResumoDoLink, orcamento: OrcamentoDasMetricas, agora: number): Sinal[] {
  const sinais: Sinal[] = [];
  if (resumo.abertoAgora) sinais.push({ id: "agora", tom: "bom", texto: "Está com o orçamento aberto agora" });
  if (resumo.visitas >= 2) sinais.push({ id: "voltou", tom: "bom", texto: `Voltou ao orçamento: ${resumo.visitas} visitas` });
  if (resumo.aparelhos >= 2) sinais.push({ id: "aparelhos", tom: "neutro", texto: `Aberto em ${resumo.aparelhos} aparelhos` });
  if (resumo.acoes.includes("aprovar") && !resumo.acoes.includes("aprovou") && !orcamento.pedidoEm) {
    sinais.push({ id: "comecou", tom: "atencao", texto: "Clicou em aprovar e não concluiu" });
  }
  if (resumo.acoes.includes("pdf")) sinais.push({ id: "pdf", tom: "neutro", texto: "Baixou ou imprimiu o PDF" });
  if (resumo.viu.includes("total")) sinais.push({ id: "total", tom: "neutro", texto: "Viu o valor total" });
  if (resumo.viu.includes("condicoes")) sinais.push({ id: "condicoes", tom: "neutro", texto: "Viu as condições de pagamento" });
  if (resumo.acoes.includes("artes")) sinais.push({ id: "artes", tom: "neutro", texto: "Abriu as artes" });
  if (resumo.aberturasDetalhadas > 0 && resumo.segundos < 20) {
    sinais.push({ id: "rapido", tom: "atencao", texto: `Leu por só ${duracaoLegivel(resumo.segundos)}` });
  }
  if (!resumo.primeira && orcamento.linkAtivo) sinais.push({ id: "nao-abriu", tom: "atencao", texto: "Ainda não abriu o link" });
  const vence = ms(orcamento.linkVenceEm);
  if (Number.isFinite(vence) && vence < agora) sinais.push({ id: "link-vencido", tom: "atencao", texto: "O link venceu" });
  return sinais;
}

export function passosDoLink(input: {
  resumo: ResumoDoLink;
  orcamento: OrcamentoDasMetricas;
  chance: ChanceDoOrcamento;
  ultimoEnvio: string | null;
  agora: number;
  hoje: string;
}): Passo[] {
  const { resumo, orcamento, chance, agora } = input;
  const passos: Passo[] = [];
  const oi = saudacao(orcamento.clienteNome);
  const numero = orcamento.numero ? ` #${orcamento.numero}` : "";
  const fechado = chance.estado === "fechado" || chance.estado === "recusado";
  if (fechado) return passos;
  if (chance.estado === "aprovado") {
    passos.push({
      id: "gerar-pedido",
      tom: "atencao",
      titulo: "Aprovado no painel, mas o pedido ainda não existe",
      texto:
        "Marcar o orçamento como aprovado não gera o pedido. Clique em Gerar pedido no topo da tela, ou deixe o cliente aprovar pelo link: aí o pedido nasce sozinho.",
      botao: null,
      mensagem: null,
    });
    return passos;
  }

  const vence = ms(orcamento.linkVenceEm);
  if (orcamento.linkAtivo && Number.isFinite(vence) && vence < agora) {
    passos.push({
      id: "renovar",
      tom: "atencao",
      titulo: "O link venceu e o cliente não consegue mais abrir",
      texto: "Gere o link de novo e mande outra vez. As aberturas registradas até aqui continuam aqui.",
      botao: "renovar",
      mensagem: null,
    });
  }

  if (resumo.acoes.includes("aprovar") && !resumo.acoes.includes("aprovou")) {
    passos.push({
      id: "comecou",
      tom: "atencao",
      titulo: "Começou a aprovar e parou no meio",
      texto: "Ele clicou em Aprovar e não concluiu. Às vezes é a assinatura, uma arte pendente ou uma dúvida de última hora.",
      botao: "whatsapp",
      mensagem: `${oi} Tudo certo com o orçamento${numero}? Se precisar de algum ajuste para aprovar, é só me falar que eu resolvo.`,
    });
  }

  if (resumo.abertoAgora) {
    passos.push({
      id: "agora",
      tom: "bom",
      titulo: "Ele está com o orçamento aberto agora",
      texto: "Uma mensagem enquanto o assunto está na frente dele costuma ser respondida na hora.",
      botao: "whatsapp",
      mensagem: `${oi} Estou por aqui se quiser tirar alguma dúvida sobre o orçamento${numero}.`,
    });
  }

  if (!resumo.primeira && orcamento.linkAtivo) {
    const desde = ms(input.ultimoEnvio) || ms(orcamento.criadoEm);
    const horas = Number.isFinite(desde) ? (agora - desde) / 3_600_000 : 0;
    if (horas >= 24) {
      passos.push({
        id: "nao-abriu",
        tom: "atencao",
        titulo: "O link ainda não foi aberto",
        texto: "Confira se a mensagem chegou: número errado, conversa arquivada ou o link perdido no meio de outras mensagens.",
        botao: "reenviar",
        mensagem: null,
      });
    }
  }

  if (resumo.aparelhos >= 2) {
    passos.push({
      id: "aparelhos",
      tom: "neutro",
      titulo: `Aberto em ${resumo.aparelhos} aparelhos diferentes`,
      texto: "Pode ser a mesma pessoa no celular e no computador, ou o orçamento repassado a alguém que também decide. Vale oferecer explicação para quem mais for decidir.",
      botao: "whatsapp",
      mensagem: `${oi} Se mais alguém for participar da decisão, posso explicar os detalhes do orçamento${numero} para vocês.`,
    });
  }

  if (resumo.acoes.includes("pdf")) {
    passos.push({
      id: "pdf",
      tom: "neutro",
      titulo: "Baixou o PDF",
      texto: "Quem baixa costuma levar o orçamento para alguém aprovar ou comparar com outro. Pergunte se precisa de alguma informação a mais.",
      botao: "whatsapp",
      mensagem: `${oi} Se precisar de alguma informação a mais no orçamento${numero} para aprovar aí, me avisa.`,
    });
  }

  const ultima = ms(resumo.ultima);
  if (resumo.primeira && Number.isFinite(ultima) && agora - ultima >= 2 * 86_400_000 && chance.estado === "aberto") {
    const dias = Math.round((agora - ultima) / 86_400_000);
    const semana = chance.daqui.find((ponto) => ponto.dias === 7);
    passos.push({
      id: "esfriando",
      tom: "atencao",
      titulo: `Faz ${dias} dias que ele não abre o orçamento`,
      texto: `A chance cai a cada dia sem resposta${semana ? `: em uma semana, orçamentos assim chegam a ${Math.round(semana.chance * 100)}%` : ""}. Um contato hoje pode destravar.`,
      botao: "whatsapp",
      mensagem: `${oi} Passando para saber se ficou alguma dúvida no orçamento${numero}. Consigo ajustar alguma coisa para fechar?`,
    });
  }

  if (resumo.aberturasDetalhadas > 0 && resumo.segundos < 20 && !resumo.abertoAgora) {
    passos.push({
      id: "rapido",
      tom: "neutro",
      titulo: "Passou pouco tempo no orçamento",
      texto: `Ao todo, ${duracaoLegivel(resumo.segundos)} de leitura. Talvez não tenha visto os detalhes: destaque o principal na conversa.`,
      botao: null,
      mensagem: null,
    });
  }

  if (orcamento.validade && orcamento.validade < input.hoje) {
    passos.push({
      id: "validade",
      tom: "atencao",
      titulo: "A validade da proposta já passou",
      texto: "Antes de cobrar uma resposta, atualize a validade no orçamento para o cliente não achar que ele não vale mais.",
      botao: null,
      mensagem: null,
    });
  }

  if (resumo.faixa) {
    passos.push({
      id: "horario",
      tom: "neutro",
      titulo: `Ele costuma abrir entre ${resumo.faixa.inicio}h e ${resumo.faixa.fim}h`,
      texto: `${resumo.faixa.aberturas} das ${resumo.faixa.total} aberturas foram nessa faixa. É um bom horário para falar com ele.`,
      botao: null,
      mensagem: null,
    });
  }

  return passos;
}

/* ---------------------------------------------------------------- */
/* O que a rota devolve para a janela                                */
/* ---------------------------------------------------------------- */

export type AparelhoDaEquipe = AparelhoDoLink & { marcadoAMao: boolean };
export type EnvioDoLink = { canal: "whatsapp" | "link"; em: string };

export type MetricasDoLink = {
  geradoEm: string;
  orcamento: {
    numero: number | null;
    status: string | null;
    criadoEm: string;
    linkAtivo: boolean;
    linkVenceEm: string | null;
    validade: string | null;
    pedidoEm: string | null;
    listaDePrecos: boolean;
    cliente: { nome: string | null; whatsapp: string | null };
  };
  resumo: ResumoDoLink;
  visitas: VisitaDoLink[];
  aparelhos: AparelhoDoLink[];
  equipe: { aberturas: number; aparelhos: AparelhoDaEquipe[] };
  envios: EnvioDoLink[];
  chance: ChanceDoOrcamento;
  sinais: Sinal[];
  passos: Passo[];
};

/** O endereço do WhatsApp com a mensagem pronta, ou null sem número. */
export function linkDoWhatsapp(numero: string | null | undefined, mensagem: string) {
  let digitos = String(numero ?? "").replace(/\D/g, "");
  /* Número brasileiro sem o 55 na frente (DDD + número). */
  if (digitos.length === 10 || digitos.length === 11) digitos = `55${digitos}`;
  if (digitos.length < 12 || digitos.length > 15) return null;
  return `https://wa.me/${digitos}?text=${encodeURIComponent(mensagem)}`;
}
