import type { SupabaseClient } from "@supabase/supabase-js";
import {
  COLUNAS_DO_CUPOM,
  avaliarCupom,
  escolherCupons,
  fraseDoCupom,
  lerCupom,
  nomeDoCupom,
  rotuloDoBeneficio,
  situacaoDoCupom,
  vagaDoCupom,
  valeNoDiaENoHorario,
  type Cupom,
  type CupomAvaliado,
  type ItemDoCupom,
  type NomesDoCupom,
  type VagaDoCupom,
} from "@/lib/regras-do-cupom";
import { calculateItemTotal, normalizeText, type ItemDoPedido } from "@/lib/pedido-da-loja.server";

/*
  Os cupons do pedido, vistos do servidor da loja.

  Até dois por pedido, um em cada vaga: um de desconto (percentual ou valor)
  e um de frete grátis (migration 202). As regras moram em
  lib/regras-do-cupom.ts (puras, testadas). Aqui o que depende do banco: ler
  os digitados e os automáticos da loja, as categorias dos produtos, os usos
  (limites) e se o cliente já comprou; e a trava dos usos.

  O fluxo de quem grava pedido:
    1. calcularCuponsDoPedido   quais cupons ficam e quanto dão (nada gravado)
    2. reservarUsosDosCupons    o banco confere os limites de novo, com as
                                linhas dos cupons presas, e grava os usos dos
                                dois numa transação só (ou nenhum)
    3. grava o pedido
    4. ligarUsosAoPedido        os usos apontam para o pedido
  Se o pedido não for gravado, soltarReservasDosCupons apaga os usos na hora
  (e reserva órfã, sem pedido, deixa de contar sozinha em 15 minutos).
*/

export type FreteParaOCupom = { valor: number; cep: string; retirada: boolean } | null;

export type CupomAplicado = {
  cupom: Cupom;
  code: string;
  nome: string;
  vaga: VagaDoCupom;
  automatico: boolean;
  desconto: number;
  descontoNoFrete: number;
  /** Frete grátis escolhido antes do frete: o valor sai quando o frete for escolhido. */
  esperandoFrete: boolean;
};

export type CuponsDoPedido = {
  aplicados: CupomAplicado[];
  /** Soma dos descontos nos produtos (só o cupom de desconto dá). */
  desconto: number;
  /** Soma do que sai do frete (só o cupom de frete grátis dá). */
  descontoNoFrete: number;
  /** Por que um digitado válido não ficou (o automático era melhor, não combina). */
  avisos: string[];
  /** Os digitados que não valem, com a razão. */
  erros: Array<{ code: string; mensagem: string }>;
  /** Cada código mandado: a vaga dele (null = não existe) e se ficou no pedido. */
  digitados: Array<{ code: string; vaga: VagaDoCupom | null; aplicado: boolean; erro: string | null }>;
};

export const SEM_CUPONS: CuponsDoPedido = { aplicados: [], desconto: 0, descontoNoFrete: 0, avisos: [], erros: [], digitados: [] };

/** O código como a loja grava: maiúsculas, sem espaço nas pontas. */
export function codigoDigitado(valor: unknown) {
  return normalizeText(valor).toUpperCase().slice(0, 60);
}

/**
 * Os códigos que o carrinho mandou, na ordem: `couponCodes` (lista, o
 * carrinho de agora) e `couponCode` (um só, o carrinho de antes do deploy).
 * No máximo quatro, sem repetir.
 */
export function codigosDoCorpo(corpo: { couponCodes?: unknown; couponCode?: unknown } | null | undefined): string[] {
  const lista = Array.isArray(corpo?.couponCodes) ? (corpo?.couponCodes as unknown[]) : [];
  const todos = [...lista, corpo?.couponCode].map(codigoDigitado).filter(Boolean);
  return Array.from(new Set(todos)).slice(0, 4);
}

/** Os itens do pedido no formato da regra, com a categoria de cada produto e as mães das categorias. */
async function itensParaOCupom(admin: SupabaseClient, storeId: string, items: ItemDoPedido[]) {
  const ids = Array.from(new Set(items.map((i) => i.product_id).filter(Boolean)));
  const [produtos, categorias] = await Promise.all([
    ids.length ? admin.from("products").select("id,category_id").in("id", ids) : Promise.resolve({ data: [] as Array<{ id: string; category_id: string | null }> }),
    admin.from("categories").select("id,parent_id").eq("user_id", storeId).limit(2000),
  ]);
  const categoriaDe = new Map(((produtos.data || []) as Array<{ id: string; category_id: string | null }>).map((p) => [String(p.id), p.category_id ? String(p.category_id) : null]));
  const maes: Record<string, string | null> = {};
  for (const c of (categorias.data || []) as Array<{ id: string; parent_id: string | null }>) {
    maes[String(c.id)] = c.parent_id ? String(c.parent_id) : null;
  }
  const itens: ItemDoCupom[] = items.map((item) => ({
    product_id: item.product_id,
    category_id: categoriaDe.get(item.product_id) ?? null,
    total: calculateItemTotal(item),
    quantity: Number(item.quantity || 0),
    is_combo_item: item.is_combo_item,
  }));
  return { itens, maes };
}

/** Os digitados (pelo código exato) e os automáticos ligados da loja. */
async function cuponsCandidatos(admin: SupabaseClient, storeId: string, codigos: string[]) {
  const [digitados, automaticos] = await Promise.all([
    codigos.length
      ? admin.from("catalog_coupons").select(COLUNAS_DO_CUPOM).eq("user_id", storeId).in("code", codigos).limit(10)
      : Promise.resolve({ data: [] }),
    admin
      .from("catalog_coupons")
      .select(COLUNAS_DO_CUPOM)
      .eq("user_id", storeId)
      .eq("active", true)
      .eq("application", "automatic")
      .limit(50),
  ]);
  const achados = ((digitados.data || []) as unknown as Array<Record<string, unknown>>).map(lerCupom);
  return {
    /* Na ordem em que o cliente mandou; código que não existe vira null. */
    digitados: codigos.map((code) => ({ code, cupom: achados.find((c) => c.code === code) ?? null })),
    automaticos: ((automaticos.data || []) as unknown as Array<Record<string, unknown>>).map(lerCupom),
  };
}

async function usosDosCupons(admin: SupabaseClient, cupons: Cupom[], clienteId: string | null) {
  const comLimite = cupons.filter((c) => c.usage_limit !== null || c.usage_limit_per_client !== null || c.audience === "first_purchase");
  const usos = new Map<string, { usos: number; doCliente: number }>();
  if (!comLimite.length) return usos;
  const { data } = await admin.rpc("contar_usos_dos_cupons", { p_ids: comLimite.map((c) => c.id), p_cliente: clienteId });
  for (const linha of (data || []) as Array<{ coupon_id: string; usos: number; usos_do_cliente: number }>) {
    usos.set(String(linha.coupon_id), { usos: Number(linha.usos) || 0, doCliente: Number(linha.usos_do_cliente) || 0 });
  }
  return usos;
}

const INEXISTENTE = "Cupom inválido ou inativo.";

/**
 * Quais cupons ficam no pedido e quanto dão. Nada é gravado aqui: serve para a
 * prévia do carrinho e para quem grava o pedido (que depois reserva os usos).
 */
export async function calcularCuponsDoPedido(
  admin: SupabaseClient,
  params: {
    storeId: string;
    codigos: string[];
    items: ItemDoPedido[];
    cliente: { id: string } | null;
    frete: FreteParaOCupom;
    /** O pedido em aberto que está tentando de novo (fica fora de "já comprou"). */
    pedidoEmAberto?: string | null;
    agora?: number;
  }
): Promise<CuponsDoPedido> {
  const codigos = Array.from(new Set(params.codigos.map(codigoDigitado).filter(Boolean))).slice(0, 4);
  const agora = params.agora ?? Date.now();
  const { digitados, automaticos } = await cuponsCandidatos(admin, params.storeId, codigos);
  const existentes = digitados.map((d) => d.cupom).filter((c): c is Cupom => Boolean(c));
  if (!existentes.length && !automaticos.length) {
    if (!codigos.length) return SEM_CUPONS;
    return {
      ...SEM_CUPONS,
      erros: codigos.map((code) => ({ code, mensagem: INEXISTENTE })),
      digitados: codigos.map((code) => ({ code, vaga: null, aplicado: false, erro: INEXISTENTE })),
    };
  }

  const todos = [...existentes, ...automaticos.filter((a) => !existentes.some((e) => e.id === a.id))];
  const clienteId = params.cliente?.id || null;
  const precisaSaberSeComprou = Boolean(clienteId) && todos.some((c) => c.audience === "first_purchase");
  const [{ itens, maes }, usos, jaComprou] = await Promise.all([
    itensParaOCupom(admin, params.storeId, params.items),
    usosDosCupons(admin, todos, clienteId),
    precisaSaberSeComprou
      ? admin
          .rpc("cliente_ja_comprou_na_loja", { p_loja: params.storeId, p_cliente: clienteId, p_excluir: params.pedidoEmAberto || null })
          .then((r) => r.data === true)
      : Promise.resolve(false),
  ]);

  const avaliar = (cupom: Cupom): CupomAvaliado => ({
    cupom,
    avaliacao: avaliarCupom(cupom, {
      itens,
      maes,
      cliente: params.cliente,
      jaComprou,
      usos: usos.get(cupom.id)?.usos ?? 0,
      usosDoCliente: usos.get(cupom.id)?.doCliente ?? 0,
      frete: params.frete,
      agora,
    }),
  });

  const avaliadosDigitados: CupomAvaliado[] = digitados.map(({ code, cupom }) =>
    cupom ? avaliar(cupom) : { cupom: lerCupom({ code, active: false }), avaliacao: { ok: false, motivo: "inativo", mensagem: INEXISTENTE } }
  );
  const escolha = escolherCupons(
    avaliadosDigitados,
    automaticos.filter((a) => !existentes.some((e) => e.id === a.id)).map(avaliar)
  );
  const aplicados: CupomAplicado[] = escolha.escolhidos.map((e) => ({
    cupom: e.cupom,
    code: e.cupom.code,
    nome: nomeDoCupom(e.cupom),
    vaga: vagaDoCupom(e.cupom),
    automatico: e.automatico,
    desconto: e.desconto,
    descontoNoFrete: e.descontoNoFrete,
    esperandoFrete: e.esperandoFrete,
  }));
  const centavos = (n: number) => Number(n.toFixed(2));
  return {
    aplicados,
    desconto: centavos(aplicados.reduce((s, a) => s + a.desconto, 0)),
    descontoNoFrete: centavos(aplicados.reduce((s, a) => s + a.descontoNoFrete, 0)),
    avisos: escolha.avisos,
    erros: escolha.erros,
    digitados: digitados.map(({ code, cupom }) => ({
      code,
      vaga: cupom ? vagaDoCupom(cupom) : null,
      aplicado: aplicados.some((a) => a.code === code),
      erro: escolha.erros.find((e) => e.code === code)?.mensagem ?? null,
    })),
  };
}

/** A recusa dos códigos que não valem: a razão de um, ou "Cupom X: razão" de cada. */
export function mensagemDosErros(cupons: CuponsDoPedido) {
  if (cupons.erros.length === 1) return cupons.erros[0].mensagem;
  return cupons.erros.map((e) => `Cupom ${e.code}: ${e.mensagem}`).join(" ");
}

const MENSAGEM_DA_TRAVA: Record<string, string> = {
  inativo: "Cupom inválido ou inativo.",
  esgotado: "acabou de esgotar. Tire o cupom e tente de novo.",
  limite_do_cliente: "você já usou este cupom.",
  precisa_entrar: "entre na sua conta para usar este cupom.",
  cliente_fora: "este cupom não está disponível para a sua conta.",
  ja_comprou: "este cupom é só para a primeira compra na loja.",
  cupons_demais: "um pedido leva no máximo um cupom de desconto e um de frete grátis.",
};

/**
 * Reserva os usos dos cupons do pedido, todos ou nenhum (a trava do banco:
 * pedidos ao mesmo tempo não passam do limite, e se o segundo cupom for
 * recusado o primeiro não fica gravado). Devolve os ids dos usos. Lança com a
 * mensagem para o cliente quando o banco recusa.
 */
export async function reservarUsosDosCupons(
  admin: SupabaseClient,
  cupons: CuponsDoPedido,
  params: { clienteId: string | null; trocarPedido?: string | null }
): Promise<string[]> {
  if (!cupons.aplicados.length) return [];
  const { data, error } = await admin.rpc("reservar_usos_dos_cupons", {
    p_cupons: cupons.aplicados.map((a) => ({
      coupon_id: a.cupom.id,
      desconto: a.desconto,
      desconto_no_frete: a.descontoNoFrete,
      automatico: a.automatico,
    })),
    p_cliente: params.clienteId,
    p_trocar_pedido: params.trocarPedido || null,
  });
  if (error) throw new Error("Não foi possível conferir o cupom agora. Tente de novo.");
  const resposta = (data || {}) as { ok?: boolean; ids?: unknown; motivo?: string; coupon_id?: string };
  if (!resposta.ok) {
    const qual = cupons.aplicados.find((a) => a.cupom.id === resposta.coupon_id);
    const motivo = MENSAGEM_DA_TRAVA[String(resposta.motivo)] || "este cupom não vale para este pedido.";
    if (resposta.motivo === "inativo") throw new Error(qual ? `Cupom ${qual.code} inválido ou inativo.` : motivo);
    throw new Error(qual ? `Cupom ${qual.code}: ${motivo}` : motivo.charAt(0).toUpperCase() + motivo.slice(1));
  }
  return (Array.isArray(resposta.ids) ? resposta.ids : []).map(String);
}

export async function ligarUsosAoPedido(admin: SupabaseClient, usos: string[], pedidoId: string) {
  if (!usos.length) return;
  await admin.from("catalog_coupon_uses").update({ catalog_order_id: pedidoId }).in("id", usos);
}

export async function soltarReservasDosCupons(admin: SupabaseClient, usos: string[]) {
  if (!usos.length) return;
  await admin.from("catalog_coupon_uses").delete().in("id", usos).is("catalog_order_id", null);
}

/** O pedido em aberto tentou de novo sem cupom: os usos que ele segurava saem. */
export async function soltarUsoDoPedidoEmAberto(admin: SupabaseClient, pedidoId: string | null | undefined) {
  if (!pedidoId) return;
  await admin.rpc("soltar_uso_do_cupom_do_pedido", { p_pedido: pedidoId });
}

/** O código que vai em catalog_orders.coupon_code: "BEMVINDO10 + FRETEGRATIS". */
export function codigoDoPedido(cupons: CuponsDoPedido) {
  return cupons.aplicados.map((a) => a.code).join(" + ");
}

/** Os códigos por vaga (para as linhas "Desconto (X)" e "Frete grátis (Y)"). */
export function codigosPorVaga(cupons: CuponsDoPedido) {
  return {
    desconto: cupons.aplicados.find((a) => a.vaga === "desconto")?.code ?? "",
    frete: cupons.aplicados.find((a) => a.vaga === "frete")?.code ?? "",
  };
}

/**
 * Os cupons como vão no cart_payload do pedido. `coupons` traz os dois;
 * `coupon` continua existindo (o de desconto, ou o de frete quando é o
 * único) para quem lia o formato de um cupom só.
 */
export function cuponsNoPedido(cupons: CuponsDoPedido) {
  const linhas = cupons.aplicados.map((a) => ({
    id: a.cupom.id,
    code: a.code,
    name: a.nome,
    type: a.cupom.type,
    automatic: a.automatico,
    discount: a.desconto,
    shipping_discount: a.descontoNoFrete,
  }));
  return {
    coupon: linhas.find((l) => l.type !== "free_shipping") ?? linhas[0] ?? null,
    coupons: linhas,
  };
}

/** Os cupons que a loja deixou à vista, valendo agora, para o carrinho oferecer. */
export async function cuponsAVista(admin: SupabaseClient, storeId: string, agora = Date.now()) {
  const { data } = await admin
    .from("catalog_coupons")
    .select(COLUNAS_DO_CUPOM)
    .eq("user_id", storeId)
    .eq("active", true)
    .eq("show_in_store", true)
    .eq("application", "code")
    .order("created_at", { ascending: false })
    .limit(20);
  const cupons = ((data || []) as unknown as Array<Record<string, unknown>>)
    .map(lerCupom)
    .filter((c) => situacaoDoCupom(c, agora) === "ativo" && valeNoDiaENoHorario(c, agora).ok)
    .slice(0, 6);
  const nomes = await nomesDoAlcance(admin, cupons);
  return cupons.map((c) => ({ code: c.code, nome: nomeDoCupom(c), rotulo: rotuloDoBeneficio(c), frase: fraseDoCupom(c, agora, nomes), vaga: vagaDoCupom(c) }));
}

/** Os nomes das categorias e produtos que os cupons citam (para "10% em Canecas"). */
export async function nomesDoAlcance(admin: SupabaseClient, cupons: Cupom[]): Promise<NomesDoCupom> {
  const categorias = Array.from(new Set(cupons.flatMap((c) => [...c.scope_category_ids, ...c.exclude_category_ids]))).slice(0, 200);
  const produtos = Array.from(new Set(cupons.flatMap((c) => [...c.scope_product_ids, ...c.exclude_product_ids]))).slice(0, 200);
  const [cats, prods] = await Promise.all([
    categorias.length ? admin.from("categories").select("id,name").in("id", categorias) : Promise.resolve({ data: [] }),
    produtos.length ? admin.from("products").select("id,name").in("id", produtos) : Promise.resolve({ data: [] }),
  ]);
  const mapa = (linhas: unknown) =>
    Object.fromEntries(((linhas || []) as Array<{ id: string; name: string | null }>).map((l) => [String(l.id), String(l.name || "").trim()]).filter(([, n]) => n));
  return { categorias: mapa(cats.data), produtos: mapa(prods.data) };
}
