/**
 * O motor de preço: um caminho só, do custo até o preço.
 *
 * A geração anterior (`getPriceSuggestion`) fazia `preço = custo ÷ (1 − margem)`
 * e parava aí. A conta está certa para o mundo em que a venda não tem imposto,
 * taxa de maquininha nem comissão — que não é este. Com 20% da venda saindo
 * nesses três, uma "margem de 50%" configurada entrega 30% de verdade, e a
 * empresa só descobre no fim do mês, quando o dinheiro não bate com a planilha.
 *
 * A correção é uma linha de álgebra. Sendo P o preço, C o custo, v os custos
 * variáveis como fração do preço e m a margem desejada:
 *
 *     P = C + v·P + m·P   →   P·(1 − v − m) = C   →   P = C ÷ (1 − v − m)
 *
 * Então os custos variáveis entram no divisor, junto com a margem. É o mesmo
 * motivo pelo qual quem repassa imposto "somando 6% no fim" fica devendo: 6%
 * sobre o custo não é 6% sobre o preço.
 *
 * Três modos, porque o ramo tem três formas de vender:
 *   unidade — brinde, caneca, camiseta: custo por peça
 *   m²      — lona, adesivo, fachada: custo por metro quadrado
 *   faixa   — o preço por peça cai conforme a quantidade sobe
 */

export type ModoDePreco = "unidade" | "m2" | "faixa";

export type FaixaDeQuantidade = {
  minQty: number;
  maxQty?: number;
  marginTargetPct: number;
};

export type Arredondamento = {
  type: "NONE" | "END_WITH" | "NEAREST_INT" | "MULTIPLE_OF";
  cents?: number;
  step?: number;
};

export type RegraDePreco = {
  marginTargetPct: number;
  marginMinPct: number;
  rounding: Arredondamento;
  qtyTiers?: FaixaDeQuantidade[];
};

export type EntradaDoMotor = {
  /* Custo de uma unidade — ou de um m², no modo m². */
  custoUnitario: number;
  quantidade: number;
  modo: ModoDePreco;
  /* Só no modo m². Em metros. */
  larguraM?: number | null;
  alturaM?: number | null;
  regra: RegraDePreco;
  /* O que sai da venda em percentual: imposto, taxa de recebimento, comissão,
     perda. Vem de `custo_variavel_da_venda`. */
  custosVariaveisPct?: number;
  /* Custos fixos absorvidos pelo tempo de produção, em R$ POR UNIDADE cobrada:
     as horas da ficha vezes a hora da estrutura (fixo de estrutura mensal ÷
     horas produtivas da empresa). Entra somado ao custo, não no divisor — o
     rateio percentual da venda foi aposentado porque cobrava a mesma fatia de
     um chaveiro de 2 minutos e de uma fachada de 3 horas. */
  custoFixoUnitario?: number;
};

export type ResultadoDoMotor = {
  precoUnitario: number;
  precoTotal: number;
  /* Quantas unidades de cobrança: peças, ou m² já multiplicados pela
     quantidade. */
  quantidadeCobravel: number;
  custoTotal: number;
  /* Margem de contribuição: o que sobra depois do custo e dos variáveis. */
  margemPct: number;
  margemValor: number;
  /* Para onde vai cada real do preço. Serve para a tela explicar em vez de
     apenas exibir. */
  composicao: {
    custoPct: number;
    variaveisPct: number;
    /* Zero quando a empresa não configurou as horas produtivas: a fatia só
       existe quando os fixos entram no preço pelo tempo. */
    fixosPct: number;
    margemPct: number;
  };
  avisos: string[];
  /* Falso quando a conta não fecha e o preço devolvido não é confiável. */
  possivel: boolean;
};

const duasCasas = (n: number) => Math.round(n * 100) / 100;

/* Acima disto o divisor fica tao pequeno que o preco dispara e qualquer erro de
   digitacao vira um numero absurdo. Nao e limite tecnico, e limite de sanidade. */
const TETO_DE_MARGEM = 95;

/**
 * Quantas unidades serão cobradas.
 *
 * No modo m² a área multiplica: 2m × 3m com quantidade 4 são 24 m² cobrados. É
 * onde mais se erra na mão, porque a conta parece simples e ninguém confere.
 */
export function quantidadeCobravel(entrada: EntradaDoMotor): number {
  const qtd = Math.max(0, Number(entrada.quantidade) || 0);
  if (entrada.modo !== "m2") return qtd;

  const largura = Math.max(0, Number(entrada.larguraM) || 0);
  const altura = Math.max(0, Number(entrada.alturaM) || 0);
  const area = largura * altura;
  /* Sem medida nao da para cobrar por area; devolver a quantidade crua seria
     cobrar por peca fingindo que e por metro. */
  if (area <= 0) return 0;
  return duasCasas(area * qtd);
}

/** A margem que vale para esta quantidade, considerando as faixas. */
export function margemDaFaixa(regra: RegraDePreco, quantidade: number): number {
  const faixas = regra.qtyTiers ?? [];
  if (faixas.length === 0) return regra.marginTargetPct;

  /* Da maior faixa para a menor: quem compra 500 deve cair na faixa de 500, nao
     na de 100 que tambem "aceita" 500 por nao ter teto. */
  const ordenadas = [...faixas].sort((a, b) => b.minQty - a.minQty);
  const escolhida = ordenadas.find((f) => {
    if (quantidade < f.minQty) return false;
    if (f.maxQty !== undefined && f.maxQty !== null && quantidade > f.maxQty) return false;
    return true;
  });

  return escolhida ? escolhida.marginTargetPct : regra.marginTargetPct;
}

function arredondar(valor: number, regra: Arredondamento): number {
  if (regra.type === "NEAREST_INT") return Math.round(valor);

  if (regra.type === "MULTIPLE_OF") {
    const passo = regra.step ?? 5;
    if (passo <= 0) return valor;
    return Math.ceil(valor / passo) * passo;
  }

  if (regra.type === "END_WITH") {
    const centavos = regra.cents ?? 90;
    const inteiro = Math.floor(valor);
    let candidato = inteiro + centavos / 100;
    /* Arredondar para baixo cortaria margem justamente no centavo que a regra
       existe para proteger. */
    if (candidato < valor) candidato += 1;
    return candidato;
  }

  return valor;
}

export function calcularPreco(entrada: EntradaDoMotor): ResultadoDoMotor {
  const avisos: string[] = [];
  const cobravel = quantidadeCobravel(entrada);
  const custoUnitario = Math.max(0, Number(entrada.custoUnitario) || 0);
  const custoTotal = duasCasas(custoUnitario * cobravel);

  const vazio = (motivo: string): ResultadoDoMotor => {
    avisos.push(motivo);
    return {
      precoUnitario: 0, precoTotal: 0, quantidadeCobravel: cobravel,
      custoTotal, margemPct: 0, margemValor: 0,
      composicao: { custoPct: 0, variaveisPct: 0, fixosPct: 0, margemPct: 0 },
      avisos, possivel: false,
    };
  };

  if (entrada.modo === "m2" && cobravel <= 0) {
    return vazio("Informe largura e altura para cobrar por metro quadrado.");
  }
  if (cobravel <= 0) return vazio("Informe a quantidade.");
  if (custoUnitario <= 0) {
    return vazio("Sem custo conhecido não dá para sugerir preço — a margem seria sobre o nada.");
  }

  const variaveis = Math.max(0, Number(entrada.custosVariaveisPct) || 0);
  /* Os fixos pelo tempo somam no que precisa ser coberto, junto do custo. É o
     que os torna proporcionais: peça que ocupa mais horas carrega mais fixo. */
  const fixoUnitario = Math.max(0, Number(entrada.custoFixoUnitario) || 0);
  const valorDosFixos = duasCasas(fixoUnitario * cobravel);
  let margemAlvo = margemDaFaixa(entrada.regra, entrada.quantidade);

  if (margemAlvo > TETO_DE_MARGEM) {
    avisos.push(`Margem de ${margemAlvo}% limitada a ${TETO_DE_MARGEM}%.`);
    margemAlvo = TETO_DE_MARGEM;
  }
  if (margemAlvo < 0) margemAlvo = 0;

  /* O divisor. Se margem e variaveis somam 100% ou mais, nao existe preco que
     satisfaca — cada real cobrado a mais gera mais imposto e mais comissao, e
     a conta nunca fecha. Melhor dizer isso do que devolver um numero gigante. */
  const divisor = 1 - variaveis / 100 - margemAlvo / 100;
  if (divisor <= 0.01) {
    return vazio(
      `Margem de ${margemAlvo}% mais ${variaveis.toFixed(1)}% de custos variáveis não deixam espaço para o custo. Reduza a margem alvo.`
    );
  }

  const precoBruto = (custoTotal + valorDosFixos) / divisor;
  const precoTotalArredondado = duasCasas(arredondar(precoBruto, entrada.regra.rounding));
  const precoUnitario = duasCasas(precoTotalArredondado / cobravel);

  const valorDosVariaveis = duasCasas(precoTotalArredondado * (variaveis / 100));
  const margemValor = duasCasas(precoTotalArredondado - custoTotal - valorDosFixos - valorDosVariaveis);
  const margemPct =
    precoTotalArredondado > 0 ? duasCasas((margemValor / precoTotalArredondado) * 100) : 0;

  if (margemPct < entrada.regra.marginMinPct) {
    avisos.push(
      `Margem de ${margemPct.toFixed(1)}% abaixo do mínimo de ${entrada.regra.marginMinPct}%.`
    );
  }
  if (margemValor < 0) {
    avisos.push(
      valorDosFixos > 0
        ? "Este preço não cobre o custo, os fixos pelo tempo e os impostos e taxas da venda."
        : "Este preço não cobre o custo mais os impostos e taxas da venda."
    );
  }

  return {
    precoUnitario,
    precoTotal: precoTotalArredondado,
    quantidadeCobravel: cobravel,
    custoTotal,
    margemPct,
    margemValor,
    composicao: {
      custoPct: precoTotalArredondado > 0 ? duasCasas((custoTotal / precoTotalArredondado) * 100) : 0,
      variaveisPct: duasCasas(variaveis),
      fixosPct: precoTotalArredondado > 0 ? duasCasas((valorDosFixos / precoTotalArredondado) * 100) : 0,
      margemPct,
    },
    avisos,
    possivel: true,
  };
}
