/*
 * Estoque v2 — contas e textos puros das telas (sem React, sem banco).
 *
 * O banco é quem decide o que sai do estoque (MOTOR.md). Aqui mora só o que a
 * tela precisa para EXPLICAR o que o banco vai fazer: a mesma comparação de
 * nomes que liga variação a célula, a grade desenhada em linhas e colunas, o
 * nome de cada movimento e o link da origem dele. Testado em regras.test.ts.
 *
 * Importável pelo navegador, pelo servidor e pelo teste (`node --test`): só
 * imports com extensão .ts e nada de recurso que o strip-types não aceita.
 */

import type {
  Celula,
  Combinacao,
  Disponibilidade,
  EixoDaGrade,
  LinhaDoMapa,
  OrigemDoMovimento,
  QuandoSai,
  TipoDeMovimento,
  AtributoDoProduto,
} from "./tipos.ts";

/* ------------------------------------------------------------------ nomes */

const COM_ACENTO = "áàâãäéèêëíìîïóòôõöúùûüçñ";
const SEM_ACENTO = "aaaaaeeeeiiiiooooouuuucn";

/**
 * O mesmo `estoque_v2_norm` do banco: minúscula, sem os acentos comuns,
 * espaços juntos e sem ":" no fim ("Tamanho:" = "tamanho"). Vazio = null.
 */
export function normalizar(texto: string | null | undefined): string | null {
  let s = String(texto ?? "").trim().toLowerCase();
  let saida = "";
  for (const letra of s) {
    const i = COM_ACENTO.indexOf(letra);
    saida += i >= 0 ? SEM_ACENTO[i] : letra;
  }
  s = saida.replace(/\s+/g, " ").replace(/[\s:]+$/, "");
  return s ? s : null;
}

export function mesmoNome(a: string | null | undefined, b: string | null | undefined): boolean {
  const x = normalizar(a);
  return x !== null && x === normalizar(b);
}

/* --------------------------------------------------------------- números */

const FORMATO_QTD = new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 3 });
const FORMATO_DINHEIRO = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" });

export function qtd(n: number | string | null | undefined): string {
  const v = Number(n);
  return FORMATO_QTD.format(Number.isFinite(v) ? v : 0);
}

export function dinheiro(n: number | string | null | undefined): string {
  const v = Number(n);
  return FORMATO_DINHEIRO.format(Number.isFinite(v) ? v : 0);
}

/** "m2" vira "m²"; o resto fica como a empresa escreveu. */
export function unidade(u: string | null | undefined): string {
  const t = String(u ?? "").trim();
  if (!t) return "un";
  if (/^m2$/i.test(t)) return "m²";
  return t;
}

/** "12 un", "0,5 m²". */
export function qtdComUnidade(n: number | string | null | undefined, u: string | null | undefined): string {
  return `${qtd(n)} ${unidade(u)}`;
}

/* ------------------------------------------------------------- movimentos */

export type TomDoMovimento = "entra" | "sai" | "neutro";

const NOMES_DOS_MOVIMENTOS: Record<TipoDeMovimento, string> = {
  saldo_inicial: "Saldo inicial",
  entrada: "Entrada",
  compra: "Compra do fornecedor",
  venda: "Venda",
  devolucao: "Devolução",
  producao_entrada: "Produção",
  producao_saida: "Produção",
  contagem: "Contagem",
  perda: "Perda",
  estorno_perda: "Perda desfeita",
  ajuste: "Ajuste",
};

/** Nome do movimento na linha do tempo, com o sentido certo na produção. */
export function nomeDoMovimento(tipo: TipoDeMovimento | string, quantidade: number): string {
  if (tipo === "producao_entrada") return quantidade >= 0 ? "Produção: ficou pronto" : "Produção desfeita";
  if (tipo === "producao_saida") return quantidade <= 0 ? "Produção: usado" : "Produção desfeita: voltou";
  if (tipo === "venda" && quantidade > 0) return "Venda desfeita";
  return NOMES_DOS_MOVIMENTOS[tipo as TipoDeMovimento] ?? "Movimento";
}

export function tomDoMovimento(quantidade: number): TomDoMovimento {
  if (quantidade > 0) return "entra";
  if (quantidade < 0) return "sai";
  return "neutro";
}

/** "+12", "−3,5" (sinal de menos de verdade, como num extrato). */
export function qtdComSinal(n: number): string {
  if (n > 0) return `+${qtd(n)}`;
  if (n < 0) return `−${qtd(Math.abs(n))}`;
  return "0";
}

export type LinkDaOrigem = { texto: string; href: string | null; tipo: "pedido" | "compra" | "producao" | "contagem" | "perda" | "estorno" };

/**
 * De onde veio o movimento, pronto para virar link. A produção não tem tela
 * própria: o link volta `href: null` e a tela abre a produção no lugar.
 */
export function origensDoMovimento(origem: OrigemDoMovimento | null | undefined): LinkDaOrigem[] {
  const o = origem ?? {};
  const links: LinkDaOrigem[] = [];
  if (o.pedido_id) {
    links.push({
      tipo: "pedido",
      texto: o.pedido_numero ? `Pedido #${o.pedido_numero}` : "Pedido",
      href: `/dashboard/orders/${o.pedido_id}`,
    });
  }
  if (o.compra_id) {
    links.push({
      tipo: "compra",
      texto: o.compra_numero ? `Compra #${o.compra_numero}` : "Compra",
      href: o.fornecedor_id ? `/dashboard/fornecedores/meus-fornecedores/${o.fornecedor_id}/compras` : null,
    });
  }
  if (o.producao_id) {
    links.push({ tipo: "producao", texto: o.producao_desfeita ? "Produção (desfeita)" : "Produção", href: null });
  }
  if (o.perda_id) {
    links.push({
      tipo: "perda",
      texto: o.perda_pedido_numero ? `Perda no pedido #${o.perda_pedido_numero}` : "Perda na produção",
      href: o.perda_pedido_id ? `/dashboard/orders/${o.perda_pedido_id}` : null,
    });
  }
  /* Contagem não ganha etiqueta: o nome do movimento já diz "Contagem". */
  return links;
}

/* ------------------------------------------------------------------ grade */

export function chaveDaCelula(valor1: string | null | undefined, valor2: string | null | undefined): string {
  return `${normalizar(valor1) ?? ""}|${normalizar(valor2) ?? ""}`;
}

export function rotuloDaCelula(nome: string, valor1: string | null | undefined, valor2: string | null | undefined): string {
  const partes = [valor1, valor2].filter((v): v is string => Boolean(v && String(v).trim()));
  return partes.length ? `${nome} (${partes.join(" / ")})` : nome;
}

/** Só os valores da célula: "G / Preta" (ou "Única" no item sem grade). */
export function valoresDaCelula(c: Pick<Celula, "valor1" | "valor2">): string {
  const partes = [c.valor1, c.valor2].filter((v): v is string => Boolean(v && String(v).trim()));
  return partes.length ? partes.join(" / ") : "Única";
}

export function gradeValida(grade: unknown): EixoDaGrade[] {
  if (!Array.isArray(grade)) return [];
  return grade
    .filter((e): e is EixoDaGrade => Boolean(e && typeof e === "object" && typeof (e as EixoDaGrade).nome === "string"))
    .map((e) => ({
      nome: e.nome,
      valores: Array.isArray(e.valores) ? e.valores.map((v) => String(v)).filter((v) => v.trim() !== "") : [],
    }))
    .slice(0, 2);
}

export type GradeDesenhada = {
  /** Eixo das linhas (o primeiro), ou null no item sem grade. */
  eixoDasLinhas: string | null;
  /** Eixo das colunas (o segundo), ou null. */
  eixoDasColunas: string | null;
  colunas: Array<string | null>;
  linhas: Array<{ valor: string | null; celulas: Array<{ coluna: string | null; celula: Celula | null }> }>;
  /* Células com saldo que não cabem mais na grade (valor apagado do eixo). */
  foraDaGrade: Celula[];
};

/** A grade em linhas (eixo 1) e colunas (eixo 2), com a célula de cada cruzamento. */
export function desenharGrade(gradeBruta: unknown, celulas: Celula[]): GradeDesenhada {
  const grade = gradeValida(gradeBruta);
  const porChave = new Map(celulas.map((c) => [chaveDaCelula(c.valor1, c.valor2), c]));
  if (grade.length === 0) {
    const unica = porChave.get("|") ?? celulas[0] ?? null;
    return {
      eixoDasLinhas: null,
      eixoDasColunas: null,
      colunas: [null],
      linhas: [{ valor: null, celulas: [{ coluna: null, celula: unica }] }],
      foraDaGrade: celulas.filter((c) => c !== unica && Number(c.saldo) !== 0),
    };
  }
  const [eixo1, eixo2] = grade;
  const colunas: Array<string | null> = eixo2 ? eixo2.valores : [null];
  const usadas = new Set<string>();
  const linhas = eixo1.valores.map((v1) => ({
    valor: v1,
    celulas: colunas.map((v2) => {
      const chave = chaveDaCelula(v1, v2);
      usadas.add(chave);
      return { coluna: v2, celula: porChave.get(chave) ?? null };
    }),
  }));
  return {
    eixoDasLinhas: eixo1.nome,
    eixoDasColunas: eixo2 ? eixo2.nome : null,
    colunas,
    linhas,
    foraDaGrade: celulas.filter((c) => !usadas.has(chaveDaCelula(c.valor1, c.valor2)) && Number(c.saldo) !== 0),
  };
}

/** Texto curto da grade: "Tamanho × Cor (12)". */
export function resumoDaGrade(gradeBruta: unknown): string | null {
  const grade = gradeValida(gradeBruta);
  if (!grade.length) return null;
  const total = grade.reduce((n, e) => n * Math.max(1, e.valores.length), 1);
  return `${grade.map((e) => e.nome).join(" × ")} (${total})`;
}

/* ------------------------------------------------- escolha → célula (nome) */

export type Escolha = { atributo: string | null; valor: string | null; option_id?: string | null };

export function escolhasDaCombinacao(comb: Combinacao): Escolha[] {
  return Object.entries(comb).map(([atributo, valor]) => ({ atributo, valor }));
}

function combina(comb: Combinacao, escolhas: Escolha[]): boolean {
  return Object.entries(comb).every(([k, v]) =>
    escolhas.some((e) => mesmoNome(e.atributo, k) && mesmoNome(e.valor, v))
  );
}

/**
 * A célula que o banco vai usar para esta escolha — o mesmo caminho de
 * `estoque_v2_resolver_celula`:
 *  1. troca manual mais específica que cabe na escolha;
 *  2. pelo nome: cada eixo pega o valor do atributo de mesmo nome; sem
 *     atributo com o nome do eixo, vale a ÚNICA escolha cujo valor existe
 *     no eixo;
 *  3. item sem grade = a célula única.
 * Devolve também COMO achou, para a tela explicar ("pelo nome", "troca manual").
 */
export function celulaDaEscolha(
  gradeBruta: unknown,
  celulas: Celula[],
  escolhas: Escolha[],
  manual: LinhaDoMapa[] = []
): { celula: Celula | null; como: "manual" | "nome" | "unica" | "falta" ; falta?: string } {
  const doItem = new Map(celulas.map((c) => [c.celula_id, c]));
  const candidatas = manual
    .filter((m) => m && m.combinacao && typeof m.combinacao === "object" && doItem.has(m.celula_id) && combina(m.combinacao, escolhas))
    .sort((a, b) => Object.keys(b.combinacao).length - Object.keys(a.combinacao).length);
  if (candidatas.length) return { celula: doItem.get(candidatas[0].celula_id) ?? null, como: "manual" };

  const grade = gradeValida(gradeBruta);
  if (!grade.length) {
    const unica = celulas.find((c) => chaveDaCelula(c.valor1, c.valor2) === "|") ?? celulas[0] ?? null;
    return { celula: unica, como: unica ? "unica" : "falta" };
  }
  const valores: Array<string | null> = [null, null];
  for (let i = 0; i < grade.length; i++) {
    const eixo = grade[i];
    const doAtributo = escolhas.find((e) => mesmoNome(e.atributo, eixo.nome) && normalizar(e.valor));
    let achado: string | null = null;
    if (doAtributo) {
      achado = eixo.valores.find((v) => mesmoNome(v, doAtributo.valor)) ?? null;
    } else {
      const cabem = eixo.valores.filter((v) => escolhas.some((e) => mesmoNome(e.valor, v)));
      achado = cabem.length === 1 ? cabem[0] : null;
    }
    if (!achado) return { celula: null, como: "falta", falta: grade.map((e) => e.nome.toLowerCase()).join(" e ") };
    valores[i] = achado;
  }
  const chave = chaveDaCelula(valores[0], valores[1]);
  const celula = celulas.find((c) => chaveDaCelula(c.valor1, c.valor2) === chave) ?? null;
  return { celula, como: celula ? "nome" : "falta", falta: celula ? undefined : grade.map((e) => e.nome.toLowerCase()).join(" e ") };
}

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

/** Atributos de escolha única (os que formam uma combinação). */
export function atributosDeEscolhaUnica(atributos: AtributoDoProduto[]): AtributoDoProduto[] {
  return atributos.filter((a) => !a.allow_multiple && Array.isArray(a.options) && a.options.some((o) => String(o.label || "").trim()));
}

/**
 * As combinações que importam para uma grade: os atributos com o nome de um
 * eixo; se nenhum casar pelo nome, todos os de escolha única (até 2), para a
 * pessoa ligar à mão. Produto sem atributo = uma combinação vazia.
 */
export function combinacoesParaAGrade(atributos: AtributoDoProduto[], gradeBruta: unknown, limite = 120): Combinacao[] {
  const grade = gradeValida(gradeBruta);
  const unicos = atributosDeEscolhaUnica(atributos);
  let usados = unicos.filter((a) => grade.some((e) => mesmoNome(e.nome, a.name)));
  if (usados.length === 0) usados = grade.length ? unicos.slice(0, 2) : [];
  let combs: Combinacao[] = [{}];
  for (const a of usados) {
    const proximas: Combinacao[] = [];
    for (const c of combs) {
      for (const o of a.options) {
        const label = String(o.label || "").trim();
        if (!label) continue;
        proximas.push({ ...c, [a.name]: label });
        if (proximas.length >= limite) break;
      }
      if (proximas.length >= limite) break;
    }
    combs = proximas;
  }
  return combs;
}

export function textoDaCombinacao(comb: Combinacao): string {
  const valores = Object.values(comb).filter(Boolean);
  return valores.length ? valores.join(" / ") : "Qualquer escolha";
}

/** `selected_options` no formato do pedido, com o id da opção quando existe. */
export function selecaoDaCombinacao(comb: Combinacao, atributos: AtributoDoProduto[] = []): { options: Array<{ attribute_name: string; label: string; option_id?: string }> } {
  return {
    options: Object.entries(comb)
      .filter(([, v]) => String(v || "").trim())
      .map(([attribute_name, label]) => {
        const attr = atributos.find((a) => mesmoNome(a.name, attribute_name));
        const opcao = attr?.options.find((o) => mesmoNome(o.label, label));
        return opcao?.id ? { attribute_name, label, option_id: opcao.id } : { attribute_name, label };
      }),
  };
}

/* ------------------------------------------------------------ disponível */

/**
 * "Dá para vender agora". Quando nada trava a venda (todos os itens permitem
 * negativo), o banco devolve `disponivel: null`; a tela ainda quer dizer com
 * quantas peças o saldo de hoje dá — a conta simples saldo ÷ quanto sai de cada.
 */
export function quantoDaParaFazer(d: Pick<Disponibilidade, "disponivel" | "itens" | "pedido" | "motivo"> | null | undefined): number | null {
  if (!d || d.motivo === "falta_escolher") return null;
  if (d.disponivel !== null && d.disponivel !== undefined) return Number(d.disponivel);
  const pedido = Number(d.pedido) > 0 ? Number(d.pedido) : 1;
  let menor: number | null = null;
  for (const it of d.itens ?? []) {
    const porPeca = Number(it.precisa) / pedido;
    if (!(porPeca > 0)) continue;
    const da = Math.max(0, Math.floor((Number(it.saldo) + 1e-9) / porPeca));
    menor = menor === null ? da : Math.min(menor, da);
  }
  return menor;
}

/* ---------------------------------------------------------- composição */

export type LinhaDaComposicao = {
  quando_sai?: QuandoSai | null;
  condicao_atributo?: string | null;
  condicao_valor?: string | null;
  condicao_opcao_id?: string | null;
  cost_total?: number | null;
};

/**
 * Custo base do produto no estoque novo: linha "só se a opção X" não entra
 * (ela só acontece quando a opção é escolhida — a tela mostra "+R$ x quando
 * escolhida"). Fora do v2 nenhuma linha é `se_opcao`, então a soma é a de
 * sempre.
 */
export function custoBaseDaComposicao(linhas: LinhaDaComposicao[]): number {
  return linhas.reduce((s, l) => (l.quando_sai === "se_opcao" ? s : s + (Number(l.cost_total) || 0)), 0);
}

export function textoQuandoSai(l: LinhaDaComposicao): string {
  switch (l.quando_sai) {
    case "pela_variacao":
      return "Pela variação";
    case "se_opcao": {
      const valor = String(l.condicao_valor || "").trim();
      const atributo = String(l.condicao_atributo || "").trim();
      if (valor && atributo) return `Só se ${atributo} = ${valor}`;
      if (valor) return `Só se escolher ${valor}`;
      return "Só com a opção";
    }
    case "so_custo":
      return "Só custo";
    default:
      return "Sempre";
  }
}

/* ------------------------------------------------------------------ erros */

const DICAS: Record<string, string> = {
  ESTOQUE_SEM_PERMISSAO: "Você não tem permissão para mexer no estoque desta empresa.",
  ESTOQUE_V2_DESLIGADO: "O estoque novo ainda não está ligado nesta empresa.",
};

/** A mensagem do banco já vem em português; só alguns códigos ganham texto próprio. */
export function mensagemDoErro(erro: { message?: string | null; hint?: string | null } | null | undefined, padrao = "Não deu certo. Tente de novo."): string {
  if (!erro) return padrao;
  const hint = String(erro.hint || "");
  if (DICAS[hint] && !erro.message) return DICAS[hint];
  const msg = String(erro.message || "").trim();
  return msg || DICAS[hint] || padrao;
}
