import type { SupabaseClient } from "@supabase/supabase-js";

import { lerVencimento, statusLibera } from "@/lib/assinatura";
import {
  decidir,
  diasQueFaltam,
  donoDaMarca,
  ehProdutoDaKtalogu,
  PRODUTOS_ANTIGOS,
  fimDoDiaEmBrasilia,
  lerAviso,
  OFERTAS_KTALOGU,
  pagamentoDoAviso,
  pagoAteDoAviso,
  planoDoPedido,
  vencimentoAoLiberar,
  type AvisoCakto,
  type PagamentoCakto,
  type SituacaoCakto,
} from "@/lib/assinatura-cakto";
import { diaEmBrasilia } from "@/lib/avisos-da-assinatura";
import { avisarSemEsperar, enviarBoasVindas } from "@/lib/avisos-da-assinatura-servidor";
import { lerAbandono } from "@/lib/automacoes-da-assinatura";
import { avisarODono, guardarAbandono } from "@/lib/automacoes-da-assinatura-servidor";
import { assinaturasDoEmail, caktoConfigurada, cancelarNaCakto } from "@/lib/cakto-api";
import { emailDeCobrancaDoFuncionario } from "@/lib/assinatura-do-funcionario";
import { esquecerAcesso } from "@/lib/assinatura-memoria";
import { emailDeCobranca, esquecerAssinaturaDaEmpresa } from "@/lib/fiscal/servidor/assinatura-da-empresa";

/**
 * O que um aviso da Cakto faz no banco. As regras moram em
 * `lib/assinatura-cakto`; aqui é achar a conta, gravar e registrar.
 *
 * Todo aviso passa primeiro por `subscription_webhook_events` (provedor
 * "cakto", chave = evento + pedido). Aviso que falha fica "failed" e o relógio
 * tenta de novo; aviso já feito não é refeito.
 */

type Admin = SupabaseClient;

const DIA_MS = 24 * 60 * 60 * 1000;

type LinhaDoAcesso = { id: string; email: string; status: string | null; expires_at: string | null };
type LinhaDaAssinatura = {
  id: string;
  email: string;
  dono_id: string | null;
  situacao: SituacaoCakto;
  pago_ate: string | null;
  ultimo_pedido_id: string | null;
  dias_de_credito: number | null;
};

export type Resultado = { status: "processed" | "ignored"; detalhe: string };

/*
 * Uma fila só para tudo que mexe na assinatura. A Cakto manda dois avisos
 * quase juntos para a mesma compra ("compra aprovada" e "assinatura criada");
 * aplicados ao mesmo tempo, os dois viam a conta sem a assinatura nova e a
 * troca de plano podia contar como crédito o próprio período recém-pago. Um
 * de cada vez resolve, e o volume (poucos avisos por dia) não sente.
 */
declare global {
  var __filaDaCakto: Promise<unknown> | undefined;
}

export function naFila<T>(tarefa: () => Promise<T>): Promise<T> {
  const anterior = globalThis.__filaDaCakto ?? Promise.resolve();
  const esta = anterior.catch(() => undefined).then(tarefa);
  globalThis.__filaDaCakto = esta.catch(() => undefined);
  return esta;
}

function segredoDoWebhook() {
  return String(process.env.CAKTO_WEBHOOK_SECRET || "").trim();
}

function origemDoPainel() {
  if (process.env.NODE_ENV === "production") return "https://admin.ktalogu.com.br";
  return String(process.env.NEXT_PUBLIC_ADMIN_BASE_URL || process.env.ADMIN_BASE_URL || "http://localhost:3000").replace(/\/$/, "");
}

/** Tira o segredo do corpo antes de guardar: ele não precisa morar no banco. */
export function corpoSemSegredo(corpo: unknown) {
  if (!corpo || typeof corpo !== "object" || Array.isArray(corpo)) return corpo;
  const copia = { ...(corpo as Record<string, unknown>) };
  delete copia.secret;
  return copia;
}

/* ------------------------------------------------------------------ */
/* Registro das entregas                                               */
/* ------------------------------------------------------------------ */

export type Entrega = { id: string; jaFeita: boolean };

export async function registrarEntrega(admin: Admin, chave: string, corpo: unknown, aviso: AvisoCakto | null): Promise<Entrega> {
  const inserido = await admin
    .from("subscription_webhook_events")
    .insert({
      provider: "cakto",
      delivery_hash: chave,
      event_type: aviso?.evento ?? null,
      external_id: aviso?.pedidoId ?? null,
      customer_email: aviso?.email || null,
      raw_body: null,
      payload: corpoSemSegredo(corpo) ?? {},
      status: "received",
    })
    .select("id")
    .single();

  if (!inserido.error) return { id: String(inserido.data.id), jaFeita: false };

  if (inserido.error.code !== "23505") throw new Error(`Não foi possível registrar o aviso: ${inserido.error.message}`);
  const { data, error } = await admin
    .from("subscription_webhook_events")
    .select("id,status,updated_at")
    .eq("provider", "cakto")
    .eq("delivery_hash", chave)
    .single();
  if (error) throw new Error(`Não foi possível ler o aviso repetido: ${error.message}`);
  /* "received" recente é a mesma entrega sendo aplicada agora (a Cakto
     reenviou antes de a primeira terminar): não aplica duas vezes ao mesmo
     tempo. Se ela travar, o relógio pega depois de um minuto. */
  const emAndamento = data.status === "received" && Date.now() - new Date(String(data.updated_at)).getTime() < 2 * 60_000;
  return { id: String(data.id), jaFeita: data.status === "processed" || data.status === "ignored" || emAndamento };
}

export async function marcarEntrega(admin: Admin, id: string, status: "processed" | "ignored" | "failed", detalhe: string) {
  const { data } = await admin.from("subscription_webhook_events").select("tentativas").eq("id", id).maybeSingle();
  await admin
    .from("subscription_webhook_events")
    .update({
      status,
      processing_details: detalhe.slice(0, 2000),
      processed_at: new Date().toISOString(),
      updated_at: new Date().toISOString(),
      tentativas: Number(data?.tentativas ?? 0) + 1,
    })
    .eq("id", id);
}

/* ------------------------------------------------------------------ */
/* De quem é a compra                                                  */
/* ------------------------------------------------------------------ */

async function assinaturaGuardada(admin: Admin, assinaturaId: string | null): Promise<LinhaDaAssinatura | null> {
  if (!assinaturaId) return null;
  const { data, error } = await admin
    .from("assinaturas_cakto")
    .select("id,email,dono_id,situacao,pago_ate,ultimo_pedido_id,dias_de_credito")
    .eq("cakto_subscription_id", assinaturaId)
    .maybeSingle();
  if (error) throw new Error(`Não foi possível ler a assinatura guardada: ${error.message}`);
  return (data as LinhaDaAssinatura | null) ?? null;
}

/**
 * A conta que o pagamento alimenta, nesta ordem:
 * 1. a assinatura já conhecida (os avisos seguintes nunca trocam de conta);
 * 2. a marca da empresa no link do painel;
 * 3. o e-mail de quem pagou — e, se for funcionário, o da empresa dele.
 */
async function contaDoAviso(admin: Admin, aviso: AvisoCakto, guardada: LinhaDaAssinatura | null) {
  if (guardada) return { email: guardada.email, donoId: guardada.dono_id };

  const donoId = donoDaMarca(aviso.callback, segredoDoWebhook());
  if (donoId) {
    const email = String((await emailDeCobranca(admin, donoId)) ?? "").trim().toLowerCase();
    if (email) return { email, donoId };
  }

  if (!aviso.email) return null;
  const daEmpresa = await emailDeCobrancaDoFuncionario(admin, aviso.email).catch(() => null);
  return { email: daEmpresa ?? aviso.email, donoId: null };
}

async function acessoDoEmail(admin: Admin, email: string): Promise<LinhaDoAcesso | null> {
  const { data, error } = await admin.from("users_app").select("id,email,status,expires_at").eq("email", email).maybeSingle();
  if (error) throw new Error(`Não foi possível ler o acesso: ${error.message}`);
  return (data as LinhaDoAcesso | null) ?? null;
}

async function usuarioDeLogin(admin: Admin, email: string) {
  const { data, error } = await admin.rpc("get_auth_user_id_by_email", { p_email: email });
  if (error) throw new Error(`Não foi possível procurar o login: ${error.message}`);
  if (typeof data === "string" && data) return { id: data, criado: false };

  const criado = await admin.auth.admin.createUser({ email, email_confirm: true });
  if (criado.error || !criado.data.user?.id) throw new Error(`Não foi possível criar o login: ${criado.error?.message ?? "sem id"}`);
  return { id: criado.data.user.id, criado: true };
}

/* ------------------------------------------------------------------ */
/* Aplicar                                                             */
/* ------------------------------------------------------------------ */

async function guardarAssinatura(
  admin: Admin,
  aviso: AvisoCakto,
  conta: { email: string; donoId: string | null },
  guardada: LinhaDaAssinatura | null,
  mudanca: { situacao: SituacaoCakto; pagoAte?: Date | null; diasDeCredito?: number; anterioresResolvidas?: boolean }
) {
  if (!aviso.assinaturaId) return;
  const oferta = OFERTAS_KTALOGU[aviso.ofertaId];
  const agora = new Date().toISOString();
  const campos: Record<string, unknown> = {
    cakto_subscription_id: aviso.assinaturaId,
    email: conta.email,
    oferta_id: aviso.ofertaId,
    produto_id: aviso.produtoId,
    plano: oferta.plano,
    situacao: mudanca.situacao,
    ultimo_pedido_id: aviso.pedidoId,
    atualizada_em: agora,
  };
  if (aviso.metodo) campos.metodo = aviso.metodo;
  if (aviso.proximoPagamento) campos.proxima_cobranca = aviso.proximoPagamento.toISOString();
  if (mudanca.diasDeCredito !== undefined) campos.dias_de_credito = mudanca.diasDeCredito;
  if (mudanca.anterioresResolvidas !== undefined) campos.anteriores_resolvidas = mudanca.anterioresResolvidas;
  if (aviso.clienteId) campos.cakto_customer_id = aviso.clienteId;
  if (conta.donoId) campos.dono_id = conta.donoId;
  if (mudanca.pagoAte !== undefined) campos.pago_ate = mudanca.pagoAte ? mudanca.pagoAte.toISOString() : null;
  if (mudanca.situacao === "cancelada") campos.cancelada_em = agora;
  if (mudanca.situacao === "ativa") campos.cancelada_em = null;

  const gravado = guardada
    ? await admin.from("assinaturas_cakto").update(campos).eq("id", guardada.id)
    : await admin.from("assinaturas_cakto").upsert(campos, { onConflict: "cakto_subscription_id" });
  if (gravado.error) throw new Error(`Não foi possível guardar a assinatura: ${gravado.error.message}`);
}

async function liberar(admin: Admin, aviso: AvisoCakto, pagoAte: Date): Promise<Resultado> {
  const guardada = await assinaturaGuardada(admin, aviso.assinaturaId);
  const conta = await contaDoAviso(admin, aviso, guardada);
  if (!conta) return { status: "ignored", detalhe: "Pagamento sem e-mail e sem marca da empresa: não há conta para liberar." };
  const oferta = OFERTAS_KTALOGU[aviso.ofertaId];

  /* Aviso velho chegando depois de um novo (reprocessamento) não volta a
     assinatura para "ativa" nem encurta o período já pago. */
  const pagoAteGuardado = guardada?.pago_ate ? new Date(guardada.pago_ate) : null;
  /* "Avança" é pagar outro período, não o mesmo com segundos de diferença:
     a mesma cobrança pode chegar em avisos com horários ligeiramente distintos. */
  const avanca = !pagoAteGuardado || pagoAte.getTime() > pagoAteGuardado.getTime() + DIA_MS;
  const pagoAteFinal = avanca ? pagoAte : pagoAteGuardado;
  /* O pedido que foi reembolsado não reabre nada, chegue quando chegar. Só um
     pagamento novo, que paga outro período, reabre. */
  if (guardada && (guardada.situacao === "reembolsada" || guardada.situacao === "contestada")) {
    if (!avanca || aviso.pedidoId === guardada.ultimo_pedido_id) {
      return { status: "processed", detalhe: `Pedido ou período já ${guardada.situacao}; nada liberado.` };
    }
  }
  const acesso = await acessoDoEmail(admin, conta.email);
  const atual = lerVencimento(acesso?.expires_at);
  const atualValia = Boolean(atual && atual.getTime() > Date.now() && statusLibera(acesso?.status).libera);

  /* Assinatura nova numa conta que ainda tinha dias (troca de plano, compra
     repetida, cliente do plano antigo migrando): os dias que faltavam viram
     crédito desta assinatura, e as anteriores são canceladas na Cakto para
     não cobrarem em dobro. O crédito é calculado uma vez só, na primeira vez
     que a assinatura aparece. */
  const nova = !guardada;
  const diasDeCredito = nova ? diasQueFaltam(atualValia ? atual : null) : Math.max(0, Number(guardada?.dias_de_credito ?? 0));
  await guardarAssinatura(admin, aviso, conta, guardada, {
    situacao: avanca || !guardada ? "ativa" : guardada.situacao,
    pagoAte: pagoAteFinal,
    ...(nova ? { diasDeCredito, anterioresResolvidas: false } : {}),
  });

  const login = await usuarioDeLogin(admin, conta.email);
  if (!login.criado) {
    /* O fluxo antigo banía o login no bloqueio; pagar tem que destrancar. */
    const { error } = await admin.auth.admin.updateUserById(login.id, { ban_duration: "none" });
    if (error) throw new Error(`Não foi possível destrancar o login: ${error.message}`);
  }

  const vence = vencimentoAoLiberar(atual, atualValia, pagoAteFinal ?? pagoAte, diasDeCredito);

  const campos = {
    email: conta.email,
    status: "active",
    expires_at: vence.toISOString(),
    plan: oferta.plano,
    plano: oferta.plano,
    plan_name: oferta.nome,
    updated_at: new Date().toISOString(),
  };
  const gravado = acesso
    ? await admin.from("users_app").update(campos).eq("id", acesso.id)
    : await admin.from("users_app").insert({ ...campos, name: aviso.nome || null });
  if (gravado.error) throw new Error(`Não foi possível gravar o acesso: ${gravado.error.message}`);

  /* Login novo: um e-mail só de boas-vindas, com o botão de criar a senha. Se
     o e-mail da Ktalogu não estiver ligado (ou falhar), vai o do Supabase. */
  let emailDeSenha = "";
  let boasVindas = false;
  if (login.criado) {
    const redirectTo = `${origemDoPainel()}/auth/callback?next=/reset-password`;
    boasVindas = await enviarBoasVindas(admin, conta.email, { plano: oferta.plano, venceEm: vence, redirectTo }).catch(() => false);
    if (boasVindas) {
      emailDeSenha = " Boas-vindas com o link de criar a senha enviado.";
    } else {
      const { error } = await admin.auth.resetPasswordForEmail(conta.email, { redirectTo });
      emailDeSenha = error ? ` E-mail de senha falhou: ${error.message}.` : " E-mail para criar a senha enviado (Supabase).";
    }
  }

  esquecerAcesso(conta.email);
  esquecerAssinaturaDaEmpresa();
  if ((avanca || nova) && !boasVindas) avisarSemEsperar(admin, conta.email, "pagamento-confirmado", diaEmBrasilia(vence));

  /* Cancelar as anteriores fala com a Cakto e pode passar do tempo que ela
     espera pela resposta. Roda sem segurar o aviso; se falhar, o relógio
     termina (a assinatura fica com `anteriores_resolvidas = false`). */
  if (nova && aviso.assinaturaId) {
    const assinaturaId = aviso.assinaturaId;
    /* Na fila, depois deste aviso: não roda junto com outro aviso da mesma conta. */
    void naFila(() => resolverAnteriores(admin, assinaturaId)).catch((erro) =>
      console.error("[cakto] anteriores ficaram para o relógio", assinaturaId, erro instanceof Error ? erro.message : erro)
    );
  }

  return {
    status: "processed",
    detalhe: `Acesso de ${conta.email} liberado até ${vence.toISOString()} (${oferta.plano}${diasDeCredito ? `, +${diasDeCredito} dias de crédito` : ""}).${emailDeSenha}`,
  };
}

/* ------------------------------------------------------------------ */
/* Troca de plano: as assinaturas anteriores da conta                  */
/* ------------------------------------------------------------------ */

const AINDA_COBRAM = new Set(["active", "late", "paused", "trial", "inactive"]);

/**
 * Acha e cancela as assinaturas que a nova substitui: as nossas (planos novos)
 * e as dos planos antigos na Cakto, pelo e-mail da conta. Cada uma vira uma
 * linha "cancelada" com `cancelar_na_cakto`, e o cancelamento é tentado na
 * hora; o que não der, o relógio tenta de novo.
 */
export async function resolverAnteriores(admin: Admin, assinaturaNovaId: string) {
  const { data: nova, error } = await admin
    .from("assinaturas_cakto")
    .select("id,email,cakto_subscription_id,anteriores_resolvidas,criada_em")
    .eq("cakto_subscription_id", assinaturaNovaId)
    .maybeSingle();
  if (error) throw new Error(error.message);
  if (!nova || nova.anteriores_resolvidas) return { canceladas: 0, nossas: 0 };
  const email = String(nova.email);
  const criadaEm = String(nova.criada_em);
  const agora = new Date().toISOString();

  /* Só as mais antigas que esta. Uma mais nova ainda de pé é quem manda: ela
     cancela esta quando for a vez dela, e esta não cancela aquela. */
  const { data: nossas, error: erroNossas } = await admin
    .from("assinaturas_cakto")
    .update({ situacao: "cancelada", cancelada_em: agora, cancelar_na_cakto: true, substituida_por: assinaturaNovaId, atualizada_em: agora })
    .eq("email", email)
    .neq("cakto_subscription_id", assinaturaNovaId)
    .lt("criada_em", criadaEm)
    .in("situacao", ["ativa", "atrasada", "pausada"])
    .select("cakto_subscription_id");
  if (erroNossas) throw new Error(erroNossas.message);

  /* As da Cakto que ainda não conhecemos (planos antigos, ou compra que não
     passou por aqui). Sem a chave da API, só as nossas são canceladas. */
  if (caktoConfigurada()) {
    const { data: conhecidas } = await admin.from("assinaturas_cakto").select("cakto_subscription_id").eq("email", email);
    const jaConhecidas = new Set(((conhecidas ?? []) as Array<{ cakto_subscription_id: string }>).map((l) => l.cakto_subscription_id));
    const acesso = await acessoDoEmail(admin, email);
    const acessoAte = lerVencimento(acesso?.expires_at);
    for (const item of await assinaturasDoEmail(email)) {
      if (item.id === assinaturaNovaId || jaConhecidas.has(item.id)) continue;
      if (!ehProdutoDaKtalogu(item.product) || !AINDA_COBRAM.has(String(item.status))) continue;
      /* Do plano novo, só a que nasceu antes desta (a mais nova manda). Dos
         planos antigos, todas: quem assinou o plano novo saiu do antigo. */
      const nasceu = item.createdAt ? new Date(item.createdAt).getTime() : 0;
      if (!PRODUTOS_ANTIGOS[item.product] && !(nasceu && nasceu < new Date(criadaEm).getTime())) continue;
      const plano = planoDoPedido(item.product, item.offer) ?? "mensal";
      const { error: erroAntiga } = await admin.from("assinaturas_cakto").upsert(
        {
          cakto_subscription_id: item.id,
          email,
          oferta_id: item.offer || "desconhecida",
          produto_id: item.product,
          plano,
          metodo: item.paymentMethod ?? null,
          situacao: "cancelada",
          cancelada_em: agora,
          pago_ate: acessoAte ? acessoAte.toISOString() : null,
          cancelar_na_cakto: true,
          substituida_por: assinaturaNovaId,
          atualizada_em: agora,
        },
        { onConflict: "cakto_subscription_id" }
      );
      if (erroAntiga) throw new Error(erroAntiga.message);
    }
  }

  const { error: erroFim } = await admin
    .from("assinaturas_cakto")
    .update({ anteriores_resolvidas: true, atualizada_em: new Date().toISOString() })
    .eq("id", nova.id);
  if (erroFim) throw new Error(erroFim.message);

  const canceladas = await cancelarPendentesNaCakto(admin, email);
  return { canceladas, nossas: nossas?.length ?? 0 };
}

/** Cancela na Cakto o que está marcado para cancelar (de um e-mail, ou de todos). */
export async function cancelarPendentesNaCakto(admin: Admin, email?: string) {
  if (!caktoConfigurada()) return 0;
  let consulta = admin
    .from("assinaturas_cakto")
    .select("id,cakto_subscription_id")
    .eq("cancelar_na_cakto", true)
    .is("cancelada_na_cakto_em", null)
    .limit(25);
  if (email) consulta = consulta.eq("email", email);
  const { data, error } = await consulta;
  if (error) throw new Error(error.message);
  let feitas = 0;
  for (const linha of (data ?? []) as Array<{ id: string; cakto_subscription_id: string }>) {
    const resultado = await cancelarNaCakto(linha.cakto_subscription_id);
    await admin
      .from("assinaturas_cakto")
      .update(
        resultado.ok
          ? { cancelada_na_cakto_em: new Date().toISOString(), erro_ao_cancelar: null }
          : { erro_ao_cancelar: resultado.erro.slice(0, 500) }
      )
      .eq("id", linha.id);
    if (resultado.ok) feitas += 1;
  }
  return feitas;
}

/* ------------------------------------------------------------------ */
/* Histórico de pagamentos                                             */
/* ------------------------------------------------------------------ */

export async function guardarPagamento(admin: Admin, email: string, pagamento: PagamentoCakto) {
  const { error } = await admin
    .from("pagamentos_cakto")
    .upsert({ ...pagamento, email, atualizado_em: new Date().toISOString() }, { onConflict: "pedido_id" });
  if (error) throw new Error(`Não foi possível guardar o pagamento: ${error.message}`);
}

/* O e-mail da conta a que o pedido pertence: o da assinatura, se já é
   conhecida; senão, o de quem pagou. */
export async function emailDaContaDoPedido(admin: Admin, assinaturaId: string | null, emailDeQuemPagou: string) {
  if (assinaturaId) {
    const { data } = await admin.from("assinaturas_cakto").select("email").eq("cakto_subscription_id", assinaturaId).maybeSingle();
    if (data?.email) return String(data.email);
  }
  return emailDeQuemPagou;
}

async function marcar(admin: Admin, aviso: AvisoCakto, situacao: "atrasada" | "pausada" | "cancelada"): Promise<Resultado> {
  const guardada = await assinaturaGuardada(admin, aviso.assinaturaId);
  const conta = await contaDoAviso(admin, aviso, guardada);
  if (!conta) return { status: "ignored", detalhe: `Assinatura ${situacao} sem conta conhecida.` };
  /* Reembolso e chargeback já cortaram: um aviso de cancelamento depois não
     muda a assinatura de volta. */
  if (guardada && (guardada.situacao === "reembolsada" || guardada.situacao === "contestada")) {
    return { status: "processed", detalhe: `Assinatura já ${guardada.situacao}; ${situacao} só registrado.` };
  }
  await guardarAssinatura(admin, aviso, conta, guardada, { situacao });
  if (situacao === "atrasada" && aviso.assinaturaId) {
    /* Uma vez por ciclo: as novas tentativas da Cakto no mesmo ciclo não repetem o e-mail. */
    avisarSemEsperar(admin, conta.email, "pagamento-recusado", `${aviso.assinaturaId}:${guardada?.pago_ate?.slice(0, 10) ?? "sem-periodo"}`);
  }
  /* O acesso não muda: ele já vence no fim do dia do período pago e fecha no
     dia seguinte se a renovação não entrar. */
  return { status: "processed", detalhe: `Assinatura de ${conta.email} marcada como ${situacao}; acesso segue até o fim do período pago.` };
}

async function cortar(admin: Admin, aviso: AvisoCakto, situacao: "reembolsada" | "contestada"): Promise<Resultado> {
  const guardada = await assinaturaGuardada(admin, aviso.assinaturaId);
  const conta = await contaDoAviso(admin, aviso, guardada);
  if (!conta) return { status: "ignored", detalhe: `Pedido ${situacao} sem conta conhecida.` };
  /* Guarda até onde o pedido reembolsado pagava. Se o aviso da compra desse
     mesmo pedido chegar depois (reprocessamento), ele não passa daqui e não
     reabre o acesso. */
  const pagoAteDoPedido = pagoAteDoAviso(aviso, OFERTAS_KTALOGU[aviso.ofertaId].dias);
  const pagoAteGuardado = guardada?.pago_ate ? new Date(guardada.pago_ate) : null;
  const pagoAte =
    pagoAteDoPedido && (!pagoAteGuardado || pagoAteDoPedido.getTime() > pagoAteGuardado.getTime()) ? pagoAteDoPedido : pagoAteGuardado;
  await guardarAssinatura(admin, aviso, conta, guardada, { situacao, pagoAte });

  /* Outra assinatura paga da mesma conta continua valendo; sem ela, fecha agora. */
  const { data: outras, error } = await admin
    .from("assinaturas_cakto")
    .select("pago_ate")
    .eq("email", conta.email)
    .in("situacao", ["ativa", "atrasada", "pausada", "cancelada"])
    .gt("pago_ate", new Date().toISOString())
    .order("pago_ate", { ascending: false })
    .limit(1);
  if (error) throw new Error(`Não foi possível ler as outras assinaturas: ${error.message}`);
  const outraAte = (outras as Array<{ pago_ate: string }> | null)?.[0]?.pago_ate;

  const campos = outraAte
    ? { status: "active", expires_at: fimDoDiaEmBrasilia(new Date(outraAte)).toISOString(), updated_at: new Date().toISOString() }
    : { status: situacao === "reembolsada" ? "refunded" : "chargeback", expires_at: new Date().toISOString(), updated_at: new Date().toISOString() };
  const gravado = await admin.from("users_app").update(campos).eq("email", conta.email);
  if (gravado.error) throw new Error(`Não foi possível cortar o acesso: ${gravado.error.message}`);

  esquecerAcesso(conta.email);
  esquecerAssinaturaDaEmpresa();
  if (!outraAte && aviso.pedidoId) avisarSemEsperar(admin, conta.email, "acesso-cortado", aviso.pedidoId);
  return {
    status: "processed",
    detalhe: outraAte
      ? `Pedido ${situacao}; ${conta.email} segue com outra assinatura paga.`
      : `Pedido ${situacao}; acesso de ${conta.email} cortado na hora.`,
  };
}

export async function aplicarAviso(admin: Admin, aviso: AvisoCakto): Promise<Resultado> {
  const acao = decidir(aviso);
  if (acao.tipo === "ignorar") return { status: "ignored", detalhe: acao.motivo };
  if (acao.tipo === "liberar") return liberar(admin, aviso, acao.pagoAte);
  if (acao.tipo === "marcar") return marcar(admin, aviso, acao.situacao);
  return cortar(admin, aviso, acao.situacao);
}

/**
 * Grava a entrega e devolve o que falta aplicar. A rota responde à Cakto logo
 * depois de gravar e aplica em seguida: a Cakto desiste em 8 segundos, e
 * aplicar (achar a conta, liberar, mandar e-mail) pode passar disso.
 */
export async function registrarAviso(admin: Admin, corpo: unknown, chaveSemPedido: string) {
  const aviso = lerAviso(corpo);
  const entrega = await registrarEntrega(admin, aviso?.chave ?? chaveSemPedido, corpo, aviso);
  return { entrega, aviso };
}

export function processarEntrega(admin: Admin, entrega: Entrega, aviso: AvisoCakto | null, corpo?: unknown) {
  return naFila(() => aplicarEntrega(admin, entrega, aviso, corpo));
}

async function aplicarEntrega(admin: Admin, entrega: Entrega, aviso: AvisoCakto | null, corpo?: unknown) {
  if (!aviso) {
    /* Abandono de checkout: sem pedido, forma própria. Vira um carrinho para os lembretes. */
    const abandono = corpo === undefined ? null : lerAbandono(corpo);
    if (abandono) {
      try {
        await guardarAbandono(admin, abandono);
        await marcarEntrega(admin, entrega.id, "processed", `Carrinho abandonado de ${abandono.email} guardado para os lembretes.`);
        return { entrega, resultado: { status: "processed", detalhe: "carrinho" } as Resultado, erro: null };
      } catch (e) {
        const erro = e instanceof Error ? e.message : String(e);
        await marcarEntrega(admin, entrega.id, "failed", erro).catch(() => undefined);
        return { entrega, resultado: null, erro };
      }
    }
    const resultado: Resultado = { status: "ignored", detalhe: "Aviso sem pedido (formato desconhecido)." };
    await marcarEntrega(admin, entrega.id, resultado.status, resultado.detalhe);
    return { entrega, resultado, erro: null };
  }
  try {
    const resultado = await aplicarAviso(admin, aviso);
    await marcarEntrega(admin, entrega.id, resultado.status, resultado.detalhe);
    /* O histórico nunca derruba o aviso: se falhar, a leitura da API corrige. */
    const pagamento = pagamentoDoAviso(aviso);
    const emailDaConta = aviso.email ? await emailDaContaDoPedido(admin, aviso.assinaturaId, aviso.email) : "";
    if (pagamento && emailDaConta) {
      await guardarPagamento(admin, emailDaConta, pagamento).catch((e) =>
        console.error("[cakto] pagamento não entrou no histórico", aviso.pedidoId, e instanceof Error ? e.message : e)
      );
    }
    /* O dono fica sabendo do que mudou de verdade: aviso só registrado ou que
       não liberou nada (período já reembolsado) não vira e-mail. */
    if (resultado.status === "processed" && emailDaConta && !/nada liberado|só registrado/.test(resultado.detalhe)) {
      void avisarODono(admin, aviso, resultado.detalhe, emailDaConta).catch(() => undefined);
    }
    return { entrega, resultado, erro: null };
  } catch (e) {
    const erro = e instanceof Error ? e.message : String(e);
    await marcarEntrega(admin, entrega.id, "failed", erro).catch(() => undefined);
    return { entrega, resultado: null, erro };
  }
}
