/*
  O estoque na hora de vender, no painel (PDV, orçamento, assistente) — contas
  puras, sem React e sem banco.

  A regra é UMA só para todos os canais e mora no banco:
  - estoque_disponivel(produto, escolha, quantidade, largura, altura): "Ao
    vender sai…" e "Dá para vender agora: N" de uma combinação;
  - estoque_conferir_itens(dono, itens): o carrinho/orçamento inteiro,
    somando por célula (dois produtos que usam a mesma camiseta lisa disputam
    o mesmo saldo).
  Fora do estoque v2 as duas respondem pela regra antiga (controla estoque e
  não permite negativo, geral/combinação/opção). Nas duas, produto que não
  controla, ou que permite negativo, NUNCA aparece como falta.

  Aqui só se traduz a resposta para a tela: a frase de cada linha e a frase de
  um erro do banco. Testado em estoque-da-venda.test.ts.
*/

export type ItemParaConferir = {
  product_id?: string | null;
  quantity?: number | string | null;
  selected_options?: unknown;
  width_m?: number | string | null;
  height_m?: number | string | null;
  title?: string | null;
};

export type ProblemaDeEstoque = {
  indice?: number | null;
  produto_id?: string | null;
  titulo?: string | null;
  celula_id?: string | null;
  disponivel?: number | null;
  mensagem?: string | null;
};

export type RespostaDaConferencia = { ok?: boolean; problemas?: ProblemaDeEstoque[] | null } | null;

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

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

/**
 * As linhas no formato do banco. Guarda de onde cada uma veio (`origem` =
 * índice na lista original), porque linha avulsa e quantidade zero não vão.
 */
export function itensParaConferir(linhas: ItemParaConferir[] | null | undefined): {
  itens: Array<{ product_id: string; quantity: number; selected_options: unknown; width_m: number | null; height_m: number | null; title: string | null }>;
  origem: number[];
} {
  const itens: Array<{ product_id: string; quantity: number; selected_options: unknown; width_m: number | null; height_m: number | null; title: string | null }> = [];
  const origem: number[] = [];
  (Array.isArray(linhas) ? linhas : []).forEach((linha, indice) => {
    const produto = String(linha?.product_id || "");
    const quantidade = numeroOuNulo(linha?.quantity) ?? 0;
    if (!UUID.test(produto) || !(quantidade > 0)) return;
    const escolha = linha?.selected_options;
    itens.push({
      product_id: produto,
      quantity: quantidade,
      selected_options: escolha && typeof escolha === "object" && !Array.isArray(escolha) ? escolha : null,
      width_m: (numeroOuNulo(linha?.width_m) ?? 0) > 0 ? numeroOuNulo(linha?.width_m) : null,
      height_m: (numeroOuNulo(linha?.height_m) ?? 0) > 0 ? numeroOuNulo(linha?.height_m) : null,
      title: linha?.title ? String(linha.title) : null,
    });
    origem.push(indice);
  });
  return { itens, origem };
}

/**
 * A falta de cada linha (índice da lista ORIGINAL → frases), pela resposta
 * de estoque_conferir_itens. A frase é a do banco, que já diz a combinação
 * e o que falta: 'Acabou o estoque de "Camiseta lisa (G / Preta)" para
 * "Camisetas": tem 0, o pedido precisa de 3.'
 */
export function faltasPorLinha(resposta: RespostaDaConferencia, origem: number[]): Map<number, string[]> {
  const faltas = new Map<number, string[]>();
  if (!resposta || resposta.ok !== false) return faltas;
  for (const problema of Array.isArray(resposta.problemas) ? resposta.problemas : []) {
    const doBanco = Number(problema?.indice);
    if (!Number.isInteger(doBanco) || doBanco < 0 || doBanco >= origem.length) continue;
    const linha = origem[doBanco];
    const frase = String(problema?.mensagem || "").trim() || "Sem estoque suficiente para este item.";
    const lista = faltas.get(linha) || [];
    if (!lista.includes(frase)) lista.push(frase);
    faltas.set(linha, lista);
  }
  return faltas;
}

/** Todas as faltas numa frase só (para o aviso que trava). null = cabe. */
export function fraseDasFaltas(faltas: Map<number, string[]>, limite = 3): string | null {
  const todas = Array.from(faltas.values()).flat();
  if (!todas.length) return null;
  const primeiras = todas.slice(0, limite).join(" ");
  const resto = todas.length > limite ? ` E mais ${todas.length - limite}.` : "";
  return `${primeiras}${resto}`;
}

/**
 * O erro de estoque que o banco devolve (venda, aprovação, edição), em
 * frase para a pessoa: a mensagem do banco já está em português; o código
 * (hint) e o detalhe técnico (JSON) ficam de fora.
 */
export function mensagemDoErroDeEstoque(
  erro: { message?: string | null; hint?: string | null; details?: string | null } | null | undefined,
  padrao = "Não foi possível concluir por causa do estoque."
): string {
  const mensagem = String(erro?.message || "").trim();
  if (!mensagem) return padrao;
  return mensagem;
}

/** O erro veio do estoque (v2 ou trava antiga)? */
export function ehErroDeEstoque(erro: { message?: string | null; hint?: string | null } | null | undefined): boolean {
  const hint = String(erro?.hint || "");
  if (hint.startsWith("ESTOQUE_")) return true;
  return /estoque insuficiente|sem estoque|opcao de estoque nao encontrada/i.test(String(erro?.message || ""));
}

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

export type Disponibilidade = {
  v2?: boolean;
  modo?: string | null;
  controla?: boolean | null;
  limita?: boolean | null;
  pedido?: number | null;
  cabe?: boolean | null;
  disponivel?: number | null;
  motivo?: string | null;
  itens?: Array<{
    nome?: string | null;
    rotulo?: string | null;
    unidade?: string | null;
    saldo?: number | null;
    precisa?: number | null;
    permite_negativo?: boolean | null;
    sem_celula?: boolean | null;
    falta_escolher?: string | null;
  }> | null;
};

const FORMATO = new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 3 });
function qtd(n: unknown): string {
  const v = Number(n);
  return FORMATO.format(Number.isFinite(v) ? v : 0);
}

/**
 * O que falta para vender a quantidade pedida, item a item, só do que limita:
 * "Falta Camiseta lisa (G / Preta): tem 0, precisa 3." Nada = cabe (ou não
 * controla / permite negativo).
 */
export function faltasDaDisponibilidade(d: Disponibilidade | null | undefined): string[] {
  if (!d) return [];
  const frases: string[] = [];
  for (const item of Array.isArray(d.itens) ? d.itens : []) {
    const nome = String(item?.rotulo || item?.nome || "item").trim();
    if (item?.sem_celula) {
      frases.push(`Escolha ${item.falta_escolher || "a variação"} para tirar "${nome}" do estoque.`);
      continue;
    }
    if (item?.permite_negativo !== false) continue;
    const saldo = Number(item?.saldo ?? 0);
    const precisa = Number(item?.precisa ?? 0);
    if (saldo + 0.000001 < precisa) {
      frases.push(`Falta ${nome}: tem ${qtd(Math.max(saldo, 0))}, precisa ${qtd(precisa)}.`);
    }
  }
  if (!frases.length && d.cabe === false && d.motivo === "combinacao_nao_encontrada") {
    frases.push("A combinação escolhida não existe nas variações do produto.");
  }
  if (!frases.length && d.cabe === false && d.motivo === "opcao_sem_estoque") {
    frases.push("A opção escolhida não tem estoque cadastrado.");
  }
  if (!frases.length && d.cabe === false && d.limita) {
    frases.push(`Sem estoque suficiente: dá para vender ${qtd(Math.max(Number(d.disponivel ?? 0), 0))}.`);
  }
  return frases;
}

/** "Dá para vender agora: 12" / "Sem limite de estoque" / "Esgotado" para a linha do painel. */
export function textoDoDisponivel(d: Disponibilidade | null | undefined): string | null {
  if (!d) return null;
  if (d.motivo === "falta_escolher") return null;
  if (!d.limita) return d.controla ? "Vende sem limite (estoque pode ficar negativo)" : null;
  const n = Math.max(Number(d.disponivel ?? 0), 0);
  return n <= 0 ? "Esgotado" : `Dá para vender agora: ${qtd(n)}`;
}

/** Esgotado na lista (sem escolha, quantidade 1): limita, não cabe e não falta escolher. */
export function esgotadoNaLista(d: Pick<Disponibilidade, "limita" | "cabe" | "motivo"> | null | undefined): boolean {
  return Boolean(d) && d?.limita === true && d?.cabe === false && !d?.motivo;
}


/* -------------------------------------------- conferir (com outro caminho) */

/*
  estoque_conferir_itens limpa a tabela temporária com DELETE sem WHERE, e o
  Supabase recusa isso vindo da API ("DELETE requires a WHERE clause"). A
  correção está na migration 212 (ainda não aplicada). Até lá, no estoque v2,
  a mesma conta é refeita aqui com estoque_disponivel item a item, somando
  por célula. Fora do v2: null ("não deu para conferir"), como era até hoje.
  Cópia igual em apps/public/src/lib/conferir-itens.ts.
*/

type RpcDaConferencia = {
  rpc: (fn: string, params?: Record<string, unknown>) => PromiseLike<{ data: unknown; error: { message?: string } | null }>;
};

export type ItemDaConferencia = {
  product_id: string;
  quantity: number;
  selected_options?: unknown;
  width_m?: number | null;
  height_m?: number | null;
  title?: string | null;
};

export type ProblemaDaConferencia = {
  indice: number;
  produto_id: string;
  titulo: string;
  celula_id: string | null;
  disponivel: number;
  mensagem: string;
};

type ItemDoDisponivel = {
  item_id?: string | null;
  celula_id?: string | null;
  nome?: string | null;
  rotulo?: string | null;
  saldo?: number | null;
  precisa?: number | null;
  permite_negativo?: boolean | null;
  sem_celula?: boolean | null;
  falta_escolher?: string | null;
};

function numero(valor: number): string {
  const arredondado = Number(valor.toFixed(4));
  return new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 4 }).format(arredondado);
}

/** A conta da função do banco, refeita com as respostas de estoque_disponivel (só v2). */
export function somarPorCelula(
  itens: ItemDaConferencia[],
  respostas: Array<{ v2?: boolean; itens?: ItemDoDisponivel[] | null } | null>
): { ok: boolean; problemas: ProblemaDaConferencia[] } | null {
  if (respostas.some((r) => !r || r.v2 !== true)) return null;
  const porCelula = new Map<string, number>();
  respostas.forEach((r) => {
    for (const linha of r!.itens || []) {
      if (!linha.celula_id) continue;
      porCelula.set(linha.celula_id, (porCelula.get(linha.celula_id) || 0) + Number(linha.precisa || 0));
    }
  });
  const problemas: ProblemaDaConferencia[] = [];
  respostas.forEach((r, indice) => {
    const titulo = String(itens[indice]?.title || "produto");
    const vistos = new Set<string>();
    for (const linha of r!.itens || []) {
      const chave = String(linha.celula_id || `sem:${linha.item_id || ""}`);
      if (vistos.has(chave)) continue;
      if (!linha.celula_id) {
        vistos.add(chave);
        problemas.push({
          indice,
          produto_id: itens[indice].product_id,
          titulo,
          celula_id: null,
          disponivel: 0,
          mensagem: linha.item_id
            ? `Escolha ${linha.falta_escolher || "a variação"} de "${titulo}".`
            : `"${titulo}" sai como pronto, mas não tem item de estoque ligado.`,
        });
        continue;
      }
      if (linha.permite_negativo !== false) continue;
      const saldo = Number(linha.saldo || 0);
      const precisa = porCelula.get(linha.celula_id) || 0;
      if (saldo + 0.0000001 >= precisa) continue;
      vistos.add(chave);
      problemas.push({
        indice,
        produto_id: itens[indice].product_id,
        titulo,
        celula_id: linha.celula_id,
        disponivel: Math.max(saldo, 0),
        mensagem: `Acabou o estoque de "${linha.rotulo || linha.nome || "item"}" para "${titulo}": tem ${numero(Math.max(saldo, 0))}, o pedido precisa de ${numero(precisa)}.`,
      });
    }
  });
  return { ok: problemas.length === 0, problemas };
}

/** O carrinho no banco. null = não deu para conferir (a trava do pedido decide). */
export async function conferirItensNoBanco(
  cliente: RpcDaConferencia,
  dono: string,
  itens: ItemDaConferencia[]
): Promise<{ ok: boolean; problemas: ProblemaDaConferencia[] } | null> {
  if (!dono || !itens.length) return { ok: true, problemas: [] };
  const direta = await cliente.rpc("estoque_conferir_itens", { p_dono: dono, p_itens: itens });
  if (!direta.error && direta.data && typeof direta.data === "object") {
    return direta.data as { ok: boolean; problemas: ProblemaDaConferencia[] };
  }
  const perguntar = async (item: ItemDaConferencia) => {
    const { data, error } = await cliente.rpc("estoque_disponivel", {
      p_produto: item.product_id,
      p_selected_options: item.selected_options && typeof item.selected_options === "object" ? item.selected_options : null,
      p_quantidade: Number(item.quantity) || 0,
      p_largura: Number(item.width_m) > 0 ? Number(item.width_m) : null,
      p_altura: Number(item.height_m) > 0 ? Number(item.height_m) : null,
      p_maximo: false,
    });
    return error ? null : ((data ?? null) as { v2?: boolean; itens?: ItemDoDisponivel[] } | null);
  };
  /* O primeiro diz se a empresa está no v2; fora dele não há outro caminho
     e não vale gastar uma pergunta por item. */
  const primeira = await perguntar(itens[0]);
  if (!primeira || primeira.v2 !== true) return null;
  const resto = await Promise.all(itens.slice(1).map(perguntar));
  return somarPorCelula(itens, [primeira, ...resto]);
}

/* ------------------------------------------------- conferir no servidor */

type Rpc = {
  rpc: (fn: string, params?: Record<string, unknown>) => PromiseLike<{ data: unknown; error: { message?: string } | null }>;
};

/**
 * Confere as linhas no banco (estoque_conferir_itens) e devolve a frase de
 * falta, ou null quando cabe. Falha do próprio banco ao conferir não trava
 * (a trava da criação do pedido continua valendo e responde com a frase dela).
 */
export async function conferirEstoqueDaVenda(
  cliente: Rpc,
  dono: string | null | undefined,
  linhas: ItemParaConferir[]
): Promise<{ frase: string | null; faltas: Map<number, string[]> }> {
  const { itens, origem } = itensParaConferir(linhas);
  if (!dono || !itens.length) return { frase: null, faltas: new Map() };
  const resposta = await conferirItensNoBanco(cliente, dono, itens).catch(() => null);
  if (!resposta) return { frase: null, faltas: new Map() };
  const faltas = faltasPorLinha(resposta as RespostaDaConferencia, origem);
  return { frase: fraseDasFaltas(faltas), faltas };
}
