import {
  extractMercadoPagoQrData,
  fetchMercadoPagoApi,
  getActiveMercadoPagoCredentials,
  normalizeMercadoPagoPaymentMethod,
  normalizeMercadoPagoPaymentStatus,
  parseMercadoPagoSettings,
} from "@/lib/mercado-pago";
import {
  chamarAsaas,
  credenciaisAtivas as credenciaisDoAsaas,
  estadoDoPagamento as estadoDoAsaas,
  instanteDoPagamento,
  lerConfiguracaoDoAsaas,
  metodoDoPagamento as metodoDoAsaas,
} from "@/lib/asaas";
import { applyApprovedCatalogPayment } from "@/lib/catalog-payment-processing";
import { avisarLojaPorPush, avisarVendaAprovada } from "@/lib/chats-push";
import { avisarVenda } from "@/lib/chats-aviso";
import { registrarCompraNaRota } from "@/lib/rota-do-visitante";
/* Mesma forma usada pelas rotas de pagamento: o provedor devolve muito campo e
   so estes interessam aqui. */
type MercadoPagoPaymentResponse = Record<string, unknown> & {
  status?: unknown;
  status_detail?: unknown;
  payment_method_id?: unknown;
  payment_type_id?: unknown;
  date_approved?: unknown;
  date_of_expiration?: unknown;
};

type CobrancaDoAsaas = Record<string, unknown> & {
  status?: unknown;
  billingType?: unknown;
  confirmedDate?: unknown;
  paymentDate?: unknown;
  clientPaymentDate?: unknown;
};


/*
  Conciliacao ativa do pagamento do catalogo.

  Ate aqui o pedido so mudava de estado quando o aviso (webhook) do provedor
  chegava. Se ele nao chega — a maquina esta em localhost e nao tem endereco
  publico, o aviso se perdeu, o provedor atrasou — o cliente pagava o Pix de
  verdade e a tela ficava para sempre em "aguarde a confirmacao", com o pedido
  pendente e o dono da loja sem saber que o dinheiro entrou.

  Esta funcao faz o caminho contrario: pergunta ao provedor como esta o
  pagamento e grava o resultado, exatamente como o webhook faria. E chamada pela
  consulta de status que a tela do checkout ja fazia em looping, entao o cliente
  ve a confirmacao sozinho, sem depender de aviso externo.

  DOIS PROVEDORES, UMA CONCILIACAO. Quando o Asaas entrou, a tentacao era
  escrever uma copia disto para ele. Foi exatamente o que este arquivo existe
  para nao acontecer: antes, o webhook tinha a sua copia, e qualquer ajuste numa
  delas deixava a outra para tras. Entao o que muda por provedor e so a LEITURA
  — como se pergunta o estado e como se traduz a resposta. Gravar, dar baixa e
  avisar continua sendo um caminho so.
*/

type SupabaseLike = {
  from: (table: string) => {
    select: (columns: string) => {
      eq: (column: string, value: unknown) => {
        maybeSingle: () => Promise<{ data: unknown; error: unknown }>;
      };
    };
    update: (values: Record<string, unknown>) => {
      eq: (column: string, value: unknown) => Promise<{ data: unknown; error: unknown }>;
    };
    insert: (values: Record<string, unknown>) => Promise<{ data: unknown; error: unknown }>;
  };
};

type LocalPayment = {
  id?: string | null;
  user_id?: string | null;
  catalog_order_id?: string | null;
  amount?: number | null;
  integration_id?: string | null;
  provider_payment_id?: string | null;
  status?: string | null;
  updated_at?: string | null;
};

/* O que a conciliacao precisa saber, venha de onde vier. Os campos do QR so
   aparecem quando o provedor os entrega de graca na mesma resposta: sobrescrever
   um QR guardado com `null` apagaria da tela o codigo que o cliente esta
   olhando naquele instante. */
type LeituraDoProvedor = {
  status: string;
  providerStatus: string | null;
  providerStatusDetail: string | null;
  metodo: string;
  tipo: string | null;
  aprovadoEm: string | null;
  qr: { qrCode: string | null; qrCodeBase64: string | null; ticketUrl: string | null; expiraEm: string | null } | null;
  bruto: Record<string, unknown>;
  darBaixaAutomatica: boolean;
};

/* Estados que ainda podem virar outra coisa. Aprovado, recusado e cancelado
   sao finais: nao vale gastar chamada no provedor por eles. */
const ESTADOS_EM_ABERTO = new Set(["pending", "in_process", "authorized", "initiated"]);

/* Uma consulta ao provedor a cada 8 segundos por pagamento e suficiente para a
   tela parecer instantanea sem transformar o looping do checkout numa enxurrada
   de chamadas na API do provedor. */
const INTERVALO_MINIMO_MS = 8000;

const texto = (valor: unknown) => (String(valor || "").trim() ? String(valor).trim() : null);

/* A compra de um produto digital (a compra expressa da loja) vira pedido
   interno sempre que é aprovada, mesmo com "criar pedido ao aprovar" desligado:
   é o pedido pago que libera a entrega para o comprador. */
async function ehCompraDigital(admin: SupabaseLike, catalogOrderId: string | null | undefined): Promise<boolean> {
  if (!catalogOrderId) return false;
  const { data } = await admin.from("catalog_orders").select("cart_payload").eq("id", catalogOrderId).maybeSingle();
  const payload = (data as { cart_payload?: unknown } | null)?.cart_payload;
  return Boolean(payload && typeof payload === "object" && (payload as Record<string, unknown>).compra_digital === true);
}

export function pagamentoPodeSerConciliado(payment: LocalPayment | null | undefined) {
  if (!payment?.id || !payment.integration_id || !payment.provider_payment_id) return false;
  if (!ESTADOS_EM_ABERTO.has(String(payment.status || "pending"))) return false;
  const ultimaAtualizacao = payment.updated_at ? new Date(payment.updated_at).getTime() : 0;
  if (!Number.isFinite(ultimaAtualizacao) || ultimaAtualizacao <= 0) return true;
  return Date.now() - ultimaAtualizacao >= INTERVALO_MINIMO_MS;
}

async function lerNoMercadoPago(settings: unknown, providerPaymentId: string): Promise<LeituraDoProvedor | null> {
  const config = parseMercadoPagoSettings(settings);
  const credentials = getActiveMercadoPagoCredentials(config);
  if (!credentials) return null;

  const remoto = await fetchMercadoPagoApi<MercadoPagoPaymentResponse>(
    `/v1/payments/${encodeURIComponent(providerPaymentId)}`,
    credentials.accessToken,
    { method: "GET" }
  );

  const qrData = extractMercadoPagoQrData(remoto);
  const status = normalizeMercadoPagoPaymentStatus(remoto.status);

  return {
    status,
    providerStatus: texto(remoto.status),
    providerStatusDetail: texto(remoto.status_detail),
    metodo: normalizeMercadoPagoPaymentMethod(remoto.payment_method_id, remoto.payment_type_id),
    tipo: texto(remoto.payment_type_id),
    aprovadoEm: status === "approved" ? texto(remoto.date_approved) || new Date().toISOString() : null,
    /* O Mercado Pago devolve o QR junto do pagamento, entao ele e reescrito a
       cada leitura sem custo nenhum. */
    qr: {
      qrCode: qrData.qrCode,
      qrCodeBase64: qrData.qrCodeBase64,
      ticketUrl: qrData.ticketUrl,
      expiraEm: texto(remoto.date_of_expiration),
    },
    bruto: remoto,
    darBaixaAutomatica: config.auto_create_order_on_approved_payment,
  };
}

async function lerNoAsaas(settings: unknown, providerPaymentId: string): Promise<LeituraDoProvedor | null> {
  const config = lerConfiguracaoDoAsaas(settings);
  const credenciais = credenciaisDoAsaas(config);
  if (!credenciais) return null;

  const remoto = await chamarAsaas<CobrancaDoAsaas>(
    `/payments/${encodeURIComponent(providerPaymentId)}`,
    credenciais,
    { method: "GET" }
  );

  const status = estadoDoAsaas(remoto.status);

  return {
    status,
    providerStatus: texto(remoto.status),
    /* O Asaas nao tem um detalhe de status separado como o Mercado Pago: o
       motivo, quando existe, ja vem dentro do proprio estado. */
    providerStatusDetail: null,
    metodo: metodoDoAsaas(remoto.billingType),
    tipo: texto(remoto.billingType),
    aprovadoEm:
      status === "approved"
        ? instanteDoPagamento(remoto.confirmedDate) ||
          instanteDoPagamento(remoto.clientPaymentDate) ||
          instanteDoPagamento(remoto.paymentDate) ||
          new Date().toISOString()
        : null,
    /* No Asaas o QR mora em outra chamada. Buscar de novo a cada consulta de
       status seria dobrar o trafego para reescrever o mesmo codigo que ja esta
       gravado desde a criacao da cobranca. */
    qr: null,
    bruto: remoto,
    darBaixaAutomatica: config.auto_create_order_on_approved_payment,
  };
}

export async function reconcileCatalogPaymentWithProvider(
  admin: SupabaseLike,
  localPayment: LocalPayment,
  options?: { webhookPayload?: unknown; forcar?: boolean }
) {
  if (!localPayment?.id || !localPayment.integration_id || !localPayment.provider_payment_id) {
    return { atualizado: false as const, status: null as string | null };
  }
  if (!options?.forcar && !pagamentoPodeSerConciliado(localPayment)) {
    return { atualizado: false as const, status: String(localPayment.status || "") };
  }

  const { data: integration } = await admin
    .from("integrations")
    .select("id,provider,settings,active")
    .eq("id", localPayment.integration_id)
    .maybeSingle();

  const integracao = (integration || null) as
    | { id?: string; provider?: string; settings?: unknown; active?: boolean }
    | null;
  if (!integracao?.id || integracao.active === false) {
    return { atualizado: false as const, status: String(localPayment.status || "") };
  }

  const provedor = String(integracao.provider || "mercado_pago");
  const leitura =
    provedor === "asaas"
      ? await lerNoAsaas(integracao.settings, String(localPayment.provider_payment_id))
      : await lerNoMercadoPago(integracao.settings, String(localPayment.provider_payment_id));

  if (!leitura) {
    return { atualizado: false as const, status: String(localPayment.status || "") };
  }

  await admin
    .from("catalog_order_payments")
    .update({
      status: leitura.status,
      provider_status: leitura.providerStatus,
      provider_status_detail: leitura.providerStatusDetail,
      payment_method: leitura.metodo,
      payment_type: leitura.tipo,
      approved_at: leitura.aprovadoEm,
      rejected_at: leitura.status === "rejected" ? new Date().toISOString() : null,
      cancelled_at: leitura.status === "cancelled" ? new Date().toISOString() : null,
      ...(leitura.qr
        ? {
            expires_at: leitura.qr.expiraEm,
            qr_code: leitura.qr.qrCode,
            qr_code_base64: leitura.qr.qrCodeBase64,
            ticket_url: leitura.qr.ticketUrl,
          }
        : {}),
      raw_response: leitura.bruto,
      ...(options?.webhookPayload ? { last_webhook_payload: options.webhookPayload } : {}),
      updated_at: new Date().toISOString(),
    })
    .eq("id", localPayment.id);

  await admin.from("catalog_payment_events").insert({
    catalog_payment_id: localPayment.id,
    user_id: localPayment.user_id,
    provider: provedor,
    event_type: options?.webhookPayload ? "payment.webhook" : "payment.reconciliation",
    payload: options?.webhookPayload ?? { provider_status: leitura.providerStatus, source: "status_poll" },
  });

  if (leitura.status === "approved" && (leitura.darBaixaAutomatica || (await ehCompraDigital(admin, localPayment.catalog_order_id)))) {
    await applyApprovedCatalogPayment(admin as never, String(localPayment.id));
  }

  /*
    Dinheiro entrando merece aviso na hora.

    E o unico aviso que a loja quer receber no meio da rua: quanto entrou e como
    foi pago. Falhar aqui nao pode desfazer o pagamento, entao o erro morre em
    silencio.
  */
  if (leitura.status === "approved") {
    try {
      await avisarVendaAprovada(admin as never, {
        tenantId: localPayment.user_id,
        valor: Number(localPayment.amount || 0),
        metodo: leitura.metodo || leitura.tipo,
        rota: "/dashboard/catalog/orders",
      });
      await avisarVenda(localPayment.user_id, {
        valor: Number(localPayment.amount || 0),
        metodo: leitura.metodo || leitura.tipo,
        origem: "loja",
      });
      /* A visita que virou venda: fecha a rota daquele navegador. */
      await registrarCompraNaRota(admin as never, {
        tenantId: localPayment.user_id,
        paymentId: localPayment.id,
        valor: Number(localPayment.amount || 0),
      });
    } catch {
      /* O pagamento ja esta registrado; o aviso e conveniencia. */
    }
  }

  /*
    Dinheiro saindo merece o mesmo aviso que dinheiro entrando.

    A devolução e a contestação já desfazem o pagamento no pedido sozinhas (o
    gatilho da migration 161 tira o valor do caixa e recalcula o pedido); o que
    falta é a loja saber, antes de produzir ou entregar. Só na virada de
    "aprovado" para devolvido, para o mesmo aviso não chegar duas vezes.
  */
  if ((leitura.status === "refunded" || leitura.status === "charged_back") && String(localPayment.status || "") === "approved") {
    try {
      const dinheiro = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(
        Number(localPayment.amount || 0)
      );
      const { data: pedido } = await admin
        .from("catalog_orders")
        .select("number")
        .eq("id", String(localPayment.catalog_order_id || ""))
        .maybeSingle();
      const numero = (pedido as { number?: number | null } | null)?.number;
      await avisarLojaPorPush(admin as never, {
        tenantId: localPayment.user_id,
        titulo: leitura.status === "charged_back" ? "Contestação no cartão" : "Pagamento devolvido",
        corpo: `${dinheiro}${numero ? ` · Pedido #${numero}` : ""} · O valor saiu do pedido e do caixa. Confira antes de entregar.`,
        rota: "/dashboard/catalog/orders",
        tag: `devolucao-${localPayment.id}`,
      });
    } catch {
      /* O estorno já foi registrado pelo banco; o aviso é conveniência. */
    }
  }

  return { atualizado: true as const, status: leitura.status };
}
