import { createHmac, timingSafeEqual } from "node:crypto";

/**
 * As regras da assinatura da Ktalogu vendida pela Cakto, sem banco: o que um
 * aviso da Cakto significa e o que ele faz com o acesso. A rota e o relógio de
 * reprocessamento leem daqui, e os testes também.
 *
 * Combinado com o Natan (29/09/2026):
 * - só o produto novo e as duas ofertas dele valem; o plano sai do código da
 *   oferta, nunca do nome;
 * - pagamento libera até o fim do período pago, e o acesso fecha no dia
 *   seguinte se a renovação não entrar;
 * - cancelar a assinatura mantém o acesso até o fim do que já foi pago;
 * - reembolso e chargeback cortam na hora.
 */

export const PRODUTO_KTALOGU = "cd514a06-7b3e-4319-bab3-d7f8f107d0de";

export type PlanoCakto = "mensal" | "anual";

export const OFERTAS_KTALOGU: Record<string, { plano: PlanoCakto; dias: number; preco: number; nome: string }> = {
  b9jcqix: { plano: "mensal", dias: 30, preco: 69.9, nome: "Ktalogu - Mensal" },
  gvndd3a: { plano: "anual", dias: 365, preco: 597, nome: "Ktalogu - Anual" },
};

/**
 * Os produtos dos planos antigos (R$ 37 e R$ 147), ainda atendidos pelo webhook
 * do LazzerPro. A rota nova não libera nada por eles: só os conhece para ler o
 * histórico e para cancelar a renovação de quem migra para o plano novo.
 */
export const PRODUTOS_ANTIGOS: Record<string, PlanoCakto> = {
  "aa9796b5-6abd-4c5c-88eb-2c8361b9f42e": "anual",
  "50777bc2-1b7c-4d6f-803c-41a12c6e5f97": "anual",
  "16d34667-ef62-4c43-8d46-3ab93fda7385": "mensal",
};

export function ehProdutoDaKtalogu(produtoId: string) {
  return produtoId === PRODUTO_KTALOGU || Boolean(PRODUTOS_ANTIGOS[produtoId]);
}

/** Mensal ou anual: pela oferta nos planos novos, pelo produto nos antigos. */
export function planoDoPedido(produtoId: string, ofertaId: string): PlanoCakto | null {
  if (produtoId === PRODUTO_KTALOGU) return OFERTAS_KTALOGU[ofertaId]?.plano ?? null;
  return PRODUTOS_ANTIGOS[produtoId] ?? null;
}

export type SituacaoCakto = "ativa" | "atrasada" | "pausada" | "cancelada" | "reembolsada" | "contestada";

export type AvisoCakto = {
  evento: string;
  /** Evento + id do pedido: a mesma entrega chega mais de uma vez, o mesmo par não conta duas. */
  chave: string;
  pedidoId: string | null;
  status: string;
  pagoEm: Date | null;
  produtoId: string;
  ofertaId: string;
  assinaturaId: string | null;
  clienteId: string | null;
  email: string;
  nome: string;
  callback: string | null;
  proximoPagamento: Date | null;
  refId: string | null;
  metodo: string | null;
  parcelas: number | null;
  valor: number | null;
  criadoEm: Date | null;
  reembolsadoEm: Date | null;
  contestadoEm: Date | null;
  /** Pix copia-e-cola e até quando ele vale (aviso `pix_gerado`). */
  pixCodigo: string | null;
  pixExpiraEm: Date | null;
};

export type AcaoCakto =
  | { tipo: "liberar"; pagoAte: Date }
  | { tipo: "marcar"; situacao: "atrasada" | "pausada" | "cancelada" }
  | { tipo: "cortar"; situacao: "reembolsada" | "contestada" }
  | { tipo: "ignorar"; motivo: string };

type Registro = Record<string, unknown>;

const DIA_MS = 24 * 60 * 60 * 1000;
/* Brasília não tem horário de verão desde 2019. */
const BRASILIA_MS = 3 * 60 * 60 * 1000;

function registro(valor: unknown): Registro | null {
  return valor && typeof valor === "object" && !Array.isArray(valor) ? (valor as Registro) : null;
}

function texto(valor: unknown) {
  if (typeof valor === "number" && Number.isFinite(valor)) return String(valor);
  return typeof valor === "string" ? valor.trim() : "";
}

function data(valor: unknown): Date | null {
  const bruto = texto(valor);
  if (!bruto) return null;
  const lida = new Date(bruto);
  return Number.isFinite(lida.getTime()) ? lida : null;
}

function numero(valor: unknown): number | null {
  const lido = typeof valor === "number" ? valor : Number.parseFloat(texto(valor));
  return Number.isFinite(lido) ? Math.round(lido * 100) / 100 : null;
}

export function normalizarEmail(valor: unknown) {
  const email = texto(valor).toLowerCase();
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : "";
}

/**
 * Lê o corpo de uma entrega. Devolve null quando não é um aviso de pedido que
 * a gente entende (abandono de checkout, webhook V2 com lista, lixo).
 */
export function lerAviso(corpo: unknown): AvisoCakto | null {
  const raiz = registro(corpo);
  if (!raiz) return null;
  const evento = texto(raiz.event).toLowerCase();
  const dados = registro(raiz.data);
  if (!evento || !dados) return null;

  const pedidoId = texto(dados.id) || null;
  if (!pedidoId) return null;

  const produto = registro(dados.product);
  const oferta = registro(dados.offer);
  const assinatura = registro(dados.subscription);
  const cliente = registro(dados.customer);
  const clienteDaAssinatura = registro(assinatura?.customer);

  return {
    evento,
    chave: `${evento}:${pedidoId}`,
    pedidoId,
    status: texto(dados.status).toLowerCase(),
    pagoEm: data(dados.paidAt),
    produtoId: texto(produto?.id) || texto(assinatura?.product),
    ofertaId: texto(oferta?.id) || texto(assinatura?.offer),
    assinaturaId: texto(assinatura?.id) || null,
    clienteId: texto(cliente?.id) || null,
    email: normalizarEmail(cliente?.email) || normalizarEmail(clienteDaAssinatura?.email),
    nome: texto(cliente?.name) || texto(clienteDaAssinatura?.name),
    callback: texto(dados.callback) || null,
    proximoPagamento: data(assinatura?.next_payment_date),
    refId: texto(dados.refId) || null,
    metodo: texto(dados.paymentMethod) || null,
    parcelas: numero(dados.installments),
    valor: numero(dados.amount) ?? numero(dados.baseAmount),
    criadoEm: data(dados.createdAt),
    reembolsadoEm: data(dados.refundedAt),
    contestadoEm: data(dados.chargedbackAt),
    pixCodigo: texto(registro(dados.pix)?.qrCode) || null,
    pixExpiraEm: data(registro(dados.pix)?.expirationDate),
  };
}

const LIBERAM = new Set([
  "purchase_approved",
  "subscription_created",
  "subscription_renewed",
  "subscription_late_recovered",
  "subscription_resumed",
]);

/**
 * Até quando o pagamento vale. A Cakto manda a data da próxima cobrança
 * quando a assinatura é recorrente no cartão; no Pix ela vem vazia e o período
 * conta da data do pagamento. Uma data fora do razoável (antes do pagamento,
 * ou bem depois de um período) não é aceita: aí vale a conta pelo plano.
 */
export function pagoAteDoAviso(aviso: Pick<AvisoCakto, "pagoEm" | "proximoPagamento">, dias: number): Date | null {
  if (!aviso.pagoEm) return null;
  const pelaConta = new Date(aviso.pagoEm.getTime() + dias * DIA_MS);
  const proximo = aviso.proximoPagamento;
  if (!proximo) return pelaConta;
  const minimo = aviso.pagoEm.getTime() + DIA_MS;
  const maximo = pelaConta.getTime() + 10 * DIA_MS;
  return proximo.getTime() >= minimo && proximo.getTime() <= maximo ? proximo : pelaConta;
}

export function decidir(aviso: AvisoCakto): AcaoCakto {
  if (aviso.produtoId !== PRODUTO_KTALOGU) return { tipo: "ignorar", motivo: `Produto fora da Ktalogu: ${aviso.produtoId || "sem produto"}.` };
  const oferta = OFERTAS_KTALOGU[aviso.ofertaId];
  if (!oferta) return { tipo: "ignorar", motivo: `Oferta desconhecida: ${aviso.ofertaId || "sem oferta"}.` };

  if (LIBERAM.has(aviso.evento)) {
    if (aviso.status !== "paid") return { tipo: "ignorar", motivo: `Pedido ainda não pago (${aviso.status || "sem status"}).` };
    const pagoAte = pagoAteDoAviso(aviso, oferta.dias);
    if (!pagoAte) return { tipo: "ignorar", motivo: "Pedido pago sem data de pagamento." };
    return { tipo: "liberar", pagoAte };
  }

  switch (aviso.evento) {
    case "subscription_renewal_refused":
    case "subscription_late":
      return { tipo: "marcar", situacao: "atrasada" };
    case "subscription_paused":
      return { tipo: "marcar", situacao: "pausada" };
    case "subscription_canceled":
      return { tipo: "marcar", situacao: "cancelada" };
    case "refund":
      return { tipo: "cortar", situacao: "reembolsada" };
    case "chargeback":
      return { tipo: "cortar", situacao: "contestada" };
    default:
      return { tipo: "ignorar", motivo: `Evento só registrado: ${aviso.evento}.` };
  }
}

/** O último segundo do dia, no horário de Brasília, em que `momento` cai. */
export function fimDoDiaEmBrasilia(momento: Date): Date {
  const local = new Date(momento.getTime() - BRASILIA_MS);
  const fim = Date.UTC(local.getUTCFullYear(), local.getUTCMonth(), local.getUTCDate(), 23, 59, 59);
  return new Date(fim + BRASILIA_MS);
}

/**
 * O vencimento que a linha de `users_app` recebe quando um pagamento entra:
 * o fim do dia do período pago, mais os dias de crédito que a assinatura
 * herdou na troca de plano, sem nunca encurtar um acesso que já valia mais
 * (tempo pago antes, ou dado à mão). Reprocessar o mesmo aviso dá o mesmo
 * número: nada soma duas vezes.
 */
export function vencimentoAoLiberar(atual: Date | null, atualValia: boolean, pagoAte: Date, diasDeCredito = 0): Date {
  const novo = new Date(fimDoDiaEmBrasilia(pagoAte).getTime() + Math.max(0, diasDeCredito) * DIA_MS);
  if (atual && atualValia && atual.getTime() > novo.getTime()) return atual;
  return novo;
}

/**
 * Troca de plano: os dias que ainda faltavam no acesso viram crédito da
 * assinatura nova. Dia começado conta inteiro — o cliente pagou por ele.
 */
export function diasQueFaltam(acessoAte: Date | null, agora = new Date()): number {
  if (!acessoAte) return 0;
  const falta = acessoAte.getTime() - agora.getTime();
  return falta > 0 ? Math.ceil(falta / DIA_MS) : 0;
}

export type PagamentoCakto = {
  pedido_id: string;
  ref_id: string | null;
  assinatura_id: string | null;
  produto_id: string;
  oferta_id: string | null;
  plano: PlanoCakto | null;
  valor: number | null;
  metodo: string | null;
  parcelas: number | null;
  status: string;
  criado_em: string | null;
  pago_em: string | null;
  reembolsado_em: string | null;
  contestado_em: string | null;
  pix_codigo?: string | null;
  pix_expira_em?: string | null;
};

/**
 * Um pedido lido da API de pedidos da Cakto (`/public_api/orders/`), que tem
 * outra forma: a assinatura vem só como id e a oferta só dentro do link do
 * checkout. Devolve o e-mail de quem pagou junto, para achar a conta.
 */
export function lerPedidoDaApi(bruto: unknown): { pagamento: PagamentoCakto; email: string } | null {
  const pedido = registro(bruto);
  if (!pedido) return null;
  const pedidoId = texto(pedido.id);
  const produtoId = texto(registro(pedido.product)?.id);
  if (!pedidoId || !ehProdutoDaKtalogu(produtoId)) return null;
  const email = normalizarEmail(registro(pedido.customer)?.email);
  if (!email) return null;
  const ofertaId = (texto(pedido.checkoutUrl).match(/pay\.cakto\.com\.br\/([A-Za-z0-9]+)/)?.[1] ?? "") || null;
  const iso = (valor: unknown) => data(valor)?.toISOString() ?? null;
  return {
    email,
    pagamento: {
      pedido_id: pedidoId,
      ref_id: texto(pedido.refId) || null,
      assinatura_id: texto(pedido.subscription) || null,
      produto_id: produtoId,
      oferta_id: ofertaId,
      plano: planoDoPedido(produtoId, ofertaId ?? ""),
      valor: numero(pedido.amount) ?? numero(pedido.baseAmount),
      metodo: texto(pedido.paymentMethod) || null,
      parcelas: numero(pedido.installments),
      status: texto(pedido.status).toLowerCase() || "desconhecido",
      criado_em: iso(pedido.createdAt),
      pago_em: iso(pedido.paidAt),
      reembolsado_em: iso(pedido.refundedAt),
      contestado_em: iso(pedido.chargedbackAt),
    },
  };
}

/** A linha do histórico que um aviso de pedido descreve (sem o e-mail da conta). */
export function pagamentoDoAviso(aviso: AvisoCakto): PagamentoCakto | null {
  if (!aviso.pedidoId || !ehProdutoDaKtalogu(aviso.produtoId)) return null;
  const iso = (d: Date | null) => (d ? d.toISOString() : null);
  return {
    pedido_id: aviso.pedidoId,
    ref_id: aviso.refId,
    assinatura_id: aviso.assinaturaId,
    produto_id: aviso.produtoId,
    oferta_id: aviso.ofertaId || null,
    plano: planoDoPedido(aviso.produtoId, aviso.ofertaId),
    valor: aviso.valor,
    metodo: aviso.metodo,
    parcelas: aviso.parcelas,
    status: aviso.status || "desconhecido",
    criado_em: iso(aviso.criadoEm),
    pago_em: iso(aviso.pagoEm),
    reembolsado_em: iso(aviso.reembolsadoEm),
    contestado_em: iso(aviso.contestadoEm),
    /* Só o Pix que ainda espera pagamento guarda o código; os outros avisos
       do mesmo pedido não apagam o que o `pix_gerado` gravou. */
    ...(aviso.pixCodigo ? { pix_codigo: aviso.pixCodigo, pix_expira_em: iso(aviso.pixExpiraEm) } : {}),
  };
}

/* ------------------------------------------------------------------ */
/* Origem da entrega                                                   */
/* ------------------------------------------------------------------ */

const TOLERANCIA_S = 5 * 60;

function iguais(a: string, b: string) {
  const x = Buffer.from(a);
  const y = Buffer.from(b);
  return x.length === y.length && timingSafeEqual(x, y);
}

/**
 * `X-Cakto-Signature: v1=<hmac-sha256 de "{timestamp}.{corpo cru}">`. Entrega
 * com mais de 5 minutos é recusada: um corpo capturado não serve depois.
 */
export function assinaturaValida(corpoCru: string, timestamp: string, cabecalho: string, segredo: string, agoraS = Date.now() / 1000) {
  if (!segredo || !timestamp || !cabecalho) return false;
  const quando = Number(timestamp);
  if (!Number.isFinite(quando) || Math.abs(agoraS - quando) > TOLERANCIA_S) return false;
  const esperado = `v1=${createHmac("sha256", segredo).update(`${timestamp}.`).update(corpoCru).digest("hex")}`;
  return cabecalho
    .split(",")
    .map((parte) => parte.trim())
    .some((parte) => iguais(parte, esperado));
}

/** O `secret` que também vem no corpo: só vale quando a entrega não trouxe a assinatura no cabeçalho. */
export function segredoDoCorpoValido(corpo: unknown, segredo: string) {
  const recebido = texto(registro(corpo)?.secret);
  return Boolean(segredo && recebido && iguais(recebido, segredo));
}

/* ------------------------------------------------------------------ */
/* Marca da empresa no link                                            */
/* ------------------------------------------------------------------ */

/*
 * O link de pagamento do painel leva `?callback=<marca>`, e a Cakto devolve a
 * marca no aviso. Com ela a compra cai na empresa certa mesmo com outro
 * e-mail. A marca é assinada: sem isso, alguém pagaria "em nome" de outra
 * empresa e depois pediria o reembolso, derrubando o acesso dela.
 * Só letras, números, ponto e hífen, como a Cakto aceita.
 */
function chaveDaMarca(segredo: string) {
  return createHmac("sha256", segredo).update("ktalogu-callback-v1").digest();
}

function selo(donoId: string, segredo: string) {
  return createHmac("sha256", chaveDaMarca(segredo)).update(donoId).digest("base64url").slice(0, 22);
}

export function marcaDaEmpresa(donoId: string, segredo: string) {
  const limpo = donoId.replace(/-/g, "").toLowerCase();
  if (!/^[0-9a-f]{32}$/.test(limpo) || !segredo) return null;
  return `k1.${limpo}.${selo(limpo, segredo)}`;
}

export function donoDaMarca(marca: string | null | undefined, segredo: string): string | null {
  const partes = String(marca ?? "").split(".");
  if (partes.length !== 3 || partes[0] !== "k1" || !segredo) return null;
  const [, limpo, recebido] = partes;
  if (!/^[0-9a-f]{32}$/.test(limpo) || !iguais(recebido, selo(limpo, segredo))) return null;
  return `${limpo.slice(0, 8)}-${limpo.slice(8, 12)}-${limpo.slice(12, 16)}-${limpo.slice(16, 20)}-${limpo.slice(20)}`;
}

export function linkDePagamento(ofertaId: string, marca: string | null) {
  const base = `https://pay.cakto.com.br/${ofertaId}`;
  return marca ? `${base}?callback=${encodeURIComponent(marca)}` : base;
}
