/**
 * As variações da loja antiga no jeito da Ktalogu.
 *
 * Na loja antiga, cada combinação tem o seu preço ("P / Azul: R$ 50"). Na
 * Ktalogu o preço mora em cada opção, somado ao preço do produto ("Tamanho G:
 * + R$ 10"), e a loja, o carrinho, o orçamento e o PDV fazem essa conta. Então
 * a tradução tenta, nesta ordem:
 *
 *   sem_diferenca  todas as combinações custam o mesmo: opções sem acréscimo;
 *   somada         o preço se explica por soma (cada opção tem o seu
 *                  acréscimo, e toda combinação confere): é o caso comum;
 *   lista_unica    não se explica por soma: as combinações viram uma lista só
 *                  ("P / Azul", "G / Preto"), cada uma com o seu acréscimo.
 *                  Nenhum preço sai errado; só a escolha fica numa lista;
 *   demais         combinações demais para uma lista: as variações ficam para
 *                  o painel, e a prévia avisa.
 *
 * Atributo com um valor só ("Material: algodão") não é escolha: fica de fora
 * (era o que mais poluía as variações importadas por planilha).
 */

import type { ProdutoLido, VarianteLida } from "./modelo.ts";

export type OpcaoNova = {
  id: string;
  label: string;
  price: number;
  price_applies_per_quantity: true;
  stock: 0;
  is_required: false;
  position: number;
  image_id: string | null;
  minimum_stock: 0;
  reorder_point: 0;
};

export type AtributoNovo = {
  name: string;
  options: OpcaoNova[];
  allow_multiple: false;
  is_required: true;
  position: number;
};

export type ModoDasVariacoes = "nenhuma" | "sem_diferenca" | "somada" | "lista_unica" | "demais";

export type Variacoes = {
  modo: ModoDasVariacoes;
  /* O preço do produto: o da combinação mais barata. null = a loja não mostra preço. */
  base: number | null;
  atributos: AtributoNovo[];
  /* A foto de cada opção (id da opção → endereço da foto na loja antiga). */
  fotosDasOpcoes: Map<string, string>;
  avisos: string[];
};

export const MAXIMO_DE_OPCOES = 100;
/* Shopify para em 3; o WooCommerce e a Ktalogu vão além. */
export const MAXIMO_DE_ESCOLHAS = 5;
const centavos = (valor: number) => Math.round(valor * 100) / 100;
const iguais = (a: number, b: number) => Math.abs(a - b) <= 0.005;

type Linha = { valores: string[]; preco: number | null; foto: string | null };

function opcao(id: string, label: string, price: number, position: number): OpcaoNova {
  return {
    id,
    label: label.slice(0, 80),
    price: centavos(Math.max(0, price)),
    price_applies_per_quantity: true,
    stock: 0,
    is_required: false,
    position,
    image_id: null,
    minimum_stock: 0,
    reorder_point: 0,
  };
}

function atributo(nome: string, options: OpcaoNova[], position: number): AtributoNovo {
  return { name: nome.slice(0, 60), options, allow_multiple: false, is_required: true, position };
}

/** A ordem dos valores de cada atributo: a da loja antiga, quando ela informa. */
function valoresEmOrdem(produto: ProdutoLido, nome: string, linhas: Linha[], indice: number): string[] {
  const usados = new Set(linhas.map((item) => item.valores[indice]));
  const daLoja = produto.atributos.find((item) => item.nome === nome)?.valores.filter((valor) => usados.has(valor)) ?? [];
  const resto = linhas.map((item) => item.valores[indice]).filter((valor) => !daLoja.includes(valor));
  return [...new Set([...daLoja, ...resto])];
}

/**
 * O acréscimo de cada valor, se o preço se explica por soma; null se não.
 * A combinação mais barata é a referência: os valores dela valem zero, e cada
 * outro valor vale a diferença da combinação que só muda nele.
 */
function decompor(linhas: Linha[], quantos: number, base: number): Array<Map<string, number>> | null {
  const referencia = linhas.find((item) => item.preco !== null && iguais(item.preco, base));
  if (!referencia) return null;
  const acrescimos = Array.from({ length: quantos }, () => new Map<string, number>());
  for (let indice = 0; indice < quantos; indice += 1) {
    acrescimos[indice].set(referencia.valores[indice], 0);
    for (const candidata of linhas) {
      const soNeste = candidata.valores.every((valor, outro) => outro === indice || valor === referencia.valores[outro]);
      if (!soNeste || candidata.preco === null) continue;
      const valor = candidata.valores[indice];
      if (!acrescimos[indice].has(valor)) acrescimos[indice].set(valor, candidata.preco - base);
    }
  }
  /* Todo valor precisa de um acréscimo, e toda combinação tem de conferir. */
  for (const linhaDaLoja of linhas) {
    let soma = base;
    for (let indice = 0; indice < quantos; indice += 1) {
      const acrescimo = acrescimos[indice].get(linhaDaLoja.valores[indice]);
      if (acrescimo === undefined) return null;
      soma += acrescimo;
    }
    if (linhaDaLoja.preco !== null && !iguais(soma, linhaDaLoja.preco)) return null;
  }
  return acrescimos;
}

/** O atributo que decide a foto (a cor, quase sempre): cada valor dele tem uma foto só. */
function atributoDaFoto(linhas: Linha[], quantos: number, nomes: string[]): number | null {
  const candidatos: number[] = [];
  for (let indice = 0; indice < quantos; indice += 1) {
    const fotoDoValor = new Map<string, string | null>();
    let confere = true;
    for (const item of linhas) {
      const valor = item.valores[indice];
      if (!fotoDoValor.has(valor)) fotoDoValor.set(valor, item.foto);
      else if (fotoDoValor.get(valor) !== item.foto) confere = false;
    }
    const distintas = new Set([...fotoDoValor.values()].filter(Boolean));
    if (confere && distintas.size >= 2) candidatos.push(indice);
  }
  if (!candidatos.length) return null;
  return candidatos.find((indice) => /cor|color|colour|estampa/i.test(nomes[indice])) ?? candidatos[0];
}

export function montarVariacoes(produto: ProdutoLido, novoId: () => string): Variacoes {
  const avisos: string[] = [];
  const nada = (modo: ModoDasVariacoes, base: number | null): Variacoes => ({ modo, base, atributos: [], fotosDasOpcoes: new Map(), avisos });
  const variantes = produto.variantes.filter((variante: VarianteLida) => Object.keys(variante.opcoes).length > 0);
  if (variantes.length < 2) return nada("nenhuma", produto.preco);

  /* Os atributos que de fato separam as combinações (dois valores ou mais). */
  const todosOsNomes = [...new Set([...produto.atributos.map((item) => item.nome), ...variantes.flatMap((variante) => Object.keys(variante.opcoes))])];
  const nomes = todosOsNomes.filter((nome) => new Set(variantes.map((variante) => variante.opcoes[nome]).filter(Boolean)).size >= 2).slice(0, MAXIMO_DE_ESCOLHAS);
  if (!nomes.length) return nada("nenhuma", produto.preco);

  /* Uma linha por combinação; a repetida fica com o menor preço. */
  const porChave = new Map<string, Linha>();
  for (const variante of variantes) {
    const valores = nomes.map((nome) => variante.opcoes[nome]?.trim() ?? "");
    if (valores.some((valor) => !valor)) continue;
    const chave = valores.join("\u0001");
    const atual = porChave.get(chave);
    const preco = variante.preco ?? null;
    if (!atual || (preco !== null && (atual.preco === null || preco < atual.preco))) {
      porChave.set(chave, { valores, preco, foto: variante.foto ?? atual?.foto ?? null });
    }
  }
  const linhas = [...porChave.values()];
  if (linhas.length < 2) return nada("nenhuma", produto.preco);

  const comPreco = linhas.filter((item) => item.preco !== null && item.preco > 0);
  const base = comPreco.length ? Math.min(...comPreco.map((item) => item.preco as number)) : produto.preco;
  if (comPreco.length && comPreco.length < linhas.length) {
    avisos.push(`${linhas.length - comPreco.length} combinação(ões) sem preço na loja antiga ficaram com o preço do produto.`);
    for (const item of linhas) if (item.preco === null || item.preco <= 0) item.preco = base;
  }

  const fotosDasOpcoes = new Map<string, string>();
  const todasIguais = !comPreco.length || linhas.every((item) => item.preco === null || iguais(item.preco, base ?? 0));
  const acrescimos = todasIguais || base === null ? null : decompor(linhas, nomes.length, base);

  if (todasIguais || acrescimos) {
    const ordem = nomes.map((nome, indice) => valoresEmOrdem(produto, nome, linhas, indice));
    if (ordem.some((valores) => valores.length > MAXIMO_DE_OPCOES)) {
      avisos.push(`As variações têm opções demais (mais de ${MAXIMO_DE_OPCOES}) e ficam para o painel.`);
      return nada("demais", base);
    }
    const daFoto = atributoDaFoto(linhas, nomes.length, nomes);
    const atributos = nomes.map((nome, indice) =>
      atributo(
        nome,
        ordem[indice].map((valor, posicao) => {
          const nova = opcao(novoId(), valor, acrescimos?.[indice].get(valor) ?? 0, posicao);
          if (indice === daFoto) {
            const foto = linhas.find((item) => item.valores[indice] === valor)?.foto;
            if (foto) fotosDasOpcoes.set(nova.id, foto);
          }
          return nova;
        }),
        indice
      )
    );
    return { modo: todasIguais ? "sem_diferenca" : "somada", base, atributos, fotosDasOpcoes, avisos };
  }

  if (linhas.length > MAXIMO_DE_OPCOES) {
    avisos.push(`O produto tem ${linhas.length} combinações com preços próprios, mais do que cabe numa lista; as variações ficam para o painel.`);
    return nada("demais", base);
  }
  avisos.push(
    `Os preços das combinações (${nomes.join(" e ")}) não se explicam por soma; elas viraram uma lista única de escolha ("${linhas[0].valores.join(" / ")}"…), cada uma com o seu preço.`
  );
  const opcoes = linhas
    .sort((a, b) => (a.preco ?? 0) - (b.preco ?? 0))
    .map((item, posicao) => {
      const nova = opcao(novoId(), item.valores.join(" / "), (item.preco ?? base ?? 0) - (base ?? 0), posicao);
      if (item.foto) fotosDasOpcoes.set(nova.id, item.foto);
      return nova;
    });
  return { modo: "lista_unica", base, atributos: [atributo(nomes.join(" / "), opcoes, 0)], fotosDasOpcoes, avisos };
}
