/*
  As regras do cupom da loja, numa conta só.

  O servidor da loja (checkout, pedido pelo WhatsApp, prévia do carrinho) e o
  painel (lista de cupons, "como o cliente vê", assistente) leem daqui. Regras
  puras, sem "@/": o teste roda sozinho (regras-do-cupom.test.ts). O painel tem
  a cópia idêntica em apps/admin/src/lib/regras-do-cupom.ts (os dois apps não
  dividem pacote) e o teste do painel confere que as duas são iguais: mudou
  aqui, copie lá.

  Cupom antigo (de antes da migration 200) vem com os padrões, que são o
  comportamento de sempre: vale na loja toda, para todos, com código, sem
  limite de uso, a qualquer hora, mínimo sobre o carrinho inteiro, e as
  mesmas mensagens de erro de antes.

  Decisões
  - O desconto (percentual ou fixo) é calculado só sobre os itens que entram
    no cupom (onde vale, menos as exclusões) e nunca passa do subtotal deles.
  - O mínimo (valor e itens) olha o carrinho todo, como antes, ou só os itens
    que entram no cupom (min_scope = "eligible"), à escolha da loja.
  - Categoria escolhida vale também para as subcategorias dela.
  - Frete grátis abate o frete escolhido, todo ou até shipping_max; se a loja
    escolheu estados, só vale para CEP desses estados.
  - Até dois cupons por pedido, um em cada vaga: um de desconto (percentual
    ou valor) e um de frete grátis. Nunca dois descontos nem dois fretes.
    Em cada vaga, entre o digitado e os automáticos, fica o que dá mais ao
    cliente (o digitado ganha empate e ganha quando ainda falta escolher o
    frete para saber quanto ele vale). Com dois digitados na mesma vaga,
    vale o último. Cupom marcado "não combinar" é usado sozinho no pedido.
  - O mínimo do frete grátis olha o valor dos produtos ANTES de qualquer
    desconto (o mesmo subtotal de sempre); o desconto não tira o frete
    grátis de quem já tinha chegado ao mínimo.
  - Primeira compra, limite por cliente, só logados e clientes escolhidos
    precisam de cliente com conta na loja (o portal identifica a pessoa).
  - Dias da semana e horário são os de Brasília; faixa que vira a noite
    (22h às 2h) pertence ao dia em que começou.
*/

export type TipoDoCupom = "percent" | "value" | "free_shipping";
export type EscopoDoCupom = "all" | "categories" | "products";
export type PublicoDoCupom = "all" | "logged" | "first_purchase" | "clients";
export type AplicacaoDoCupom = "code" | "automatic";
export type BaseDoMinimo = "cart" | "eligible";

export type Cupom = {
  id: string;
  code: string;
  name: string | null;
  type: TipoDoCupom;
  value: number;
  max_discount: number | null;
  shipping_max: number | null;
  shipping_states: string[];
  min_total: number;
  min_items: number;
  min_scope: BaseDoMinimo;
  scope: EscopoDoCupom;
  scope_category_ids: string[];
  scope_product_ids: string[];
  exclude_category_ids: string[];
  exclude_product_ids: string[];
  audience: PublicoDoCupom;
  audience_client_ids: string[];
  usage_limit: number | null;
  usage_limit_per_client: number | null;
  weekdays: number[];
  hours_start: string | null;
  hours_end: string | null;
  application: AplicacaoDoCupom;
  show_in_store: boolean;
  /** Pode dividir o pedido com um cupom do outro tipo (frete grátis + desconto). */
  combinable: boolean;
  active: boolean;
  starts_at: string | null;
  ends_at: string | null;
  created_at: string | null;
};

/** As colunas que o servidor lê para conferir um cupom. */
export const COLUNAS_DO_CUPOM =
  "id,code,name,type,value,max_discount,shipping_max,shipping_states,min_total,min_items,min_scope,scope," +
  "scope_category_ids,scope_product_ids,exclude_category_ids,exclude_product_ids,audience,audience_client_ids," +
  "usage_limit,usage_limit_per_client,weekdays,hours_start,hours_end,application,show_in_store,combinable,active,starts_at,ends_at,created_at";

function numeroOuNulo(valor: unknown): number | null {
  if (valor === null || valor === undefined || valor === "") return null;
  const n = Number(valor);
  return Number.isFinite(n) ? n : null;
}

function listaDeTextos(valor: unknown): string[] {
  return Array.isArray(valor) ? valor.map((v) => String(v ?? "").trim()).filter(Boolean) : [];
}

function hora(valor: unknown): string | null {
  const texto = String(valor ?? "").trim();
  const achado = /^(\d{1,2}):(\d{2})/.exec(texto);
  if (!achado) return null;
  const h = Number(achado[1]);
  const m = Number(achado[2]);
  if (h > 23 || m > 59) return null;
  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}

function umDe<T extends string>(valor: unknown, opcoes: readonly T[], padrao: T): T {
  return (opcoes as readonly string[]).includes(String(valor)) ? (valor as T) : padrao;
}

/** A linha do banco (ou o que o painel monta) com os padrões no lugar do vazio. */
export function lerCupom(linha: Record<string, unknown>): Cupom {
  const tipo = umDe(linha.type, ["percent", "value", "free_shipping"] as const, "value");
  const dias = Array.isArray(linha.weekdays)
    ? Array.from(new Set((linha.weekdays as unknown[]).map(Number).filter((d) => Number.isInteger(d) && d >= 0 && d <= 6))).sort()
    : [];
  const positivo = (v: unknown) => {
    const n = numeroOuNulo(v);
    return n !== null && n > 0 ? n : null;
  };
  const inteiroPositivo = (v: unknown) => {
    const n = numeroOuNulo(v);
    return n !== null && n > 0 ? Math.floor(n) : null;
  };
  return {
    id: String(linha.id ?? ""),
    code: String(linha.code ?? "").trim().toUpperCase(),
    name: String(linha.name ?? "").trim() || null,
    type: tipo,
    value: Number(linha.value) || 0,
    max_discount: positivo(linha.max_discount),
    shipping_max: positivo(linha.shipping_max),
    shipping_states: listaDeTextos(linha.shipping_states).map((uf) => uf.toUpperCase()),
    min_total: Math.max(Number(linha.min_total) || 0, 0),
    min_items: Math.max(Math.floor(Number(linha.min_items) || 0), 0),
    min_scope: umDe(linha.min_scope, ["cart", "eligible"] as const, "cart"),
    scope: umDe(linha.scope, ["all", "categories", "products"] as const, "all"),
    scope_category_ids: listaDeTextos(linha.scope_category_ids),
    scope_product_ids: listaDeTextos(linha.scope_product_ids),
    exclude_category_ids: listaDeTextos(linha.exclude_category_ids),
    exclude_product_ids: listaDeTextos(linha.exclude_product_ids),
    audience: umDe(linha.audience, ["all", "logged", "first_purchase", "clients"] as const, "all"),
    audience_client_ids: listaDeTextos(linha.audience_client_ids),
    usage_limit: inteiroPositivo(linha.usage_limit),
    usage_limit_per_client: inteiroPositivo(linha.usage_limit_per_client),
    weekdays: dias,
    hours_start: hora(linha.hours_start),
    hours_end: hora(linha.hours_end),
    application: umDe(linha.application, ["code", "automatic"] as const, "code"),
    show_in_store: linha.show_in_store === true,
    combinable: linha.combinable !== false,
    active: linha.active !== false,
    starts_at: (linha.starts_at as string | null | undefined) || null,
    ends_at: (linha.ends_at as string | null | undefined) || null,
    created_at: (linha.created_at as string | null | undefined) || null,
  };
}

/* ------------------------------------------------------------ dinheiro e texto */

const MOEDA_INTEIRA = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL", minimumFractionDigits: 0, maximumFractionDigits: 0 });
const MOEDA = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" });

/** "R$ 50" quando é inteiro, "R$ 49,90" quando não é. */
export function reaisCurtos(valor: number) {
  const n = Number(valor) || 0;
  return (Number.isInteger(Math.round(n * 100) / 100) ? MOEDA_INTEIRA : MOEDA).format(n).replace(/ /g, " ");
}

/** Sempre com centavos (as mensagens de erro de antes usavam assim). */
export function reais(valor: number) {
  return MOEDA.format(Number(valor) || 0).replace(/ /g, " ");
}

function centavos(valor: number) {
  return Number((Number(valor) || 0).toFixed(2));
}

function juntar(lista: string[]) {
  if (lista.length <= 1) return lista.join("");
  return `${lista.slice(0, -1).join(", ")} e ${lista[lista.length - 1]}`;
}

/* ------------------------------------------------------------ relógio de Brasília */

const DIAS_CURTOS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

export function relogioDeBrasilia(agora: number) {
  const partes = new Intl.DateTimeFormat("en-US", {
    timeZone: "America/Sao_Paulo",
    weekday: "short",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    hourCycle: "h23",
  }).formatToParts(new Date(agora));
  const parte = (tipo: string) => partes.find((p) => p.type === tipo)?.value ?? "";
  const horas = Number(parte("hour")) % 24;
  return {
    diaDaSemana: Math.max(DIAS_CURTOS.indexOf(parte("weekday")), 0),
    minutos: horas * 60 + Number(parte("minute")),
    dia: Number(parte("day")),
    mes: Number(parte("month")),
    ano: Number(parte("year")),
  };
}

function minutosDe(h: string | null): number | null {
  if (!h) return null;
  const [hh, mm] = h.split(":").map(Number);
  return hh * 60 + mm;
}

/** O cupom vale neste dia da semana e neste horário (Brasília)? */
export function valeNoDiaENoHorario(cupom: Pick<Cupom, "weekdays" | "hours_start" | "hours_end">, agora: number): { ok: true } | { ok: false; motivo: "fora_do_dia" | "fora_do_horario" } {
  const r = relogioDeBrasilia(agora);
  let dia = r.diaDaSemana;
  const ini = minutosDe(cupom.hours_start);
  const fim = minutosDe(cupom.hours_end);
  if (ini !== null || fim !== null) {
    const comeco = ini ?? 0;
    const termino = fim ?? 24 * 60;
    if (comeco < termino) {
      if (r.minutos < comeco || r.minutos >= termino) return { ok: false, motivo: "fora_do_horario" };
    } else if (comeco > termino) {
      /* Vira a noite (22h às 2h): a madrugada é do dia em que começou. */
      if (r.minutos >= termino && r.minutos < comeco) return { ok: false, motivo: "fora_do_horario" };
      if (r.minutos < termino) dia = (dia + 6) % 7;
    }
  }
  if (cupom.weekdays.length && !cupom.weekdays.includes(dia)) return { ok: false, motivo: "fora_do_dia" };
  return { ok: true };
}

const NOMES_DOS_DIAS = ["domingos", "segundas", "terças", "quartas", "quintas", "sextas", "sábados"];

/** "de segunda a sexta", "nos fins de semana", "às terças e quintas". */
export function textoDosDias(dias: number[]): string {
  const lista = Array.from(new Set(dias)).sort();
  if (!lista.length || lista.length === 7) return "";
  const chave = lista.join(",");
  if (chave === "1,2,3,4,5") return "de segunda a sexta";
  if (chave === "0,6") return "nos fins de semana";
  if (chave === "1,2,3,4,5,6") return "de segunda a sábado";
  /* Segunda primeiro, domingo por último, como se fala. */
  const ordem = lista.slice().sort((a, b) => ((a + 6) % 7) - ((b + 6) % 7));
  const masculinos = ordem.filter((d) => d === 0 || d === 6);
  const femininos = ordem.filter((d) => d !== 0 && d !== 6);
  const partes: string[] = [];
  if (femininos.length) partes.push(`às ${juntar(femininos.map((d) => NOMES_DOS_DIAS[d]))}`);
  if (masculinos.length) partes.push(`aos ${juntar(masculinos.map((d) => NOMES_DOS_DIAS[d]))}`);
  return partes.join(" e ");
}

function horaCurta(h: string) {
  const [hh, mm] = h.split(":");
  return `${Number(hh)}h${mm !== "00" ? mm : ""}`;
}

/** "das 18h às 22h" (vazio quando vale o dia todo). */
export function textoDoHorario(cupom: Pick<Cupom, "hours_start" | "hours_end">): string {
  if (!cupom.hours_start && !cupom.hours_end) return "";
  if (cupom.hours_start && cupom.hours_end) return `das ${horaCurta(cupom.hours_start)} às ${horaCurta(cupom.hours_end)}`;
  if (cupom.hours_start) return `a partir das ${horaCurta(cupom.hours_start)}`;
  return `até as ${horaCurta(cupom.hours_end as string)}`;
}

/* ------------------------------------------------------------ CEP e estado */

const FAIXAS_DE_CEP: Array<[number, number, string]> = [
  [1000, 19999, "SP"], [20000, 28999, "RJ"], [29000, 29999, "ES"], [30000, 39999, "MG"],
  [40000, 48999, "BA"], [49000, 49999, "SE"], [50000, 56999, "PE"], [57000, 57999, "AL"],
  [58000, 58999, "PB"], [59000, 59999, "RN"], [60000, 63999, "CE"], [64000, 64999, "PI"],
  [65000, 65999, "MA"], [66000, 68899, "PA"], [68900, 68999, "AP"], [69000, 69299, "AM"],
  [69300, 69399, "RR"], [69400, 69899, "AM"], [69900, 69999, "AC"], [70000, 72799, "DF"],
  [72800, 72999, "GO"], [73000, 73699, "DF"], [73700, 76799, "GO"], [76800, 76999, "RO"],
  [77000, 77999, "TO"], [78000, 78899, "MT"], [79000, 79999, "MS"], [80000, 87999, "PR"],
  [88000, 89999, "SC"], [90000, 99999, "RS"],
];

export const ESTADOS = ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"];

/** O estado do CEP, pelas faixas dos Correios (null quando não dá para saber). */
export function ufDoCep(cep: string | null | undefined): string | null {
  const digitos = String(cep ?? "").replace(/\D/g, "");
  if (digitos.length !== 8) return null;
  const prefixo = Number(digitos.slice(0, 5));
  for (const [de, ate, uf] of FAIXAS_DE_CEP) if (prefixo >= de && prefixo <= ate) return uf;
  return null;
}

/* ------------------------------------------------------------ situação */

export type SituacaoDoCupom = "ativo" | "agendado" | "expirado" | "esgotado" | "pausado";

export const ROTULO_DA_SITUACAO: Record<SituacaoDoCupom, string> = {
  ativo: "Ativo",
  agendado: "Agendado",
  expirado: "Expirado",
  esgotado: "Esgotado",
  pausado: "Pausado",
};

/** A situação para a lista do painel. */
export function situacaoDoCupom(cupom: Pick<Cupom, "active" | "starts_at" | "ends_at" | "usage_limit">, agora: number, usos?: number | null): SituacaoDoCupom {
  if (cupom.active === false) return "pausado";
  const fim = cupom.ends_at ? new Date(cupom.ends_at).getTime() : NaN;
  if (Number.isFinite(fim) && fim < agora) return "expirado";
  if (cupom.usage_limit && typeof usos === "number" && usos >= cupom.usage_limit) return "esgotado";
  const inicio = cupom.starts_at ? new Date(cupom.starts_at).getTime() : NaN;
  if (Number.isFinite(inicio) && inicio > agora) return "agendado";
  return "ativo";
}

/* ------------------------------------------------------------ onde vale */

export type ItemDoCupom = {
  product_id: string;
  category_id: string | null;
  /** Total da linha, já com o preço conferido no servidor. */
  total: number;
  quantity: number;
  is_combo_item?: boolean;
};

function cadeiaDaCategoria(id: string | null, maes: Record<string, string | null> | undefined): string[] {
  const cadeia: string[] = [];
  let atual = id;
  while (atual && !cadeia.includes(atual) && cadeia.length < 20) {
    cadeia.push(atual);
    atual = maes?.[atual] ?? null;
  }
  return cadeia;
}

/** O item entra no cupom? (onde vale, menos as exclusões; a categoria vale para as filhas) */
export function itemEntraNoCupom(
  cupom: Pick<Cupom, "scope" | "scope_category_ids" | "scope_product_ids" | "exclude_category_ids" | "exclude_product_ids">,
  item: Pick<ItemDoCupom, "product_id" | "category_id">,
  maes?: Record<string, string | null>
): boolean {
  const cadeia = cadeiaDaCategoria(item.category_id, maes);
  if (cupom.exclude_product_ids.includes(item.product_id)) return false;
  if (cadeia.some((id) => cupom.exclude_category_ids.includes(id))) return false;
  if (cupom.scope === "products") return cupom.scope_product_ids.includes(item.product_id);
  if (cupom.scope === "categories") return cadeia.some((id) => cupom.scope_category_ids.includes(id));
  return true;
}

/** O mínimo olha só os itens que entram no cupom? (com a loja toda valendo, é o carrinho mesmo) */
export function minimoSoDosParticipantes(cupom: Pick<Cupom, "min_scope" | "scope" | "exclude_category_ids" | "exclude_product_ids">) {
  return cupom.min_scope === "eligible" && (cupom.scope !== "all" || cupom.exclude_category_ids.length > 0 || cupom.exclude_product_ids.length > 0);
}

/* ------------------------------------------------------------ a conta */

export type MotivoDoCupom =
  | "inativo"
  | "agendado"
  | "expirado"
  | "fora_do_dia"
  | "fora_do_horario"
  | "precisa_entrar"
  | "cliente_fora"
  | "ja_comprou"
  | "esgotado"
  | "limite_do_cliente"
  | "sem_itens"
  | "minimo_valor"
  | "minimo_itens"
  | "frete_fora";

export type ContextoDoCupom = {
  itens: ItemDoCupom[];
  /** Categoria -> categoria mãe (para valer nas subcategorias). */
  maes?: Record<string, string | null>;
  /** Cliente com conta na loja (portal). */
  cliente: { id: string } | null;
  /** O cliente já comprou na loja? (só importa na primeira compra) */
  jaComprou?: boolean;
  usos?: number;
  usosDoCliente?: number;
  /** O frete escolhido (null = ainda não escolheu). */
  frete: { valor: number; cep: string; retirada: boolean } | null;
  agora: number;
};

export type AvaliacaoDoCupom =
  | { ok: true; desconto: number; descontoNoFrete: number; beneficio: number; base: number; esperandoFrete: boolean }
  | { ok: false; motivo: MotivoDoCupom; mensagem: string; falta?: number };

function unidades(itens: ItemDoCupom[]) {
  return itens.filter((i) => !i.is_combo_item).reduce((soma, i) => soma + (Number(i.quantity) || 0), 0);
}

function recusa(motivo: MotivoDoCupom, mensagem: string, falta?: number): AvaliacaoDoCupom {
  return falta === undefined ? { ok: false, motivo, mensagem } : { ok: false, motivo, mensagem, falta };
}

/** Confere o cupom contra o carrinho e devolve o desconto, ou a razão de não valer. */
export function avaliarCupom(cupom: Cupom, ctx: ContextoDoCupom): AvaliacaoDoCupom {
  if (!cupom.active) return recusa("inativo", "Cupom inválido ou inativo.");
  const inicio = cupom.starts_at ? new Date(cupom.starts_at).getTime() : NaN;
  const fim = cupom.ends_at ? new Date(cupom.ends_at).getTime() : NaN;
  if (Number.isFinite(inicio) && inicio > ctx.agora) return recusa("agendado", "Este cupom ainda não começou a valer.");
  if (Number.isFinite(fim) && fim < ctx.agora) return recusa("expirado", "Este cupom já venceu.");

  const quando = valeNoDiaENoHorario(cupom, ctx.agora);
  if (!quando.ok) {
    if (quando.motivo === "fora_do_dia") {
      const horario = textoDoHorario(cupom);
      return recusa("fora_do_dia", `Este cupom vale ${textoDosDias(cupom.weekdays)}${horario ? `, ${horario}` : ""}.`);
    }
    const dias = textoDosDias(cupom.weekdays);
    return recusa("fora_do_horario", `Este cupom vale ${textoDoHorario(cupom)}${dias ? `, ${dias}` : ""}.`);
  }

  const limiteDoCliente = cupom.audience === "first_purchase" ? 1 : cupom.usage_limit_per_client;
  const exigeConta = cupom.audience !== "all" || limiteDoCliente !== null;
  if (exigeConta && !ctx.cliente) return recusa("precisa_entrar", "Entre na sua conta para usar este cupom.");
  if (cupom.audience === "clients" && !cupom.audience_client_ids.includes(String(ctx.cliente?.id))) {
    return recusa("cliente_fora", "Este cupom não está disponível para a sua conta.");
  }
  if (cupom.audience === "first_purchase" && ctx.jaComprou) {
    return recusa("ja_comprou", "Este cupom é só para a primeira compra na loja.");
  }
  if (cupom.usage_limit !== null && (ctx.usos ?? 0) >= cupom.usage_limit) {
    return recusa("esgotado", "Este cupom já foi usado o máximo de vezes.");
  }
  if (limiteDoCliente !== null && (ctx.usosDoCliente ?? 0) >= limiteDoCliente) {
    return recusa(
      "limite_do_cliente",
      limiteDoCliente === 1 ? "Você já usou este cupom." : `Você já usou este cupom ${limiteDoCliente} vezes, o máximo por cliente.`
    );
  }

  const elegiveis = ctx.itens.filter((item) => itemEntraNoCupom(cupom, item, ctx.maes));
  const base = centavos(elegiveis.reduce((soma, i) => soma + Math.max(Number(i.total) || 0, 0), 0));
  const temItem = cupom.type === "free_shipping" ? elegiveis.length > 0 : base > 0;
  if (!temItem) return recusa("sem_itens", "Nenhum produto do carrinho entra neste cupom.");

  const soParticipantes = minimoSoDosParticipantes(cupom);
  const totalDoMinimo = soParticipantes
    ? base
    : centavos(ctx.itens.reduce((soma, i) => soma + Math.max(Number(i.total) || 0, 0), 0));
  const unidadesDoMinimo = soParticipantes ? unidades(elegiveis) : unidades(ctx.itens);
  if (cupom.min_total > 0 && totalDoMinimo < cupom.min_total) {
    return recusa(
      "minimo_valor",
      soParticipantes
        ? `Este cupom vale a partir de ${reais(cupom.min_total)} em produtos participantes.`
        : `Este cupom vale a partir de ${reais(cupom.min_total)} em produtos.`,
      centavos(cupom.min_total - totalDoMinimo)
    );
  }
  if (cupom.min_items > 0 && unidadesDoMinimo < cupom.min_items) {
    const palavra = cupom.min_items === 1 ? "unidade" : "unidades";
    return recusa(
      "minimo_itens",
      soParticipantes
        ? `Este cupom vale a partir de ${cupom.min_items} ${palavra} de produtos participantes.`
        : `Este cupom vale a partir de ${cupom.min_items} ${palavra}.`,
      cupom.min_items - unidadesDoMinimo
    );
  }

  if (cupom.type === "free_shipping") {
    if (!ctx.frete) return { ok: true, desconto: 0, descontoNoFrete: 0, beneficio: 0, base, esperandoFrete: true };
    if (cupom.shipping_states.length && !ctx.frete.retirada) {
      const uf = ufDoCep(ctx.frete.cep);
      if (!uf || !cupom.shipping_states.includes(uf)) {
        return recusa("frete_fora", `O frete grátis deste cupom vale só para ${juntar(cupom.shipping_states)}.`);
      }
    }
    const valor = Math.max(Number(ctx.frete.valor) || 0, 0);
    const descontoNoFrete = centavos(Math.min(valor, cupom.shipping_max ?? valor));
    return { ok: true, desconto: 0, descontoNoFrete, beneficio: descontoNoFrete, base, esperandoFrete: false };
  }

  let desconto: number;
  if (cupom.type === "percent") {
    const pct = Math.min(Math.max(cupom.value, 0), 100);
    desconto = (base * pct) / 100;
    if (cupom.max_discount !== null) desconto = Math.min(desconto, cupom.max_discount);
  } else {
    desconto = cupom.value;
  }
  desconto = centavos(Math.min(Math.max(desconto, 0), base));
  return { ok: true, desconto, descontoNoFrete: 0, beneficio: desconto, base, esperandoFrete: false };
}

/* ------------------------------------------------------------ qual fica */

export type CupomAvaliado = { cupom: Cupom; avaliacao: AvaliacaoDoCupom };
export type CupomEscolhido = {
  cupom: Cupom;
  desconto: number;
  descontoNoFrete: number;
  esperandoFrete: boolean;
  automatico: boolean;
};

/**
 * Um cupom por pedido: o digitado ou o melhor automático, o que der mais ao
 * cliente. O digitado ganha empate e ganha quando ainda falta o frete para
 * saber quanto ele vale. Devolve também o aviso para o cliente quando o
 * digitado não ficou.
 */
export function escolherCupom(digitado: CupomAvaliado | null, automaticos: CupomAvaliado[]): { escolhido: CupomEscolhido | null; aviso: string | null } {
  const validos = automaticos
    .filter((a) => a.avaliacao.ok && (a.avaliacao.beneficio > 0 || a.avaliacao.esperandoFrete))
    .sort((a, b) => {
      const ba = a.avaliacao.ok ? a.avaliacao.beneficio : 0;
      const bb = b.avaliacao.ok ? b.avaliacao.beneficio : 0;
      if (bb !== ba) return bb - ba;
      return String(a.cupom.created_at ?? "").localeCompare(String(b.cupom.created_at ?? ""));
    });
  const melhor = validos[0] ?? null;
  const virar = (c: CupomAvaliado, automatico: boolean): CupomEscolhido | null =>
    c.avaliacao.ok
      ? { cupom: c.cupom, desconto: c.avaliacao.desconto, descontoNoFrete: c.avaliacao.descontoNoFrete, esperandoFrete: c.avaliacao.esperandoFrete, automatico }
      : null;

  if (digitado && digitado.avaliacao.ok) {
    if (!melhor || !melhor.avaliacao.ok || digitado.avaliacao.esperandoFrete || digitado.avaliacao.beneficio >= melhor.avaliacao.beneficio) {
      return { escolhido: virar(digitado, digitado.cupom.application === "automatic"), aviso: null };
    }
    return {
      escolhido: virar(melhor, true),
      aviso: `O desconto automático ${nomeDoCupom(melhor.cupom)} é melhor que o cupom ${digitado.cupom.code} e já está aplicado.`,
    };
  }
  return {
    escolhido: melhor ? virar(melhor, true) : null,
    aviso: digitado && !digitado.avaliacao.ok ? digitado.avaliacao.mensagem : null,
  };
}

/* ------------------------------------------------------------ dois cupons: desconto + frete */

export type VagaDoCupom = "desconto" | "frete";

/** Em que vaga do pedido o cupom entra: frete grátis numa, desconto na outra. */
export function vagaDoCupom(cupom: Pick<Cupom, "type">): VagaDoCupom {
  return cupom.type === "free_shipping" ? "frete" : "desconto";
}

export type EscolhaDosCupons = {
  /** Os cupons que ficam (desconto primeiro, frete depois), no máximo dois. */
  escolhidos: CupomEscolhido[];
  /** 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 }>;
};

function beneficioDe(c: CupomEscolhido | null) {
  return c ? c.desconto + c.descontoNoFrete : 0;
}

/**
 * Os cupons do pedido: até um de desconto e um de frete grátis.
 *
 * `digitados` na ordem em que o cliente digitou (o último de cada vaga vale).
 * Em cada vaga a escolha é a de escolherCupom. Se um dos dois não combina com
 * outro cupom, fica um só: o digitado (quando só um deles foi digitado) ou o
 * que der mais ao cliente (empate: o desconto); e, se o que ficou combina, a
 * outra vaga é preenchida de novo só com cupons que combinam.
 */
export function escolherCupons(digitados: CupomAvaliado[], automaticos: CupomAvaliado[]): EscolhaDosCupons {
  const erros: Array<{ code: string; mensagem: string }> = [];
  const avisos: string[] = [];
  for (const d of digitados) {
    if (!d.avaliacao.ok && !erros.some((e) => e.code === d.cupom.code)) erros.push({ code: d.cupom.code, mensagem: d.avaliacao.mensagem });
  }
  const digitadoDaVaga = (vaga: VagaDoCupom, soCombinaveis = false) =>
    [...digitados].reverse().find((d) => d.avaliacao.ok && vagaDoCupom(d.cupom) === vaga && (!soCombinaveis || d.cupom.combinable)) ?? null;
  const automaticosDaVaga = (vaga: VagaDoCupom, soCombinaveis = false) =>
    automaticos.filter((a) => vagaDoCupom(a.cupom) === vaga && (!soCombinaveis || a.cupom.combinable));
  const escolherNaVaga = (vaga: VagaDoCupom, soCombinaveis = false) => {
    const r = escolherCupom(digitadoDaVaga(vaga, soCombinaveis), automaticosDaVaga(vaga, soCombinaveis));
    return { escolhido: r.escolhido, aviso: r.escolhido && r.aviso ? r.aviso : null };
  };

  let desconto = escolherNaVaga("desconto");
  let frete = escolherNaVaga("frete");

  const d = desconto.escolhido;
  const f = frete.escolhido;
  if (d && f && (!d.cupom.combinable || !f.cupom.combinable)) {
    const ficaODesconto =
      !d.automatico && f.automatico ? true : d.automatico && !f.automatico ? false : beneficioDe(d) >= beneficioDe(f);
    const fica = ficaODesconto ? d : f;
    const sai = ficaODesconto ? f : d;
    const sozinho = !d.cupom.combinable ? d : f;
    avisos.push(`O cupom ${nomeDoCupom(sozinho.cupom)} é usado sozinho no pedido: ficou ${sozinho === fica ? "ele" : nomeDoCupom(fica.cupom)}, sem ${nomeDoCupom(sai.cupom)}.`);
    if (ficaODesconto) {
      frete = fica.cupom.combinable ? escolherNaVaga("frete", true) : { escolhido: null, aviso: null };
    } else {
      desconto = fica.cupom.combinable ? escolherNaVaga("desconto", true) : { escolhido: null, aviso: null };
    }
  }

  for (const r of [desconto, frete]) if (r.aviso) avisos.push(r.aviso);
  return {
    escolhidos: [desconto.escolhido, frete.escolhido].filter((c): c is CupomEscolhido => Boolean(c)),
    avisos,
    erros,
  };
}

/* ------------------------------------------------------------ como o cliente vê */

/** O nome que aparece ("Desconto aplicado: NOME"): o nome dado, ou o código. */
export function nomeDoCupom(cupom: Pick<Cupom, "name" | "code">) {
  return cupom.name?.trim() || cupom.code;
}

/** "10% OFF", "R$ 20 OFF", "Frete grátis". */
export function rotuloDoBeneficio(cupom: Pick<Cupom, "type" | "value">) {
  if (cupom.type === "free_shipping") return "Frete grátis";
  if (cupom.type === "percent") return `${Number(cupom.value).toLocaleString("pt-BR", { maximumFractionDigits: 2 })}% OFF`;
  return `${reaisCurtos(cupom.value)} OFF`;
}

export type NomesDoCupom = { categorias?: Record<string, string>; produtos?: Record<string, string> };

function nomesDe(ids: string[], nomes: Record<string, string> | undefined, singular: string, plural: string) {
  const conhecidos = ids.map((id) => nomes?.[id]).filter((n): n is string => Boolean(n));
  if (!ids.length) return "";
  if (conhecidos.length === ids.length && ids.length <= 3) return juntar(conhecidos);
  return ids.length === 1 ? `1 ${singular}` : `${ids.length} ${plural}`;
}

/** Onde o cupom vale, em português ("em Canecas e Camisetas", "exceto Banners"). */
export function textoDoAlcance(cupom: Cupom, nomes?: NomesDoCupom): string {
  const partes: string[] = [];
  if (cupom.scope === "categories" && cupom.scope_category_ids.length) {
    partes.push(`em ${nomesDe(cupom.scope_category_ids, nomes?.categorias, "categoria escolhida", "categorias escolhidas")}`);
  } else if (cupom.scope === "products" && cupom.scope_product_ids.length) {
    partes.push(`em ${nomesDe(cupom.scope_product_ids, nomes?.produtos, "produto escolhido", "produtos escolhidos")}`);
  }
  const fora = [
    nomesDe(cupom.exclude_category_ids, nomes?.categorias, "categoria", "categorias"),
    nomesDe(cupom.exclude_product_ids, nomes?.produtos, "produto", "produtos"),
  ].filter(Boolean);
  if (fora.length) partes.push(`exceto ${juntar(fora)}`);
  return partes.join(", ");
}

function diaCurto(iso: string, agora: number) {
  const r = relogioDeBrasilia(new Date(iso).getTime());
  const hoje = relogioDeBrasilia(agora);
  const dois = (n: number) => String(n).padStart(2, "0");
  return `${dois(r.dia)}/${dois(r.mes)}${r.ano !== hoje.ano ? `/${r.ano}` : ""}`;
}

/** O benefício por extenso ("10% de desconto (até R$ 50)", "Frete grátis até R$ 30 para SP"). */
export function textoDoBeneficio(cupom: Cupom): string {
  if (cupom.type === "free_shipping") {
    const teto = cupom.shipping_max ? ` até ${reaisCurtos(cupom.shipping_max)}` : "";
    const estados = cupom.shipping_states.length ? ` para ${juntar(cupom.shipping_states)}` : "";
    return `Frete grátis${teto}${estados}`;
  }
  if (cupom.type === "percent") {
    const pct = Number(cupom.value).toLocaleString("pt-BR", { maximumFractionDigits: 2 });
    return `${pct}% de desconto${cupom.max_discount ? ` (até ${reaisCurtos(cupom.max_discount)})` : ""}`;
  }
  return `${reaisCurtos(cupom.value)} de desconto`;
}

/**
 * As condições, uma por linha, para quem compra: "Para compras a partir de
 * R$ 50", "Só na primeira compra", "De segunda a sexta, das 18h às 22h",
 * "Válido até 31/10". Vazio quando vale para qualquer compra.
 */
export function condicoesDoCupom(cupom: Cupom, agora: number, opcoes: { nomes?: NomesDoCupom; validade?: boolean; limites?: boolean } = {}): string[] {
  const lista: string[] = [];
  const alcance = textoDoAlcance(cupom, opcoes.nomes);
  if (alcance) lista.push(alcance.charAt(0).toUpperCase() + alcance.slice(1));
  const participantes = minimoSoDosParticipantes(cupom);
  if (cupom.min_total > 0) {
    lista.push(`Para compras a partir de ${reaisCurtos(cupom.min_total)}${participantes ? " em produtos participantes" : ""}${cupom.type === "free_shipping" ? " (antes dos descontos)" : ""}`);
  }
  if (cupom.min_items > 1) lista.push(`A partir de ${cupom.min_items} itens${participantes ? " participantes" : " no carrinho"}`);
  if (cupom.audience === "first_purchase") lista.push("Só na primeira compra");
  else if (cupom.audience === "logged") lista.push("Para clientes com conta na loja");
  else if (cupom.audience === "clients") lista.push("Para clientes escolhidos pela loja");
  const dias = textoDosDias(cupom.weekdays);
  const horario = textoDoHorario(cupom);
  if (dias || horario) {
    const frase = [dias, horario].filter(Boolean).join(", ");
    lista.push(frase.charAt(0).toUpperCase() + frase.slice(1));
  }
  if (opcoes.limites !== false) {
    if (cupom.usage_limit) lista.push(`Só os primeiros ${cupom.usage_limit} ${cupom.usage_limit === 1 ? "uso" : "usos"}`);
    if (cupom.audience !== "first_purchase" && cupom.usage_limit_per_client) {
      lista.push(cupom.usage_limit_per_client === 1 ? "Um uso por cliente" : `Até ${cupom.usage_limit_per_client} usos por cliente`);
    }
  }
  if (!cupom.combinable) lista.push("Não vale junto com outro cupom");
  else if (cupom.type === "free_shipping") lista.push("Vale junto com um cupom de desconto");
  if (opcoes.validade !== false && cupom.ends_at && Number.isFinite(new Date(cupom.ends_at).getTime())) {
    lista.push(`Válido até ${diaCurto(cupom.ends_at, agora)}`);
  }
  return lista;
}

/**
 * A frase inteira, como o cliente lê:
 * "10% de desconto em Canecas, para compras a partir de R$ 50, até 31/10".
 */
export function fraseDoCupom(cupom: Cupom, agora: number, nomes?: NomesDoCupom): string {
  const partes = [textoDoBeneficio(cupom)];
  const alcance = textoDoAlcance(cupom, nomes);
  const comeco = alcance ? `${partes[0]} ${alcance.startsWith("exceto") ? `na loja toda, ${alcance}` : alcance}` : partes[0];
  const resto: string[] = [];
  const participantes = minimoSoDosParticipantes(cupom);
  if (cupom.min_total > 0) {
    resto.push(`para compras a partir de ${reaisCurtos(cupom.min_total)}${participantes ? " em produtos participantes" : ""}${cupom.type === "free_shipping" ? " antes dos descontos" : ""}`);
  }
  if (cupom.min_items > 1) resto.push(`a partir de ${cupom.min_items} itens${participantes ? " participantes" : ""}`);
  if (cupom.audience === "first_purchase") resto.push("na primeira compra");
  else if (cupom.audience === "logged") resto.push("para quem tem conta na loja");
  else if (cupom.audience === "clients") resto.push("para clientes escolhidos");
  const dias = textoDosDias(cupom.weekdays);
  if (dias) resto.push(dias);
  const horario = textoDoHorario(cupom);
  if (horario) resto.push(horario);
  if (cupom.ends_at && Number.isFinite(new Date(cupom.ends_at).getTime())) resto.push(`até ${diaCurto(cupom.ends_at, agora)}`);
  return [comeco, ...resto].join(", ");
}

/* ------------------------------------------------------------ para o painel */

const LETRAS_DO_CODIGO = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";

/** Código aleatório sem letras que confundem (0/O, 1/I). */
export function gerarCodigo(prefixo = "", sorteio: () => number = Math.random): string {
  const limpo = String(prefixo).toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 12);
  let sufixo = "";
  for (let i = 0; i < 6; i += 1) sufixo += LETRAS_DO_CODIGO[Math.floor(sorteio() * LETRAS_DO_CODIGO.length) % LETRAS_DO_CODIGO.length];
  return limpo ? `${limpo}-${sufixo}` : sufixo;
}

/** O código como o cliente digita: maiúsculas, letras, números e hífen. */
export function codigoValido(valor: unknown): string | null {
  const codigo = String(valor ?? "").trim().toUpperCase();
  if (!/^[A-Z0-9-]{3,30}$/.test(codigo)) return null;
  return codigo;
}

/** Confere o cupom antes de salvar. Devolve o erro em português, ou null. */
export function erroDoCupom(cupom: Cupom): string | null {
  if (cupom.type !== "free_shipping") {
    if (!Number.isFinite(cupom.value) || cupom.value <= 0) return "O valor do desconto precisa ser maior que zero.";
    if (cupom.type === "percent" && cupom.value > 100) return "Desconto em porcentagem vai no máximo até 100%.";
  }
  if (cupom.min_total < 0) return "O pedido mínimo não pode ser negativo.";
  if (!Number.isInteger(cupom.min_items) || cupom.min_items < 0) return "A quantidade mínima de itens precisa ser um número inteiro, zero ou mais.";
  if (cupom.starts_at && cupom.ends_at && new Date(cupom.ends_at).getTime() < new Date(cupom.starts_at).getTime()) return "O fim da validade é antes do começo.";
  if (cupom.scope === "categories" && !cupom.scope_category_ids.length) return "Escolha pelo menos uma categoria onde o cupom vale.";
  if (cupom.scope === "products" && !cupom.scope_product_ids.length) return "Escolha pelo menos um produto onde o cupom vale.";
  if (cupom.audience === "clients" && !cupom.audience_client_ids.length) return "Escolha pelo menos um cliente que pode usar o cupom.";
  if (cupom.hours_start && cupom.hours_end && cupom.hours_start === cupom.hours_end) return "O horário de início e o de fim são iguais.";
  if (cupom.shipping_states.some((uf) => !ESTADOS.includes(uf))) return "Há um estado inválido no frete grátis.";
  if (cupom.name && cupom.name.length > 80) return "O nome do cupom vai até 80 letras.";
  return null;
}
