import type { SupabaseClient } from "@supabase/supabase-js";
import { NextResponse } from "next/server";
import { buildPublicUrl, resolvePublicOrigin } from "@/lib/request-origin";
import {
  detectDocumentType,
  extractMercadoPagoQrData,
  fetchMercadoPagoApi,
  getActiveMercadoPagoCredentials,
  normalizeMercadoPagoPaymentMethod,
  normalizeMercadoPagoPaymentStatus,
  parseMercadoPagoSettings,
  splitPersonName,
} from "@/lib/mercado-pago";
import {
  billingTypeDoMeio,
  clienteDoAsaas,
  cobrancaComRetornoOpcional,
  credenciaisAtivas as credenciaisDoAsaas,
  estadoDoPagamento as estadoDoAsaas,
  lerConfiguracaoDoAsaas,
  linhaDigitavelDoBoleto,
  metodoDoPagamento as metodoDoAsaas,
  qrDoPix,
  vencimentoNoBrasil,
} from "@/lib/asaas";
import { provedorDoMeio, carregarPagamento, type Meio } from "@/lib/provedor-de-pagamento";
import { applyApprovedCatalogPayment } from "@/lib/catalog-payment-processing";
import { conferirEstoqueAntesDeCobrar } from "@/lib/estoque-antes-de-cobrar";

/*
 * A cobranca de um pedido da loja (Pix, cartao ou boleto, no Mercado Pago ou
 * no Asaas). Saiu de dentro de /api/catalog/payment/submit, sem mudar nada,
 * para a compra dos produtos digitais cobrar pelo mesmo caminho: quem chama ja
 * conferiu quem e o comprador e que o pedido e dele.
 */

export type PaymentSubmitBody = {
  slug?: string;
  orderId?: string;
  meio?: string;
  paymentType?: string;
  selectedPaymentMethod?: string;
  formData?: Record<string, unknown>;
  additionalData?: Record<string, unknown> | null;
};

type MercadoPagoPaymentResponse = Record<string, unknown> & {
  id?: unknown;
  status?: unknown;
  status_detail?: unknown;
  payment_method_id?: unknown;
  payment_type_id?: unknown;
  date_of_expiration?: unknown;
};

type CobrancaDoAsaas = Record<string, unknown> & {
  id?: unknown;
  status?: unknown;
  billingType?: unknown;
  /* A pagina de fatura que o proprio Asaas hospeda. E saida quando a busca do
     QR falha, e e o caminho INTEIRO do cartao: e nela que o comprador digita
     o cartao, fora do nosso servidor. */
  invoiceUrl?: unknown;
  /* O PDF do boleto. */
  bankSlipUrl?: unknown;
};

export function normalizeText(value: unknown) {
  return String(value || "").trim();
}

/*
  Qual meio o cliente escolheu.

  A tela manda `meio` desde que os provedores passaram a ser por meio. Mas a
  derivacao a partir do que o Mercado Pago sempre mandou continua aqui, porque
  ela e a unica coisa que segura uma aba velha aberta no navegador de alguem no
  meio de uma compra.
*/
function meioDoPedido(body: PaymentSubmitBody | null): Meio {
  const explicito = normalizeText(body?.meio).toLowerCase();
  if (explicito === "pix" || explicito === "cartao" || explicito === "boleto") return explicito;

  const formData = body?.formData && typeof body.formData === "object" ? body.formData : {};
  const tipo = normalizeText(body?.paymentType).toLowerCase();
  const metodo = normalizeText((formData as Record<string, unknown>).payment_method_id).toLowerCase();

  if (metodo === "pix" || tipo === "bank_transfer") return "pix";
  if (tipo === "ticket" || metodo.startsWith("bol")) return "boleto";
  return "cartao";
}

function isLocalNotificationOrigin(origin: string) {
  try {
    const url = new URL(origin);
    const hostname = url.hostname.trim().toLowerCase();
    return hostname === "localhost" || hostname === "127.0.0.1" || hostname.endsWith(".localhost");
  } catch {
    return true;
  }
}

export type PedidoParaCobrar = {
  id: string;
  user_id: string;
  number: number | string | null;
  total: number | string | null;
  currency: string | null;
  customer_name: string | null;
  cart_payload: unknown;
};

export async function cobrarPedidoDaLoja(params: {
  request: Request;
  admin: SupabaseClient;
  body: PaymentSubmitBody | null;
  slug: string;
  order: PedidoParaCobrar;
  /* O cliente do pedido, ja conferido por quem chama. */
  clienteId: string;
  /* Para onde volta quem paga o cartao fora da loja (Asaas); sem isto, a
     confirmacao da finalizacao de sempre. */
  voltaDoCartao?: string;
  /* Cria o pedido interno quando aprovado mesmo com "criar pedido ao aprovar"
     desligado (a compra digital precisa do pedido para entregar). */
  sempreCriarPedido?: boolean;
}): Promise<NextResponse> {
  const { request, admin, body, slug, order, clienteId: clienteDoPedido } = params;
  /*
    Qual provedor cobra nesta loja.

    A regra mora num lugar so — `provedorDaLoja` — porque a vitrine ja fez a
    mesma pergunta antes de mostrar o botao de pagar. Se as duas respostas
    divergissem, o cliente veria um caminho e seria cobrado por outro.

    O selecionador so devolve integracao com credencial completa e checkout
    aberto, entao daqui para baixo nao existe o caso de "achou, mas nao da
    para cobrar".
  */
  const meio = meioDoPedido(body);
  const escolhido = provedorDoMeio(await carregarPagamento(admin as never, String(order.user_id)), "loja", meio);
  if (!escolhido) {
    /* Nenhum provedor disponivel faz este meio nesta loja. A tela nem deveria
       ter oferecido o botao, mas uma aba velha ainda pode chegar aqui. */
    const nome = meio === "pix" ? "Pix" : meio === "boleto" ? "boleto" : "cartão";
    return NextResponse.json({ error: `Esta loja nao esta aceitando ${nome} agora.` }, { status: 400 });
  }

  const integration = escolhido.integracao;
  const ehAsaas = escolhido.provedor === "asaas";

  /* Os tres campos que os dois provedores tem com o mesmo nome e o mesmo
     significado: onde cobrar, em que conta dar baixa, e se a baixa e
     automatica. O resto de cada configuracao so existe dentro do seu ramo. */
  const configDoProvedor = ehAsaas
    ? lerConfiguracaoDoAsaas(integration.settings)
    : parseMercadoPagoSettings(integration.settings);

  const formData = body?.formData && typeof body.formData === "object" ? body.formData : {};
  const payerInput =
    formData.payer && typeof formData.payer === "object" && !Array.isArray(formData.payer)
      ? (formData.payer as Record<string, unknown>)
      : {};
  const payerIdentification =
    payerInput.identification && typeof payerInput.identification === "object" && !Array.isArray(payerInput.identification)
      ? (payerInput.identification as Record<string, unknown>)
      : {};
  const profileInput =
    order.cart_payload && typeof order.cart_payload === "object" && !Array.isArray(order.cart_payload)
      ? ((order.cart_payload as Record<string, unknown>).client_profile as Record<string, unknown> | undefined)
      : undefined;
  const orderCartPayload =
    order.cart_payload && typeof order.cart_payload === "object" && !Array.isArray(order.cart_payload)
      ? (order.cart_payload as Record<string, unknown>)
      : null;

  const customerName = normalizeText(order.customer_name || profileInput?.name || "Cliente");
  const email = normalizeText(payerInput.email || profileInput?.email);
  const document = normalizeText(payerIdentification.number || profileInput?.document || orderCartPayload?.client_document).replace(/\D/g, "");

  if (!email) {
    return NextResponse.json({ error: "Informe um e-mail valido para pagar." }, { status: 400 });
  }

  if (!document) {
    return NextResponse.json({ error: "Informe um CPF/CNPJ valido para pagar." }, { status: 400 });
  }

  const { firstName, lastName } = splitPersonName(customerName);
  /* O valor cobrado e o total do pedido gravado no servidor, nunca o que o
     navegador manda. O valor que o formulario de cartao traz so serve para
     conferir: se divergir, o pedido mudou (ou alguem mexeu) e o pagamento
     nao segue. */
  const amount = Number(order.total || 0);
  if (!Number.isFinite(amount) || amount <= 0) {
    return NextResponse.json({ error: "Valor invalido para pagamento." }, { status: 400 });
  }
  const valorDoNavegador = Number(formData?.transaction_amount);
  if (Number.isFinite(valorDoNavegador) && valorDoNavegador > 0 && Math.abs(valorDoNavegador - amount) > 0.05) {
    return NextResponse.json({ error: "O valor do pedido mudou. Recarregue a página e tente de novo." }, { status: 400 });
  }

  /* O estoque é conferido de novo na hora de cobrar: entre o carrinho e o
     pagamento (ou numa nova tentativa, horas depois) a última peça pode ter
     saído. Cobrar sem ter o que entregar nunca. */
  const { data: itensDoPedido } = await admin
    .from("catalog_order_items")
    .select("product_id,quantity,selected_options,width_m,height_m,title")
    .eq("catalog_order_id", order.id);
  const faltaEstoque = await conferirEstoqueAntesDeCobrar(admin as never, String(order.user_id), itensDoPedido ?? []);
  if (faltaEstoque) {
    return NextResponse.json({ error: faltaEstoque, code: "estoque_insuficiente" }, { status: 409 });
  }

  const idempotencyKey = crypto.randomUUID();
  const externalReference = `catalog_order:${order.id}:attempt:${idempotencyKey}`;
  const now = new Date().toISOString();
  /* O que a restricao de `payment_method` aceita: `ticket` e o boleto. */
  const paymentMethod = meio === "pix" ? "pix" : meio === "boleto" ? "ticket" : "credit_card";
  const publicOrigin = resolvePublicOrigin(request);
  /*
    O endereco do aviso.

    No Mercado Pago ele vai dentro de cada pagamento. No Asaas ele e
    registrado uma vez na conta do lojista, quando ele salva a chave, e por
    isso aqui so serve para a auditoria saber para onde o aviso deveria ir.
    Em maquina local nao existe endereco publico, e a conferencia ativa de
    status cobre a falta.
  */
  const notificationUrl = isLocalNotificationOrigin(publicOrigin)
    ? null
    : buildPublicUrl(
        request,
        ehAsaas ? `/api/webhooks/asaas/${integration.id}` : "/api/webhooks/mercado-pago"
      ).toString();

  await admin
    .from("catalog_order_payments")
    .update({
      status: "cancelled",
      provider_status: "replaced_by_new_attempt",
      provider_status_detail: "customer_selected_another_payment_attempt",
      cancelled_at: now,
      updated_at: now,
    })
    .eq("catalog_order_id", order.id)
    .eq("status", "pending");

  const { data: localPayment, error: localPaymentError } = await admin
    .from("catalog_order_payments")
    .insert({
      catalog_order_id: order.id,
      user_id: order.user_id,
      integration_id: integration.id,
      provider: escolhido.provedor,
      status: "pending",
      external_reference: externalReference,
      checkout_environment: configDoProvedor.environment,
      payment_method: paymentMethod,
      payment_type: normalizeText(body?.paymentType || body?.selectedPaymentMethod || ""),
      amount,
      currency: normalizeText(order.currency) || "BRL",
      description: `Pedido do catalogo #${order.number}`,
      payer_name: customerName,
      payer_email: email,
      payer_document: document,
      payer_document_type: detectDocumentType(document),
      idempotency_key: idempotencyKey,
      notification_url: notificationUrl,
      metadata: {
        slug,
        client_id: clienteDoPedido,
        bank_account_id: configDoProvedor.bank_account_id,
      },
      raw_request: {
        paymentType: body?.paymentType || null,
        selectedPaymentMethod: body?.selectedPaymentMethod || null,
        formData,
        additionalData: body?.additionalData || null,
      },
      created_at: now,
      updated_at: now,
    })
    .select("id")
    .single();

  if (localPaymentError || !localPayment?.id) {
    return NextResponse.json({ error: localPaymentError?.message || "Nao foi possivel iniciar o pagamento." }, { status: 500 });
  }

  /*
    A COBRANCA NO PROVEDOR.

    Os dois ramos abaixo terminam no mesmo formato: o id, o estado traduzido,
    o meio e o que a tela precisa desenhar. Gravar, avisar e dar baixa e um
    caminho so, logo adiante — e e por isso que a diferenca entre Mercado
    Pago e Asaas morre aqui dentro, e nao se espalha pelo resto da rota.
  */
  type CobrancaCriada = {
    id: string;
    status: string;
    providerStatus: string | null;
    providerStatusDetail: string | null;
    metodo: string;
    tipo: string | null;
    expiraEm: string | null;
    qrCode: string | null;
    qrCodeBase64: string | null;
    ticketUrl: string | null;
    barcode: string | null;
    /* Para onde mandar o comprador concluir o pagamento, quando ele se
       conclui fora da loja. So a fatura hospedada do Asaas preenche isto;
       tudo que se paga dentro da loja deixa em `null`. */
    urlDePagamento: string | null;
    bruto: Record<string, unknown>;
  };

  let cobranca: CobrancaCriada;

  if (ehAsaas) {
    const configAsaas = lerConfiguracaoDoAsaas(integration.settings);
    const credenciais = credenciaisDoAsaas(configAsaas);
    if (!credenciais) {
      return NextResponse.json({ error: "As credenciais do Asaas desta loja estao incompletas." }, { status: 400 });
    }
    /* O Asaas nao cobra de um e-mail solto: toda cobranca aponta para um
       cliente cadastrado na conta do lojista. A referencia e o id do cliente
       no portal, para o cadastro la e a ficha aqui contarem a mesma historia. */
    const clienteId = await clienteDoAsaas(credenciais, {
      documento: document,
      nome: customerName,
      email,
      telefone: normalizeText(profileInput?.phone),
      referencia: String(clienteDoPedido),
    });

    /*
      PARA ONDE O COMPRADOR VOLTA DEPOIS DE PAGAR NO ASAAS.

      So o cartao usa isto, porque so ele sai da loja. Precisa ser a origem
      publica de verdade: atras do proxy, `new URL(req.url).origin` devolve
      localhost, e o Asaas mandaria o cliente para a maquina do servidor em
      vez da loja. Com `autoRedirect`, ele volta sozinho assim que paga.
    */
    const voltaParaALoja = new URL(
      params.voltaDoCartao ??
        `/${encodeURIComponent(slug)}/checkout/confirmacao?orderId=${encodeURIComponent(String(order.id))}&orderNumber=${encodeURIComponent(String(order.number))}`,
      publicOrigin
    ).toString();

    const billingType = billingTypeDoMeio(meio);

    const criada = await cobrancaComRetornoOpcional<CobrancaDoAsaas>(
      credenciais,
      {
        customer: clienteId,
        billingType,
        value: amount,
        /* Cartao vence hoje: a fatura hospedada e para pagar agora, com o
           comprador na frente da tela. O prazo do lojista vale para Pix e
           boleto, que sao promessas de pagamento. */
        dueDate: vencimentoNoBrasil(meio === "cartao" ? 0 : configAsaas.dias_para_vencimento),
        description: `Pedido do catalogo #${order.number}`,
        externalReference,
      },
      /* So o cartao sai da loja, entao so ele tem para onde voltar. */
      meio === "cartao" ? voltaParaALoja : null
    );

    const idDaCobranca = normalizeText(criada.id);
    if (!idDaCobranca) {
      return NextResponse.json({ error: "O Asaas nao devolveu a cobranca criada." }, { status: 502 });
    }

    const faturaHospedada = normalizeText(criada.invoiceUrl) || null;

    /*
      O QR e a linha digitavel moram em OUTRA chamada, e ela pode falhar
      sozinha.

      Se falhar, a cobranca ja existe e nao pode ser jogada fora: o cliente
      segue pela fatura do Asaas, que o proprio provedor hospeda. Perder o
      codigo atrapalha; perder a cobranca criaria uma segunda tentativa
      cobrando de novo o mesmo pedido.
    */
    let qrCode: string | null = null;
    let qrCodeBase64: string | null = null;
    let expiraEm: string | null = null;
    let linhaDigitavel: string | null = null;
    let ticketUrl = faturaHospedada;
    let urlDePagamento: string | null = null;

    if (meio === "pix") {
      const qr = await qrDoPix(credenciais, idDaCobranca);
      qrCode = qr?.copiaECola ?? null;
      qrCodeBase64 = qr?.imagemBase64 ?? null;
      expiraEm = qr?.expiraEm ?? null;
    } else if (meio === "boleto") {
      const boleto = await linhaDigitavelDoBoleto(credenciais, idDaCobranca);
      linhaDigitavel = boleto?.linhaDigitavel ?? null;
      /* O PDF e o que a pessoa espera ver ao clicar em "ver boleto". A
         fatura fica de reserva para quando o Asaas nao devolver o PDF. */
      ticketUrl = normalizeText(criada.bankSlipUrl) || faturaHospedada;
    } else {
      /*
        CARTAO: A LOJA NAO VE O CARTAO.

        O Asaas nao tokeniza no navegador, entao a unica forma de aceitar
        cartao por ele sem o numero passar pelo nosso servidor e mandar o
        comprador para a fatura que eles hospedam. Sem essa URL nao ha
        caminho nenhum, e falhar aqui e melhor que devolver uma tela muda.
      */
      if (!faturaHospedada) {
        return NextResponse.json(
          { error: "O Asaas nao devolveu o endereco de pagamento do cartao." },
          { status: 502 }
        );
      }
      urlDePagamento = faturaHospedada;
    }

    cobranca = {
      id: idDaCobranca,
      status: estadoDoAsaas(criada.status),
      providerStatus: normalizeText(criada.status) || null,
      providerStatusDetail: null,
      metodo: metodoDoAsaas(criada.billingType) || metodoDoAsaas(billingType),
      tipo: normalizeText(criada.billingType) || billingType,
      expiraEm,
      qrCode,
      qrCodeBase64,
      ticketUrl,
      barcode: linhaDigitavel,
      urlDePagamento,
      bruto: criada,
    };
  } else {

  const settings = parseMercadoPagoSettings(integration.settings);
  const credentials = getActiveMercadoPagoCredentials(settings);
  if (!credentials) {
    return NextResponse.json({ error: "As credenciais do Mercado Pago desta loja estao incompletas." }, { status: 400 });
  }

  const requestPayload: Record<string, unknown> = {
    transaction_amount: amount,
    description: `Pedido do catalogo #${order.number}`,
    payment_method_id: normalizeText(formData?.payment_method_id || ""),
    payer: {
      email,
      first_name: firstName,
      last_name: lastName,
      identification: {
        type: detectDocumentType(document),
        number: document,
      },
      address: {
        zip_code: normalizeText(profileInput?.address_zip).replace(/\D/g, ""),
        street_name: normalizeText(profileInput?.address_street),
        street_number: normalizeText(profileInput?.address_number),
        neighborhood: normalizeText(profileInput?.address_neighborhood),
        city: normalizeText(profileInput?.address_city),
        federal_unit: normalizeText(profileInput?.address_state),
      },
    },
    external_reference: externalReference,
    metadata: {
      catalog_order_id: order.id,
      catalog_order_payment_id: localPayment.id,
      slug,
    },
  };

  if (notificationUrl) {
    requestPayload.notification_url = notificationUrl;
  }

  if (settings.statement_descriptor) {
    requestPayload.statement_descriptor = settings.statement_descriptor;
  }

  if (normalizeText(formData?.token)) requestPayload.token = normalizeText(formData.token);
  if (normalizeText(formData?.issuer_id)) requestPayload.issuer_id = normalizeText(formData.issuer_id);
  if (Number(formData?.installments) > 0) requestPayload.installments = Number(formData.installments);
  if (normalizeText(formData?.processing_mode)) requestPayload.processing_mode = normalizeText(formData.processing_mode);

  const mercadoPagoPayment = await fetchMercadoPagoApi<MercadoPagoPaymentResponse>("/v1/payments", credentials.accessToken, {
    method: "POST",
    body: JSON.stringify(requestPayload),
    idempotencyKey,
  });

  const qrData = extractMercadoPagoQrData(mercadoPagoPayment);
  cobranca = {
    id: normalizeText(mercadoPagoPayment.id),
    status: normalizeMercadoPagoPaymentStatus(mercadoPagoPayment.status),
    providerStatus: normalizeText(mercadoPagoPayment.status) || null,
    providerStatusDetail: normalizeText(mercadoPagoPayment.status_detail) || null,
    metodo: normalizeMercadoPagoPaymentMethod(mercadoPagoPayment.payment_method_id, mercadoPagoPayment.payment_type_id),
    tipo: normalizeText(mercadoPagoPayment.payment_type_id) || null,
    expiraEm: normalizeText(mercadoPagoPayment.date_of_expiration) || null,
    qrCode: qrData.qrCode,
    qrCodeBase64: qrData.qrCodeBase64,
    ticketUrl: qrData.ticketUrl,
    barcode: qrData.barcode,
    /* O Mercado Pago conclui tudo dentro da loja: cartao pelo formulario
       dele, Pix e boleto pelo codigo na tela. Nunca ha para onde mandar. */
    urlDePagamento: null,
    bruto: mercadoPagoPayment,
  };

  }

  const normalizedStatus = cobranca.status;
  const normalizedMethod = cobranca.metodo;
  const approvedAt = normalizedStatus === "approved" ? new Date().toISOString() : null;

  await admin
    .from("catalog_order_payments")
    .update({
      status: normalizedStatus,
      provider_status: cobranca.providerStatus,
      provider_status_detail: cobranca.providerStatusDetail,
      provider_payment_id: cobranca.id,
      payment_method: normalizedMethod,
      payment_type: cobranca.tipo,
      approved_at: approvedAt,
      rejected_at: normalizedStatus === "rejected" ? new Date().toISOString() : null,
      cancelled_at: normalizedStatus === "cancelled" ? new Date().toISOString() : null,
      expires_at: cobranca.expiraEm,
      qr_code: cobranca.qrCode,
      qr_code_base64: cobranca.qrCodeBase64,
      ticket_url: cobranca.ticketUrl,
      raw_response: cobranca.bruto,
      updated_at: new Date().toISOString(),
    })
    .eq("id", localPayment.id);

  await admin.from("catalog_payment_events").insert({
    catalog_payment_id: localPayment.id,
    user_id: order.user_id,
    provider: escolhido.provedor,
    event_type: "payment.created",
    payload: cobranca.bruto,
  });

  let approvedOrderId: string | null = null;
  let appliedPaymentId: string | null = null;
  if (normalizedStatus === "approved" && (configDoProvedor.auto_create_order_on_approved_payment || params.sempreCriarPedido)) {
    const applied = await applyApprovedCatalogPayment(admin as never, String(localPayment.id));
    approvedOrderId = applied.orderId || null;
    appliedPaymentId = applied.appliedPaymentId || null;
  }

  return NextResponse.json({
    ok: true,
    order_id: order.id,
    catalog_payment_id: localPayment.id,
    provider: escolhido.provedor,
    provider_payment_id: cobranca.id || null,
    status: normalizedStatus,
    status_detail: cobranca.providerStatusDetail,
    payment_method: normalizedMethod,
    qr_code: cobranca.qrCode,
    qr_code_base64: cobranca.qrCodeBase64,
    ticket_url: cobranca.ticketUrl,
    /* Linha digitavel: quem paga boleto no aplicativo do banco digita este
       numero, e nem sempre quer abrir o PDF. */
    barcode: cobranca.barcode,
    /* Preenchido so quando o pagamento se conclui fora da loja. A tela usa
       a presenca disto para desenhar o passo de ida ao provedor. */
    payment_url: cobranca.urlDePagamento,
    approved_order_id: approvedOrderId,
    applied_payment_id: appliedPaymentId,
  });
}
