/**
 * Cada produto lido, pronto para a Ktalogu gravar.
 *
 * Aqui mora toda decisão sobre o conteúdo que vem de fora:
 * - a descrição passa pelo mesmo limpador do painel, perde os links e perde
 *   telefone, e-mail, site e chave Pix (ver `tirarContatos`): um texto de site
 *   pode esconder o contato de outra pessoa, e o da loja antiga só atrapalha;
 * - o preço é o da combinação mais barata; o "de/por" vira o "por", porque a
 *   Ktalogu não mostra preço riscado (a prévia avisa);
 * - as fotos ganham o id que a linha delas vai ter, para a opção de cor
 *   apontar para a foto antes de ela chegar (a fila baixa depois);
 * - o assistente pode acertar o que a loja antiga não diz em dado (a tabela
 *   de preço por quantidade escrita na descrição, a venda por m², o prazo).
 *
 * Só regra, sem rede e sem banco: o teste ao lado roda sozinho.
 */

import { plainTextToDescriptionHtml, sanitizeProductDescription } from "../../product-description-rich-text.ts";
import type { ProductPriceTier } from "../../price-tiers.ts";
import { contatosNoTexto, tirarContatos } from "../contatos.ts";
import { lerFaixas, UNIDADES_DO_PRAZO, VALOR_MAXIMO, VENDIDO_POR, type FaixaDoAssistente } from "../ferramentas/regras-do-produto.ts";
import { absoluto, chaveDeOrigem, linha, semEntidades, semRepetir, type ProdutoLido } from "./modelo.ts";
import { MAXIMO_DE_ESCOLHAS, MAXIMO_DE_OPCOES, montarVariacoes, type AtributoNovo, type ModoDasVariacoes, type OpcaoNova } from "./variacoes.ts";

export const MAXIMO_DE_FOTOS = 12;
export const NOME_MAXIMO = 150;
const DESCRICAO_MAXIMA = 20_000;

export type OpcaoDoAssistente = { nome?: unknown; acrescimo?: unknown; foto?: unknown };
export type VariacaoDoAssistente = { nome?: unknown; opcoes?: OpcaoDoAssistente[] };

/** O que o assistente acerta por cima da loja antiga, ou manda inteiro. */
export type Ajuste = {
  nome?: string;
  descricao?: string;
  preco?: number;
  sob_consulta?: boolean;
  categoria?: string;
  vendido_por?: string;
  quantidade_minima?: number;
  prazo_de_producao?: number;
  unidade_do_prazo?: string;
  faixas_de_preco?: FaixaDoAssistente[];
  variacoes?: VariacaoDoAssistente[];
};

export type FotoNova = { id: string; endereco: string; posicao: number };

export type ProdutoMontado = {
  /* A página do produto na loja antiga, e a chave que impede trazê-lo de novo. */
  origem: string | null;
  chave: string | null;
  nome: string;
  descricao: string;
  contatosTirados: string[];
  preco: number;
  sobConsulta: boolean;
  precoAntigo: number | null;
  moeda: string | null;
  unidade: string;
  /* No máximo dois níveis: a categoria e a de dentro dela. */
  categorias: string[];
  codigo: string | null;
  codigoDeBarras: string | null;
  pesoKg: number | null;
  fotos: FotoNova[];
  fotosQueFicaram: number;
  atributos: AtributoNovo[];
  modo: ModoDasVariacoes;
  faixas: ProductPriceTier[];
  quantidadeMinima: number | null;
  prazo: { valor: number; unidade: string } | null;
  avisos: string[];
};

export type Montagem = { ok: true; produto: ProdutoMontado } | { ok: false; erro: string };

/* ------------------------------------------------------------------ texto */

/* Caracteres que não aparecem na tela (largura zero, hífen invisível, marcas
   de direção): num texto de produto, só servem para partir um telefone. */
const INVISIVEIS = /[­​-‏⁠-⁤﻿]/g;
/* Dígito "largo" (１２３) e dígito de outra escrita: viram dígito comum (os de
   outra escrita, 0), para a peneira enxergar o número. "m²" fica como está. */
const DIGITO_LARGO = /[０-９]/g;
const DIGITO_DE_OUTRA_ESCRITA = /(?![0-9０-９])\p{Nd}/gu;

function normalizarDigitos(texto: string): string {
  return texto
    .replace(INVISIVEIS, "")
    .replace(DIGITO_LARGO, (digito) => String(digito.charCodeAt(0) - 0xff10))
    .replace(DIGITO_DE_OUTRA_ESCRITA, "0");
}

/** O texto que a pessoa lê, sem as marcações (as de bloco viram quebra de linha). */
function textoSemMarcacao(html: string): string {
  return semEntidades(html.replace(/<(?:br|\/p|\/div|\/li|\/h[1-6]|\/blockquote)\b[^<>]{0,200}>/gi, "\n").replace(/<[^<>]{0,3000}>/g, ""));
}

/** A descrição limpa: o limpador do painel, sem links e sem contatos. */
export function limparDescricao(bruta: string): { html: string; tirados: string[] } {
  const limpa = normalizarDigitos(sanitizeProductDescription(bruta.slice(0, 200_000)))
    .replace(/<a\b[^<>]{0,3000}>/gi, "")
    .replace(/<\/a>/gi, "");
  const tirados: string[] = [];
  /* Só no texto, nunca dentro de uma marcação. */
  const partes = limpa.split(/(<[^<>]{0,3000}>)/);
  for (let indice = 0; indice < partes.length; indice += 2) {
    const resultado = tirarContatos(partes[indice]);
    partes[indice] = resultado.texto;
    tirados.push(...resultado.tirados);
  }
  let html = partes
    .join("")
    .replace(/<p>(?:\s|&nbsp;|<br\s*\/?>)*<\/p>/gi, "")
    .trim();

  /* Telefone partido por marcação ("12345<b></b>67890") passa pedaço por
     pedaço. Lido o texto inteiro, como a pessoa vê: se ainda sobrou contato,
     a descrição vira texto simples (perde negrito e listas) e passa de novo. */
  const corrido = textoSemMarcacao(html);
  if (contatosNoTexto(corrido).length) {
    const resultado = tirarContatos(corrido);
    tirados.push(...resultado.tirados);
    html = plainTextToDescriptionHtml(resultado.texto);
  }

  if (html.length > DESCRICAO_MAXIMA) html = sanitizeProductDescription(html.slice(0, DESCRICAO_MAXIMA));
  return { html, tirados: semRepetir(tirados) };
}

/** O nome cabendo no limite, cortado no fim de uma palavra. */
function nomeQueCabe(nome: string): { nome: string; cortado: boolean } {
  if (nome.length <= NOME_MAXIMO) return { nome, cortado: false };
  const corte = nome.slice(0, NOME_MAXIMO);
  const espaco = corte.lastIndexOf(" ");
  return { nome: (espaco > 60 ? corte.slice(0, espaco) : corte).replace(/[\s,;:.–-]+$/, ""), cortado: true };
}

/** "Camisetas > Masculinas > Manga longa" vira ["Camisetas", "Manga longa"]. */
export function categoriasDoCaminho(caminho: string[]): string[] {
  const partes = semRepetir(caminho.map((parte) => linha(parte, 80)).filter(Boolean), (parte) => parte.toLocaleLowerCase("pt-BR"));
  if (partes.length <= 1) return partes;
  return [partes[0], partes[partes.length - 1]];
}

const emReais = (valor: number) => valor.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }).replace(/ /g, " ");

/**
 * Faixas escritas como na vitrine ("10+: R$ 9; 50+: R$ 8") não dizem onde
 * cada uma termina, e a regra do painel recusa faixas que se cruzam. Cada
 * faixa sem fim termina logo antes da seguinte (em peças inteiras, uma antes;
 * em medida quebrada, um centésimo antes). A última segue aberta.
 */
export function completarFaixas(faixas: FaixaDoAssistente[]): FaixaDoAssistente[] {
  const ordenadas = [...faixas].sort((a, b) => Number(a.a_partir_de) - Number(b.a_partir_de));
  return ordenadas.map((faixa, indice) => {
    const seguinte = ordenadas[indice + 1];
    if (!seguinte || (faixa.ate !== undefined && faixa.ate !== null)) return faixa;
    const inicio = Number(faixa.a_partir_de);
    const proxima = Number(seguinte.a_partir_de);
    const passo = Number.isInteger(inicio) && Number.isInteger(proxima) ? 1 : 0.01;
    return proxima - passo >= inicio ? { ...faixa, ate: Math.round((proxima - passo) * 100) / 100 } : faixa;
  });
}

/* ------------------------------------------------------- variações do assistente */

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

/** As variações que o assistente montou, já no jeito da Ktalogu (opção com acréscimo). */
function variacoesDoAssistente(variacoes: VariacaoDoAssistente[], base: string, novoId: () => string, avisos: string[]) {
  const atributos: AtributoNovo[] = [];
  const fotosDasOpcoes = new Map<string, string>();
  for (const variacao of variacoes.slice(0, MAXIMO_DE_ESCOLHAS)) {
    const nome = linha(variacao.nome, 60);
    const opcoes = semRepetir(
      (variacao.opcoes ?? []).map((opcao) => ({ nome: linha(opcao.nome, 80), acrescimo: Number(opcao.acrescimo ?? 0), foto: absoluto(opcao.foto, base) })),
      (opcao) => opcao.nome.toLocaleLowerCase("pt-BR")
    ).filter((opcao) => opcao.nome);
    if (!nome || opcoes.length < 2) {
      if (nome) avisos.push(`A variação "${nome}" tem uma opção só e ficou de fora (não é uma escolha).`);
      continue;
    }
    const novas = opcoes.slice(0, MAXIMO_DE_OPCOES).map((opcao, posicao) => {
      const nova = opcaoNova(novoId(), opcao.nome, Number.isFinite(opcao.acrescimo) ? opcao.acrescimo : 0, posicao);
      if (opcao.foto) fotosDasOpcoes.set(nova.id, opcao.foto);
      return nova;
    });
    atributos.push({ name: nome, options: novas, allow_multiple: false, is_required: true, position: atributos.length });
  }
  return { atributos, fotosDasOpcoes };
}

/* ----------------------------------------------------------------- montar */

export function montarProduto(lido: ProdutoLido, ajuste: Ajuste | undefined, novoId: () => string): Montagem {
  const avisos: string[] = [];
  const corte = nomeQueCabe(linha(ajuste?.nome ?? lido.nome, 400));
  if (corte.nome.length < 2) return { ok: false, erro: `Produto sem nome (${lido.referencia}).` };
  const nome = corte.nome;
  if (corte.cortado) avisos.push(`${nome}: o nome tinha mais de ${NOME_MAXIMO} letras e foi encurtado.`);

  const descricao = limparDescricao(ajuste?.descricao ?? lido.descricao);

  /* Variações: as do assistente mandam; senão, as da loja antiga. */
  const daLoja = ajuste?.variacoes ? null : montarVariacoes(lido, novoId);
  const doAssistente = ajuste?.variacoes ? variacoesDoAssistente(ajuste.variacoes, lido.referencia, novoId, avisos) : null;
  const atributos = daLoja?.atributos ?? doAssistente?.atributos ?? [];
  const fotosDasOpcoes = daLoja?.fotosDasOpcoes ?? doAssistente?.fotosDasOpcoes ?? new Map<string, string>();
  const modo: ModoDasVariacoes = daLoja?.modo ?? (atributos.length ? "somada" : "nenhuma");
  avisos.push(...(daLoja?.avisos ?? []).map((aviso) => `${nome}: ${aviso}`));

  const precoPedido = ajuste?.preco !== undefined ? Number(ajuste.preco) : (daLoja?.base ?? lido.preco);
  const sobConsulta = ajuste?.sob_consulta === true || !(Number(precoPedido) > 0);
  const preco = sobConsulta ? 0 : Math.min(Number(precoPedido), VALOR_MAXIMO);
  if (!sobConsulta && lido.precoAntigo && lido.precoAntigo > preco && ajuste?.preco === undefined) {
    avisos.push(`${nome}: na loja antiga aparecia "de ${emReais(lido.precoAntigo)} por ${emReais(preco)}". A Ktalogu não mostra preço riscado; entra ${emReais(preco)}.`);
  }
  if (lido.moeda && lido.moeda.toUpperCase() !== "BRL") {
    avisos.push(`${nome}: os preços da loja antiga estão em ${lido.moeda.toUpperCase()} e entram como estão, como se fossem reais. Confira antes de publicar.`);
  }

  const unidade = VENDIDO_POR[(ajuste?.vendido_por as keyof typeof VENDIDO_POR | undefined) ?? "unidade"] ?? VENDIDO_POR.unidade;
  const categorias = categoriasDoCaminho(ajuste?.categoria ? ajuste.categoria.split(/\s*(?:>|\/|»|›)\s*/) : lido.categorias);

  /* Fotos: a principal da loja antiga continua a principal; as das opções
     entram junto (são elas que trocam quando a pessoa escolhe a cor). */
  const galeria = semRepetir(lido.fotos.map((foto) => absoluto(foto, lido.referencia)).filter((foto): foto is string => Boolean(foto)));
  const dasOpcoes = semRepetir([...fotosDasOpcoes.values()]);
  const todas = semRepetir([...galeria, ...dasOpcoes]);
  let escolhidas = todas;
  if (todas.length > MAXIMO_DE_FOTOS) {
    const prioridade = semRepetir([...galeria.slice(0, 1), ...dasOpcoes, ...galeria.slice(1)]).slice(0, MAXIMO_DE_FOTOS);
    escolhidas = todas.filter((foto) => prioridade.includes(foto));
    avisos.push(`${nome}: tinha ${todas.length} fotos; vêm as ${MAXIMO_DE_FOTOS} primeiras${dasOpcoes.length ? " (as das opções entram antes)" : ""}.`);
  }
  const fotos: FotoNova[] = escolhidas.map((endereco, posicao) => ({ id: novoId(), endereco, posicao }));
  const idDaFoto = new Map(fotos.map((foto) => [foto.endereco, foto.id]));
  for (const atributoNovo of atributos) {
    for (const opcao of atributoNovo.options) {
      const endereco = fotosDasOpcoes.get(opcao.id);
      opcao.image_id = endereco ? (idDaFoto.get(endereco) ?? null) : null;
    }
  }

  const minimo = Number(ajuste?.quantidade_minima ?? 0);
  let faixas: ProductPriceTier[] = [];
  if (ajuste?.faixas_de_preco?.length) {
    if (sobConsulta) return { ok: false, erro: `${nome}: produto sob consulta não tem faixas de preço.` };
    const lidas = lerFaixas(completarFaixas(ajuste.faixas_de_preco), { min_sale_qty_enabled: minimo > 0, min_sale_qty: minimo > 0 ? minimo : null });
    if (!lidas.ok) return { ok: false, erro: `${nome}: ${lidas.erro}` };
    faixas = lidas.faixas;
  }
  const prazo =
    ajuste?.prazo_de_producao !== undefined
      ? {
          valor: Number(ajuste.prazo_de_producao),
          unidade: UNIDADES_DO_PRAZO[(ajuste.unidade_do_prazo as keyof typeof UNIDADES_DO_PRAZO | undefined) ?? "dias úteis"] ?? UNIDADES_DO_PRAZO["dias úteis"],
        }
      : null;

  const origem = /^https?:\/\//i.test(lido.referencia) ? lido.referencia : null;
  return {
    ok: true,
    produto: {
      origem,
      chave: origem ? chaveDeOrigem(origem) : null,
      nome,
      descricao: descricao.html,
      contatosTirados: descricao.tirados,
      preco,
      sobConsulta,
      precoAntigo: lido.precoAntigo,
      moeda: lido.moeda,
      unidade,
      categorias,
      codigo: lido.codigo ? lido.codigo.slice(0, 60) : null,
      codigoDeBarras: lido.codigoDeBarras ? lido.codigoDeBarras.slice(0, 60) : null,
      pesoKg: lido.pesoKg && lido.pesoKg > 0 && lido.pesoKg < 10_000 ? lido.pesoKg : null,
      fotos,
      fotosQueFicaram: Math.max(0, todas.length - fotos.length),
      atributos,
      modo,
      faixas,
      quantidadeMinima: minimo > 0 ? minimo : null,
      prazo,
      avisos,
    },
  };
}

/** Resumo das variações para a prévia: "Cor: Azul, Preto (+ R$ 5,00); Tamanho: P, M, G". */
export function resumoDasVariacoes(atributos: AtributoNovo[]): string | null {
  if (!atributos.length) return null;
  return atributos
    .map((atributoNovo) => {
      const nomes = atributoNovo.options.slice(0, 8).map((opcao) => (opcao.price > 0 ? `${opcao.label} (+ ${emReais(opcao.price)})` : opcao.label));
      const resto = atributoNovo.options.length > 8 ? ` e mais ${atributoNovo.options.length - 8}` : "";
      return `${atributoNovo.name}: ${nomes.join(", ")}${resto}`;
    })
    .join("; ");
}
