/*
  O Asaas visto do balcão.

  Cópia do admin do que mora em `apps/public/src/lib/asaas.ts`. Os dois apps
  não compartilham pacote — a integração do Mercado Pago vive com a mesma
  divisão desde sempre. Mexeu na forma da configuração lá, mexe aqui.

  Além do que a loja usa, esta metade carrega o que só o admin faz: validar a
  chave que o lojista acabou de colar e registrar o webhook na conta dele.
*/

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

export type AmbienteAsaas = "sandbox" | "production";

export type ConfiguracaoAsaas = {
  environment: AmbienteAsaas;
  checkout_enabled: boolean;
  auto_create_order_on_approved_payment: boolean;
  bank_account_id: string | null;
  sandbox_api_key: string;
  production_api_key: string;
  aceita_pix: boolean;
  aceita_boleto: boolean;
  aceita_cartao: boolean;
  dias_para_vencimento: number;
  webhook_token: string;
};

export type CredenciaisAsaas = {
  ambiente: AmbienteAsaas;
  apiKey: string;
};

export type IntegracaoAsaas = {
  id: string;
  user_id: string;
  provider: string;
  settings: Record<string, unknown> | null;
  active: boolean | null;
};

export type EstadoDoPagamento =
  | "approved"
  | "rejected"
  | "cancelled"
  | "refunded"
  | "charged_back"
  | "pending";

export const BASE_DO_ASAAS: Record<AmbienteAsaas, string> = {
  production: "https://api.asaas.com/v3",
  sandbox: "https://api-sandbox.asaas.com/v3",
};

function texto(valor: unknown) {
  return String(valor ?? "").trim();
}

function booleano(valor: unknown, padrao: boolean) {
  if (typeof valor === "boolean") return valor;
  if (typeof valor === "string") {
    const normalizado = valor.trim().toLowerCase();
    if (normalizado === "true") return true;
    if (normalizado === "false") return false;
  }
  return padrao;
}

function objeto(valor: unknown) {
  return valor && typeof valor === "object" && !Array.isArray(valor) ? (valor as Record<string, unknown>) : null;
}

export function lerConfiguracaoDoAsaas(bruto: unknown): ConfiguracaoAsaas {
  const dados = objeto(bruto) || {};
  const dias = Number(dados.dias_para_vencimento);

  return {
    environment: dados.environment === "production" ? "production" : "sandbox",
    checkout_enabled: booleano(dados.checkout_enabled, true),
    auto_create_order_on_approved_payment: booleano(dados.auto_create_order_on_approved_payment, true),
    bank_account_id: texto(dados.bank_account_id) || null,
    sandbox_api_key: texto(dados.sandbox_api_key),
    production_api_key: texto(dados.production_api_key),
    /*
      Pix e boleto nascem ligados; cartão, não.

      Não é preferência: é que os dois primeiros acontecem dentro da loja, do
      jeito que o cliente já conhece. O cartão pelo Asaas leva o comprador para
      uma página deles e o traz de volta, e mandar alguém para fora da loja sem
      o lojista ter pedido seria decidir no lugar dele.
    */
    aceita_pix: booleano(dados.aceita_pix, true),
    aceita_boleto: booleano(dados.aceita_boleto, true),
    aceita_cartao: booleano(dados.aceita_cartao, false),
    dias_para_vencimento: Number.isFinite(dias) && dias >= 0 && dias <= 90 ? Math.trunc(dias) : 1,
    webhook_token: texto(dados.webhook_token),
  };
}

export function credenciaisAtivas(config: ConfiguracaoAsaas): CredenciaisAsaas | null {
  if (config.environment === "production") {
    return config.production_api_key ? { ambiente: "production", apiKey: config.production_api_key } : null;
  }
  return config.sandbox_api_key ? { ambiente: "sandbox", apiKey: config.sandbox_api_key } : null;
}

/*
  A tradução dos estados do Asaas para os seis que o banco conhece.

  `CONFIRMED` é pago com o dinheiro ainda indisponível na conta; `RECEIVED` é
  pago e disponível. Para liberar o pedido, confirmado basta. O estado bruto
  fica em `provider_status`, então a diferença entre os dois momentos continua
  visível para quem precisar dela.
*/
export function estadoDoPagamento(valor: unknown): EstadoDoPagamento {
  const estado = texto(valor).toUpperCase();

  if (estado === "CONFIRMED" || estado === "RECEIVED" || estado === "RECEIVED_IN_CASH") return "approved";
  if (estado === "REFUNDED" || estado === "PARTIALLY_REFUNDED" || estado === "REFUND_REQUESTED") return "refunded";
  if (
    estado === "CHARGEBACK_REQUESTED" ||
    estado === "CHARGEBACK_DISPUTE" ||
    estado === "AWAITING_CHARGEBACK_REVERSAL"
  ) {
    return "charged_back";
  }
  if (estado === "DELETED") return "cancelled";
  if (estado === "REPROVED_BY_RISK_ANALYSIS") return "rejected";

  /* `OVERDUE` cai aqui de propósito: boleto vencido continua pagável. */
  return "pending";
}

export function metodoDoPagamento(billingType: unknown): string {
  const tipo = texto(billingType).toUpperCase();
  if (tipo === "PIX") return "pix";
  if (tipo === "BOLETO") return "ticket";
  if (tipo === "CREDIT_CARD") return "credit_card";
  if (tipo === "DEBIT_CARD") return "debit_card";
  return "unknown";
}

/* O nome que o Asaas dá a cada meio, em `billingType`. */
export function billingTypeDoMeio(meio: "pix" | "boleto" | "cartao") {
  if (meio === "pix") return "PIX";
  if (meio === "boleto") return "BOLETO";
  return "CREDIT_CARD";
}

/*
  A data de hoje no Brasil, que é o que `dueDate` espera.

  A VPS trabalha em UTC: às 22h daqui já é 1h do dia seguinte lá, e um Pix de
  balcão nasceria vencendo amanhã sem ninguém entender por quê. O Brasil não
  tem horário de verão desde 2019, então o deslocamento é fixo.
*/
export function vencimentoNoBrasil(diasAFrente = 0, agora = new Date()) {
  const emBrasilia = new Date(agora.getTime() - 3 * 60 * 60 * 1000 + diasAFrente * 24 * 60 * 60 * 1000);
  return emBrasilia.toISOString().slice(0, 10);
}

/* O Asaas exige de 32 a 255 caracteres, sem espaços e sem sequência simples.
   São 24 bytes sorteados virando 48 hexadecimais, um por loja. */
export function gerarTokenDeWebhook() {
  const bytes = new Uint8Array(24);
  crypto.getRandomValues(bytes);
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
}

export function mensagemDoErro(corpo: unknown, padrao = "Não foi possível falar com o Asaas.") {
  const erros = objeto(corpo)?.errors;
  if (Array.isArray(erros)) {
    const descricoes = erros
      .map((item) => texto(objeto(item)?.description))
      .filter(Boolean)
      .join(" ");
    if (descricoes) return descricoes;
  }
  return texto(objeto(corpo)?.message) || padrao;
}

export async function chamarAsaas<T>(
  caminho: string,
  credenciais: CredenciaisAsaas,
  init?: RequestInit
): Promise<T> {
  const headers = new Headers(init?.headers || {});
  headers.set("access_token", credenciais.apiKey);
  headers.set("Content-Type", "application/json");
  headers.set("User-Agent", "Ktalogu");

  const resposta = await fetch(`${BASE_DO_ASAAS[credenciais.ambiente]}${caminho}`, {
    ...init,
    headers,
    cache: "no-store",
  });

  const corpo = (await resposta.json().catch(() => ({}))) as T;
  if (!resposta.ok) throw new Error(mensagemDoErro(corpo));

  return corpo;
}

/*
  O instante do pagamento, e nao so o dia.

  O Asaas devolve `confirmedDate`, `paymentDate` e `clientPaymentDate` como DATA
  PURA: "2026-09-14", sem hora. Guardar isso numa coluna de instante vira
  meia-noite UTC — que no Brasil e 21h do DIA ANTERIOR. Um Pix pago no dia 14 ia
  para o fluxo de caixa como dia 13, e o fechamento do dia nunca batia.

  Sem hora de verdade, o menos errado e o meio-dia de Brasilia: fica dentro do
  dia certo em qualquer fuso que alguem use para olhar, e nao inventa precisao
  que o provedor nao deu.
*/
export function instanteDoPagamento(valor: unknown): string | null {
  const bruto = texto(valor);
  if (!bruto) return null;
  if (/^\d{4}-\d{2}-\d{2}$/.test(bruto)) return `${bruto}T15:00:00.000Z`;
  const instante = new Date(bruto);
  return Number.isNaN(instante.getTime()) ? null : instante.toISOString();
}

type QrDoPix = { payload?: unknown; encodedImage?: unknown; expirationDate?: unknown };

/*
  O QR do Pix de uma cobranca — com uma segunda tentativa que nao e paranoia.

  A PRIMEIRA COBRANCA DE UMA CONTA NOVA NASCIA SEM QR, e levou um teste real
  para descobrir por que. Conta Asaas recem-criada nao tem chave Pix. Quando se
  pede o QR, o Asaas cria a chave naquele instante — e a requisicao que provocou
  a criacao volta sem o codigo. A prova esta no relogio: a chave da conta de
  teste foi criada as 08:31:50, dois segundos depois da cobranca das 08:31:48.

  Quem pagaria o preco disso e exatamente o primeiro cliente de cada loja que
  conecta o Asaas: ele veria "Pix gerado" e nenhum codigo na tela.

  Da segunda vez a chave ja existe e o QR vem. Por isso: tenta, espera um pouco,
  tenta de novo. Em conta que ja roda, a primeira tentativa acerta e a segunda
  nunca acontece.
*/
export async function qrDoPix(credenciais: CredenciaisAsaas, idDaCobranca: string) {
  const caminho = `/payments/${encodeURIComponent(idDaCobranca)}/pixQrCode`;

  for (let tentativa = 0; tentativa < 2; tentativa += 1) {
    if (tentativa > 0) await new Promise((pronto) => setTimeout(pronto, 1200));
    try {
      const qr = await chamarAsaas<QrDoPix>(caminho, credenciais, { method: "GET" });
      const copiaECola = texto(qr?.payload);
      if (!copiaECola) continue;
      return {
        copiaECola,
        imagemBase64: texto(qr?.encodedImage) || null,
        expiraEm: texto(qr?.expirationDate) || null,
      };
    } catch {
      /* Primeira tentativa falhando e o caso esperado da conta sem chave. */
    }
  }

  return null;
}

type ClienteDoAsaas = { id?: unknown };
type ListaDoAsaas<T> = { data?: T[] };

/*
  O identificador do comprador dentro da conta do lojista.

  O Asaas não cobra de um e-mail solto: toda cobrança aponta para um cliente
  cadastrado. Procurar pelo documento antes de criar evita encher a agenda do
  lojista de cópias da mesma pessoa.
*/
export async function clienteDoAsaas(
  credenciais: CredenciaisAsaas,
  dados: { documento: string; nome: string; email?: string; telefone?: string; referencia?: string }
): Promise<string> {
  const documento = texto(dados.documento).replace(/\D/g, "");
  if (!documento) throw new Error("Informe um CPF ou CNPJ para cobrar.");

  const encontrados = await chamarAsaas<ListaDoAsaas<ClienteDoAsaas>>(
    `/customers?cpfCnpj=${encodeURIComponent(documento)}&limit=1`,
    credenciais
  );

  const existente = texto(encontrados?.data?.[0]?.id);
  if (existente) return existente;

  const criado = await chamarAsaas<ClienteDoAsaas>("/customers", credenciais, {
    method: "POST",
    body: JSON.stringify({
      name: texto(dados.nome) || "Cliente",
      cpfCnpj: documento,
      email: texto(dados.email) || undefined,
      mobilePhone: texto(dados.telefone).replace(/\D/g, "") || undefined,
      externalReference: texto(dados.referencia) || undefined,
    }),
  });

  const id = texto(criado?.id);
  if (!id) throw new Error("O Asaas não devolveu o cliente criado.");
  return id;
}

/* A integração é uma por dono de conta: `user_id` é o dono, não quem está
   logado no balcão. */
export async function integracaoDoAsaas(admin: SupabaseClient, ownerId: string) {
  const { data, error } = await admin
    .from("integrations")
    .select("id,user_id,provider,settings,active")
    .eq("provider", "asaas")
    .eq("user_id", ownerId)
    .maybeSingle();

  if (error) throw new Error(error.message || "Não foi possível carregar a integração do Asaas.");
  return (data as IntegracaoAsaas | null) ?? null;
}

/* ------------------------------------------------------------ só no admin */

type ContaDoAsaas = { name?: unknown; email?: unknown; walletId?: unknown; cpfCnpj?: unknown };

/*
  A chave que o lojista colou é boa?

  Vale perguntar antes de gravar. Uma chave errada guardada em silêncio só
  aparece no primeiro cliente que tenta pagar, e aí o prejuízo é a venda.
  `/myAccount` é a chamada mais barata que prova a chave e ainda devolve o nome
  da conta, que a tela mostra para o lojista conferir que conectou a conta
  certa — e não a do sócio, ou a do sandbox achando que era produção.
*/
export async function conferirChaveDoAsaas(credenciais: CredenciaisAsaas) {
  const conta = await chamarAsaas<ContaDoAsaas>("/myAccount", credenciais);
  return {
    nome: texto(conta?.name) || null,
    email: texto(conta?.email) || null,
    documento: texto(conta?.cpfCnpj) || null,
    walletId: texto(conta?.walletId) || null,
  };
}

type WebhookDoAsaas = { id?: unknown; url?: unknown };

/* Os eventos que nos interessam. Pedir a lista inteira encheria a nossa fila
   de aviso de boleto visualizado e recarga de celular. */
export const EVENTOS_DO_WEBHOOK = [
  "PAYMENT_CREATED",
  "PAYMENT_UPDATED",
  "PAYMENT_CONFIRMED",
  "PAYMENT_RECEIVED",
  "PAYMENT_OVERDUE",
  "PAYMENT_DELETED",
  "PAYMENT_RESTORED",
  "PAYMENT_REFUNDED",
  "PAYMENT_PARTIALLY_REFUNDED",
  "PAYMENT_CHARGEBACK_REQUESTED",
  "PAYMENT_CHARGEBACK_DISPUTE",
  "PAYMENT_AWAITING_CHARGEBACK_REVERSAL",
];

/*
  Registra (ou corrige) o webhook dentro da conta do lojista.

  Ele nunca precisa abrir o painel do Asaas: salvou a chave, o aviso já está
  configurado. Se já existe um apontando para o mesmo endereço, o registro é
  atualizado em vez de duplicado — cada conta aceita no máximo dez, e uma
  segunda gravação não pode gastar uma vaga à toa.

  `SEQUENTIALLY` porque a ordem importa: "criada" antes de "recebida" evita que
  o aviso de pagamento chegue antes de a cobrança existir do nosso lado.
*/
export async function registrarWebhookDoAsaas(
  credenciais: CredenciaisAsaas,
  dados: { url: string; token: string; email: string }
) {
  const existentes = await chamarAsaas<ListaDoAsaas<WebhookDoAsaas>>("/webhooks?limit=100", credenciais);
  const nosso = (existentes?.data || []).find((item) => texto(item?.url) === dados.url);

  const corpo = JSON.stringify({
    name: "Ktalogu",
    url: dados.url,
    email: dados.email,
    enabled: true,
    interrupted: false,
    apiVersion: 3,
    authToken: dados.token,
    sendType: "SEQUENTIALLY",
    events: EVENTOS_DO_WEBHOOK,
  });

  if (nosso?.id) {
    await chamarAsaas(`/webhooks/${encodeURIComponent(texto(nosso.id))}`, credenciais, { method: "PUT", body: corpo });
    return { criado: false as const };
  }

  await chamarAsaas("/webhooks", credenciais, { method: "POST", body: corpo });
  return { criado: true as const };
}
