/*
  A chance de fechar do orçamento "no geral": a da faixa do orçamento.

  Não depende do link. A chance pelo link continua nas métricas do link
  (lib/metricas-do-link.ts), sem mudança. Esta olha a empresa em questão e o
  cliente dela:
  - da empresa: a taxa de fechamento dos orçamentos dela, pesando mais o que
    ela anda fazendo agora (meia-vida de 30 dias: o orçamento de um mês atrás
    vale metade do de hoje). É o fator que mais pesa;
  - do cliente com a empresa: se já comprou, se já aceitou orçamento, o
    ticket médio dele e se existe um orçamento mais novo para ele;
  - do orçamento: o valor e há quanto tempo está aberto.
  Os fatos vêm da função dados_da_chance_de_fechar (migrations 147 e 148),
  como eram no dia em que o orçamento nasceu. Nada do futuro entra, porque
  foi assim que os pesos foram medidos.

  A plataforma (todas as empresas) entra pelos pesos: quanto vale cada um
  desses fatos e como a chance cai com o tempo. E é o ponto de partida de
  quem tem pouco histórico (a taxa da empresa começa com 2 orçamentos "da
  média" e vai sendo tomada pelos dela).

  De onde saem os pesos (medidos em 25/09/2026, nenhum chutado):
  - 2.664 orçamentos de verdade de todas as empresas, criados entre 15/01 e
    27/07/2026, todos com 60 dias ou mais de história (99,8% dos fechamentos
    acontecem até 60 dias). "De verdade": não é venda do PDV, não é lista de
    preços e não virou pedido na primeira hora (isso é venda registrada, não
    proposta à espera do cliente).
  - Regressão logística por marcos de tempo ("landmarking", van Houwelingen,
    2007). Cada orçamento entra em cada marco em que ainda estava aberto (1 h,
    6 h, 1, 3, 7, 14 e 30 dias), somando 14.255 observações. Assim a mesma
    conta serve para um orçamento de 2 horas e para um de 20 dias.
  - Validação em orçamentos que o modelo não viu. Treinado até 15/05 e medido
    nos 1.754 orçamentos seguintes, acerta a ordem em 83,1% dos pares: entre
    um que fechou e um que não fechou, o que fechou tinha a chance maior (AUC
    0,831; só pelo tempo aberto, 0,714). Em empresas que ele nunca viu
    (validação em 5 grupos), 0,823. Quando diz 25%, fecham cerca de 25 em 100.
  - Testado e deixado de fora, porque errou mais nos meses seguintes:
    medir os pesos só com os orçamentos de cada empresa (o jeito de cada uma
    muda de um mês para o outro), a taxa da empresa separada por tipo de
    cliente, recorrência (meses com compra no ano), número de compras, dias
    desde a última compra, "pediu antes e nunca fechou" e outros orçamentos na
    mesma semana. A tela mostra os fatos do cliente, sem peso.
  - Para medir de novo (quando houver um ano de história): roteiros em
    .cache/chance-global (LEIA.md; conferir-funcao compara a função do banco
    com o treino, orçamento a orçamento).
*/

export const PESOS_DA_CHANCE = {
  intercepto: 0.0016,
  /* ln das horas aberto e o quadrado: a chance cai com o tempo, cada vez mais devagar. */
  marco: 0.0697,
  marco2: -0.0701,
  /* logit da taxa recente de fechamento da empresa (encolhida para a média da plataforma). */
  empresa: 1.107,
  empresaXmarco: -0.0404,
  comprou: 0.3508,
  fechouOrcamento: 0.9278,
  maisNovo: -0.7293,
  ticketAcima: -0.5615,
  /* log10 do valor, centrado em R$ 316 (10^2,5). */
  valor: -0.3553,
  generico: -1.505,
} as const;

/** Taxa de fechamento dos orçamentos de verdade da plataforma, no período medido. */
export const TAXA_DA_PLATAFORMA = 0.4152;
/** Quantos orçamentos "da média" entram na taxa da empresa (a validação escolheu 2 entre 1, 2, 4, 8 e 20). */
const ENCOLHIMENTO = 2;
/** Dias para um orçamento antigo valer metade na taxa da empresa (15 a 45 deram quase o mesmo). */
export const MEIA_VIDA_DA_EMPRESA = 30;
/** Valor mediano de um orçamento de verdade na plataforma. */
export const VALOR_TIPICO = 450;
/** Quantas vezes acima do ticket médio do cliente o valor começa a pesar contra. */
export const TICKET_ACIMA = 4;

export type DadosDaChanceGlobal = {
  agora: string;
  criado_em: string;
  total: number | string | null;
  status: string | null;
  lista_de_preco: boolean;
  pdv: boolean;
  pedido_em: string | null;
  cliente_generico: boolean;
  /* n e g: orçamentos do ano e quantos fecharam; *_recente: as mesmas somas com peso 0,5^(idade/30 dias). */
  empresa: { n: number; g: number; n_novo: number; g_novo: number; n_recente: number | string; g_recente: number | string };
  cliente: {
    pedidos: number;
    ticket_medio: number | string | null;
    ultima_compra: string | null;
    pedidos_no_ano: number;
    meses_no_ano: number;
    orcamentos: number;
    fechados: number;
    recentes: number;
    mais_novo_em: string | null;
  };
};

export type ChaveDoFator = "empresa" | "tempo" | "cliente" | "fechou-antes" | "mais-novo" | "ticket" | "valor" | "generico";

export type FatorDaChance = {
  chave: ChaveDoFator;
  titulo: string;
  detalhe: string;
  /** Quanto este fator move a chance, em fração (0,08 = 8 pontos). null = só informação, sem peso. */
  efeito: number | null;
};

export type TipoDeCliente = "novo" | "voltou" | "generico";

export type ChanceGlobalAberta = {
  estado: "aberto";
  chance: number;
  naCriacao: number;
  amanha: number;
  emUmaSemana: number;
  emUmMes: number;
  /** Um orçamento típico da plataforma (empresa na média, cliente novo, valor típico) com o mesmo tempo aberto. */
  referencia: number;
  horasAberto: number;
  precisao: "alta" | "media" | "baixa";
  empresa: {
    orcamentos: number;
    fechados: number;
    /** Taxa bruta do ano (fechados ÷ orçamentos), ou null sem orçamentos. */
    taxa: number | null;
    /** A taxa com mais peso no recente (a que entra na conta, antes do encolhimento), ou null sem orçamentos. */
    taxaRecente: number | null;
    orcamentosComNovos: number;
    taxaComNovos: number | null;
  };
  cliente: {
    tipo: TipoDeCliente;
    pedidos: number;
    ticketMedio: number | null;
    ultimaCompra: string | null;
    mesesNoAno: number;
    orcamentos: number;
    fechados: number;
    recentes: number;
    maisNovoEm: string | null;
  };
  valor: number;
  fatores: FatorDaChance[];
  semPeso: Array<{ titulo: string; detalhe: string }>;
  geradoEm: string;
};

export type ChanceGlobal =
  | ChanceGlobalAberta
  | { estado: "fechado"; em: string | null; geradoEm: string }
  /** Marcado como aprovado no painel, mas sem pedido gerado. */
  | { estado: "aprovado"; geradoEm: string }
  | { estado: "recusado"; geradoEm: string }
  | { estado: "sem-conta"; motivo: string; geradoEm: string };

type Variaveis = {
  empresa: number;
  comprou: number;
  fechouOrcamento: number;
  maisNovo: number;
  ticketAcima: number;
  valor: number;
  generico: number;
};

const HORA = 3_600_000;
const sig = (z: number) => 1 / (1 + Math.exp(-z));
const logit = (p: number) => Math.log(p / (1 - p));
const limitar = (valor: number, minimo: number, maximo: number) => Math.min(maximo, Math.max(minimo, valor));
const numero = (valor: unknown) => {
  const convertido = Number(valor);
  return Number.isFinite(convertido) ? convertido : 0;
};
const numeroOuNulo = (valor: unknown) => {
  if (valor === null || valor === undefined || valor === "") return null;
  const convertido = Number(valor);
  return Number.isFinite(convertido) ? convertido : null;
};

/** A taxa recente da empresa como entra na conta: com pouco histórico, puxada para a média da plataforma. */
export function taxaDaEmpresaNaConta(orcamentosRecentes: number, fechadosRecentes: number) {
  return (fechadosRecentes + ENCOLHIMENTO * TAXA_DA_PLATAFORMA) / (orcamentosRecentes + ENCOLHIMENTO);
}

const valorNaConta = (valor: number) => (valor > 0 ? limitar(Math.log10(valor), 1, 5) - 2.5 : 0);

/* Um orçamento típico da plataforma: empresa na média, cliente novo, valor típico. */
const VARIAVEIS_DE_REFERENCIA: Variaveis = {
  empresa: logit(TAXA_DA_PLATAFORMA),
  comprou: 0,
  fechouOrcamento: 0,
  maisNovo: 0,
  ticketAcima: 0,
  valor: valorNaConta(VALOR_TIPICO),
  generico: 0,
};

export function variaveisDaChance(dados: DadosDaChanceGlobal): Variaveis {
  const generico = dados.cliente_generico === true;
  const voltou = !generico && numero(dados.cliente.pedidos) > 0;
  const total = numero(dados.total);
  const ticket = numeroOuNulo(dados.cliente.ticket_medio);
  const taxa = taxaDaEmpresaNaConta(numero(dados.empresa.n_recente), numero(dados.empresa.g_recente));
  return {
    empresa: logit(limitar(taxa, 0.03, 0.97)),
    comprou: voltou ? 1 : 0,
    fechouOrcamento: !generico && numero(dados.cliente.fechados) > 0 ? 1 : 0,
    maisNovo: !generico && dados.cliente.mais_novo_em ? 1 : 0,
    ticketAcima: voltou && ticket && total > 0 && total / ticket > TICKET_ACIMA ? 1 : 0,
    valor: valorNaConta(total),
    generico: generico ? 1 : 0,
  };
}

/** A chance com estas variáveis, para um orçamento aberto há `horas` (menos de 1 hora conta como 1). */
export function pontuarChance(variaveis: Variaveis, horas: number) {
  const p = PESOS_DA_CHANCE;
  const tempo = Math.log(Math.max(1, horas));
  return sig(
    p.intercepto +
      p.marco * tempo +
      p.marco2 * tempo * tempo +
      p.empresa * variaveis.empresa +
      p.empresaXmarco * variaveis.empresa * tempo +
      p.comprou * variaveis.comprou +
      p.fechouOrcamento * variaveis.fechouOrcamento +
      p.maisNovo * variaveis.maisNovo +
      p.ticketAcima * variaveis.ticketAcima +
      p.valor * variaveis.valor +
      p.generico * variaveis.generico
  );
}

const moeda = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL", maximumFractionDigits: 0 });
const porcento = (fracao: number) => `${Math.round(fracao * 100)}%`;
const vezes = (quantidade: number) => `${quantidade} ${quantidade === 1 ? "vez" : "vezes"}`;
const formatoDia = new Intl.DateTimeFormat("pt-BR", { timeZone: "America/Sao_Paulo", day: "2-digit", month: "2-digit" });

export function tempoAberto(horas: number) {
  if (horas < 1) return "menos de 1 hora";
  if (horas < 24) {
    const inteiras = Math.floor(horas);
    return `${inteiras} ${inteiras === 1 ? "hora" : "horas"}`;
  }
  const dias = Math.floor(horas / 24);
  return `${dias} ${dias === 1 ? "dia" : "dias"}`;
}

function haDias(iso: string, agora: number) {
  const dias = Math.floor((agora - Date.parse(iso)) / (24 * HORA));
  if (dias <= 0) return "hoje";
  if (dias === 1) return "ontem";
  if (dias < 60) return `há ${dias} dias`;
  const meses = Math.floor(dias / 30);
  return `há ${meses} meses`;
}

export function chanceGlobal(dados: DadosDaChanceGlobal | null): ChanceGlobal {
  const geradoEm = dados?.agora ?? new Date().toISOString();
  if (!dados) return { estado: "sem-conta", motivo: "Não deu para calcular a chance agora.", geradoEm };
  if (dados.pedido_em) return { estado: "fechado", em: dados.pedido_em, geradoEm };
  const status = dados.status ?? "";
  if (status === "rejected") return { estado: "recusado", geradoEm };
  /* Aprovado no painel sem pedido: só o pedido gerado fecha de verdade. */
  if (["approved", "production", "delivered"].includes(status)) return { estado: "aprovado", geradoEm };
  if (dados.pdv) return { estado: "sem-conta", motivo: "Venda registrada no PDV: não é uma proposta à espera do cliente.", geradoEm };
  if (dados.lista_de_preco) {
    return { estado: "sem-conta", motivo: "Lista de preços não tem um valor fechado para o cliente aceitar, então a conta não se aplica.", geradoEm };
  }

  const agora = Date.parse(dados.agora);
  const horasAberto = Math.max(0, (agora - Date.parse(dados.criado_em)) / HORA);
  const v = variaveisDaChance(dados);
  const chance = pontuarChance(v, horasAberto);
  const naCriacao = pontuarChance(v, 1);
  const sem = (troca: Partial<Variaveis>) => chance - pontuarChance({ ...v, ...troca }, horasAberto);

  const orcamentos = numero(dados.empresa.n);
  const fechados = numero(dados.empresa.g);
  const orcamentosRecentes = numero(dados.empresa.n_recente);
  const fechadosRecentes = numero(dados.empresa.g_recente);
  const orcamentosComNovos = numero(dados.empresa.n_novo);
  const fechadosComNovos = numero(dados.empresa.g_novo);
  const taxa = orcamentos > 0 ? fechados / orcamentos : null;
  const taxaRecente = orcamentosRecentes > 0 ? fechadosRecentes / orcamentosRecentes : null;
  const taxaComNovos = orcamentosComNovos > 0 ? fechadosComNovos / orcamentosComNovos : null;
  const total = numero(dados.total);
  const ticketMedio = numeroOuNulo(dados.cliente.ticket_medio);
  const pedidos = numero(dados.cliente.pedidos);
  const tipo: TipoDeCliente = dados.cliente_generico ? "generico" : pedidos > 0 ? "voltou" : "novo";

  const fatores: FatorDaChance[] = [];

  /* A empresa: o que mais pesa, com o último mês valendo mais. Com poucos
     orçamentos, a média da plataforma completa a conta. */
  fatores.push({
    chave: "empresa",
    titulo:
      taxa === null || taxaRecente === null
        ? "Sua empresa ainda não tem histórico"
        : `Sua empresa anda fechando ${porcento(taxaRecente)} dos orçamentos`,
    detalhe:
      taxa === null || taxaRecente === null
        ? `A conta usa a média das empresas da plataforma (${porcento(TAXA_DA_PLATAFORMA)}) até os orçamentos de vocês terem desfecho.`
        : orcamentos >= 20
          ? `O último mês pesa mais. No ano, ${porcento(taxa)} de ${orcamentos.toLocaleString("pt-BR")} orçamentos; a média das empresas é ${porcento(TAXA_DA_PLATAFORMA)}.`
          : `Só ${orcamentos} ${orcamentos === 1 ? "orçamento" : "orçamentos"} no último ano: a conta completa com a média das empresas (${porcento(TAXA_DA_PLATAFORMA)}).`,
    efeito: sem({ empresa: logit(TAXA_DA_PLATAFORMA) }),
  });

  /* O cliente. */
  if (tipo === "generico") {
    fatores.push({
      chave: "generico",
      titulo: "Cliente sem cadastro próprio",
      detalhe: "Orçamento para “Consumidor final” ou cliente pendente que não vira pedido na hora quase nunca fecha depois.",
      efeito: sem({ generico: 0 }),
    });
  } else if (tipo === "novo") {
    fatores.push({
      chave: "cliente",
      titulo: "Cliente novo",
      detalhe:
        taxaComNovos !== null && orcamentosComNovos >= 5
          ? `Nunca comprou com vocês. Com clientes novos, sua empresa fecha ${porcento(taxaComNovos)} (${orcamentosComNovos} orçamentos no ano).`
          : "Nunca comprou com vocês: a conta usa a taxa da empresa e o valor do orçamento.",
      efeito: null,
    });
  } else {
    fatores.push({
      chave: "cliente",
      titulo: `Já comprou ${vezes(pedidos)} com vocês`,
      detalhe: [
        dados.cliente.ultima_compra ? `Última compra ${haDias(dados.cliente.ultima_compra, agora)}` : null,
        ticketMedio ? `ticket médio ${moeda.format(ticketMedio)}` : null,
      ]
        .filter(Boolean)
        .join(" · ")
        .replace(/^./, (letra) => letra.toUpperCase()),
      efeito: sem({ comprou: 0 }),
    });
  }
  if (v.fechouOrcamento) {
    const anteriores = numero(dados.cliente.orcamentos);
    const aceitos = numero(dados.cliente.fechados);
    fatores.push({
      chave: "fechou-antes",
      titulo: "Já aceitou orçamento de vocês",
      detalhe: `Fechou ${aceitos} de ${anteriores} ${anteriores === 1 ? "orçamento" : "orçamentos"} no último ano.`,
      efeito: sem({ fechouOrcamento: 0 }),
    });
  }
  if (v.maisNovo && dados.cliente.mais_novo_em) {
    fatores.push({
      chave: "mais-novo",
      titulo: "Existe um orçamento mais novo para este cliente",
      detalhe: `Criado em ${formatoDia.format(new Date(dados.cliente.mais_novo_em))}. Quando a proposta é refeita, a anterior costuma ficar para trás.`,
      efeito: sem({ maisNovo: 0 }),
    });
  }
  if (v.ticketAcima && ticketMedio) {
    fatores.push({
      chave: "ticket",
      titulo: "Valor bem acima do que ele costuma comprar",
      detalhe: `${moeda.format(total)} é ${Math.floor(total / ticketMedio)}× o ticket médio dele (${moeda.format(ticketMedio)}).`,
      efeito: sem({ ticketAcima: 0 }),
    });
  }
  if (total > 0) {
    const efeito = sem({ valor: valorNaConta(VALOR_TIPICO) });
    /* Só aparece quando mexe pelo menos 1 ponto: um orçamento de valor comum não precisa de explicação. */
    if (Math.abs(efeito) >= 0.01) {
      fatores.push({
        chave: "valor",
        titulo: `Valor de ${moeda.format(total)}`,
        detalhe:
          total > VALOR_TIPICO
            ? `Orçamentos maiores fecham menos. O valor típico na plataforma é ${moeda.format(VALOR_TIPICO)}.`
            : `Orçamentos menores fecham mais. O valor típico na plataforma é ${moeda.format(VALOR_TIPICO)}.`,
        efeito,
      });
    }
  }
  fatores.push({
    chave: "tempo",
    titulo: horasAberto < 1 ? "Criado agora" : `Aberto há ${tempoAberto(horasAberto)}`,
    detalhe:
      horasAberto < 24
        ? "Um orçamento que acabou de sair tem a chance mais alta. Ela cai a cada dia sem resposta."
        : `Era ${porcento(naCriacao)} na criação. Sem virar pedido, a chance cai a cada dia, primeiro rápido, depois devagar.`,
    efeito: chance - naCriacao,
  });

  /* O que mais mexe vem primeiro; informação sem peso fica no fim. */
  fatores.sort((a, b) => {
    if (a.efeito === null || b.efeito === null) return (a.efeito === null ? 1 : 0) - (b.efeito === null ? 1 : 0);
    return Math.abs(b.efeito) - Math.abs(a.efeito);
  });

  const semPeso: Array<{ titulo: string; detalhe: string }> = [];
  if (tipo === "voltou") {
    const meses = numero(dados.cliente.meses_no_ano);
    semPeso.push({
      titulo: "Recorrência",
      detalhe: `Comprou em ${meses} ${meses === 1 ? "mês" : "meses"} diferentes no último ano, ${vezes(numero(dados.cliente.pedidos_no_ano))} ao todo.`,
    });
  }
  if (tipo !== "generico" && numero(dados.cliente.orcamentos) > 0 && numero(dados.cliente.fechados) === 0) {
    const anteriores = numero(dados.cliente.orcamentos);
    semPeso.push({
      titulo: "Orçamentos anteriores",
      detalhe: `Pediu ${anteriores} ${anteriores === 1 ? "orçamento" : "orçamentos"} no último ano e não fechou nenhum.`,
    });
  }
  if (tipo !== "generico" && numero(dados.cliente.recentes) > 0) {
    const recentes = numero(dados.cliente.recentes);
    semPeso.push({
      titulo: "Na mesma semana",
      detalhe: `${recentes} ${recentes === 1 ? "outro orçamento" : "outros orçamentos"} para ele nos 7 dias antes deste.`,
    });
  }

  return {
    estado: "aberto",
    chance,
    naCriacao,
    amanha: pontuarChance(v, horasAberto + 24),
    emUmaSemana: pontuarChance(v, horasAberto + 24 * 7),
    emUmMes: pontuarChance(v, horasAberto + 24 * 30),
    referencia: pontuarChance(VARIAVEIS_DE_REFERENCIA, horasAberto),
    horasAberto,
    precisao: orcamentos >= 60 ? "alta" : orcamentos >= 20 ? "media" : "baixa",
    empresa: { orcamentos, fechados, taxa, taxaRecente, orcamentosComNovos, taxaComNovos },
    cliente: {
      tipo,
      pedidos,
      ticketMedio,
      ultimaCompra: dados.cliente.ultima_compra,
      mesesNoAno: numero(dados.cliente.meses_no_ano),
      orcamentos: numero(dados.cliente.orcamentos),
      fechados: numero(dados.cliente.fechados),
      recentes: numero(dados.cliente.recentes),
      maisNovoEm: dados.cliente.mais_novo_em,
    },
    valor: total,
    fatores,
    semPeso,
    geradoEm,
  };
}
