/**
 * Tempo de produção: o previsto na ficha contra o real da bancada, e a fila
 * contra a capacidade do mês.
 *
 * A precificação chuta minutos — 4 min de impressora por m², 10 de
 * acabamento por peça — e esses minutos viram custo de máquina, custo de
 * gente e a fatia de custo fixo pelo tempo. Se o chute está errado, o preço
 * está errado, e ninguém descobre porque o tempo real nunca voltava para a
 * ficha. Aqui ele volta: por produto, quantos pedidos, quanto era previsto,
 * quanto levou, e o fator que corrigiria a receita.
 *
 * O real vem de duas fontes, e a mais precisa vence: o apontamento por item
 * (quem fez, de quando a quando) e, na falta dele, o tempo útil do cronômetro
 * do pedido rateado entre os itens pelo previsto de cada um.
 *
 * A fila é a outra ponta: os minutos previstos dos pedidos ainda abertos,
 * contra as horas produtivas do mês — o mesmo divisor da hora da estrutura.
 * Dali sai quantos dias úteis a fila ocupa e o próximo prazo livre.
 *
 * Tudo puro; o teste ao lado cobre as pontas.
 */

export type FonteDoReal = "item" | "cronometro";

export type LinhaDeTempo = {
  productId: string;
  nome: string;
  orderId: string;
  itemId: string;
  previstoMin: number;
  realMin: number;
  fonte: FonteDoReal;
  /* Real reconstruído do histórico de status (sem pausas): vale menos. */
  aproximado?: boolean;
};

export type SituacaoDoTempo = "subestimado" | "superestimado" | "ok" | "poucos_dados";

export type ProdutoPrevistoXReal = {
  productId: string;
  nome: string;
  pedidos: number;
  itens: number;
  previstoMin: number;
  realMin: number;
  /* real ÷ previsto. 1 = a ficha acertou; 2 = leva o dobro. */
  razao: number;
  fonte: FonteDoReal | "misto";
  aproximados: number;
  situacao: SituacaoDoTempo;
  /* O fator para multiplicar os minutos da ficha, em passos de 0,05. Só
     quando há pedidos suficientes e a diferença é grande. */
  sugestaoFator: number | null;
};

const duasCasas = (n: number) => Math.round(n * 100) / 100;
const positivo = (v: unknown) => {
  const n = Number(v);
  return Number.isFinite(n) && n > 0 ? n : 0;
};

/* Acima disto a ficha subestima; abaixo, superestima. Uma diferença de 20%
   é ruído de bancada; 50% é receita errada. */
export const LIMITE_SUBESTIMADO = 1.5;
export const LIMITE_SUPERESTIMADO = 0.5;

const ORDEM_DA_SITUACAO: Record<SituacaoDoTempo, number> = {
  subestimado: 0,
  superestimado: 1,
  ok: 2,
  poucos_dados: 3,
};

export function compararPrevistoComReal(linhas: LinhaDeTempo[], minimoDePedidos = 3): ProdutoPrevistoXReal[] {
  type Acumulo = {
    nome: string;
    pedidos: Set<string>;
    itens: number;
    previsto: number;
    real: number;
    fontes: Set<FonteDoReal>;
    aproximados: number;
  };
  const porProduto = new Map<string, Acumulo>();

  for (const l of linhas) {
    const previsto = positivo(l.previstoMin);
    const real = positivo(l.realMin);
    if (!l.productId || previsto <= 0 || real <= 0) continue;
    const atual =
      porProduto.get(l.productId) ??
      { nome: l.nome, pedidos: new Set<string>(), itens: 0, previsto: 0, real: 0, fontes: new Set<FonteDoReal>(), aproximados: 0 };
    atual.pedidos.add(l.orderId);
    atual.itens += 1;
    atual.previsto += previsto;
    atual.real += real;
    atual.fontes.add(l.fonte);
    if (l.aproximado) atual.aproximados += 1;
    if (!atual.nome && l.nome) atual.nome = l.nome;
    porProduto.set(l.productId, atual);
  }

  const saida: ProdutoPrevistoXReal[] = [];
  for (const [productId, a] of porProduto) {
    const razao = duasCasas(a.real / a.previsto);
    const pedidos = a.pedidos.size;
    let situacao: SituacaoDoTempo = "ok";
    if (pedidos < minimoDePedidos) situacao = "poucos_dados";
    else if (razao >= LIMITE_SUBESTIMADO) situacao = "subestimado";
    else if (razao <= LIMITE_SUPERESTIMADO) situacao = "superestimado";

    saida.push({
      productId,
      nome: a.nome,
      pedidos,
      itens: a.itens,
      previstoMin: duasCasas(a.previsto),
      realMin: duasCasas(a.real),
      razao,
      fonte: a.fontes.size > 1 ? "misto" : a.fontes.has("item") ? "item" : "cronometro",
      aproximados: a.aproximados,
      situacao,
      sugestaoFator:
        situacao === "subestimado" || situacao === "superestimado" ? Math.max(0.05, Math.round(razao * 20) / 20) : null,
    });
  }

  return saida.sort((x, y) => {
    const s = ORDEM_DA_SITUACAO[x.situacao] - ORDEM_DA_SITUACAO[y.situacao];
    if (s !== 0) return s;
    if (y.pedidos !== x.pedidos) return y.pedidos - x.pedidos;
    return x.nome.localeCompare(y.nome, "pt-BR");
  });
}

/**
 * Divide um tempo do pedido entre os itens na proporção do previsto de cada
 * um. Sem previsto em nenhum, divide igual — é o melhor chute que existe.
 */
export function ratearPeloPrevisto(total: number, partes: { id: string; previsto: number }[]): Record<string, number> {
  const saida: Record<string, number> = {};
  if (partes.length === 0) return saida;
  const soma = partes.reduce((acc, p) => acc + positivo(p.previsto), 0);
  const valor = positivo(total);
  for (const p of partes) {
    const fracao = soma > 0 ? positivo(p.previsto) / soma : 1 / partes.length;
    saida[p.id] = duasCasas(valor * fracao);
  }
  return saida;
}

export type DataSimples = { ano: number; mes: number; dia: number };

function utcDe(d: DataSimples, deslocamentoDias = 0) {
  return Date.UTC(d.ano, d.mes - 1, d.dia + deslocamentoDias);
}

function dataSimplesDe(ms: number): DataSimples {
  const d = new Date(ms);
  return { ano: d.getUTCFullYear(), mes: d.getUTCMonth() + 1, dia: d.getUTCDate() };
}

export function formatarDataSimples(d: DataSimples): string {
  return `${d.ano}-${String(d.mes).padStart(2, "0")}-${String(d.dia).padStart(2, "0")}`;
}

/** Quantos dias do mês caem nos dias de expediente. */
export function diasUteisNoMes(ano: number, mes: number, diasDeExpediente: number[]): number {
  const dias = new Date(Date.UTC(ano, mes, 0)).getUTCDate();
  let total = 0;
  for (let dia = 1; dia <= dias; dia += 1) {
    if (diasDeExpediente.includes(new Date(Date.UTC(ano, mes - 1, dia)).getUTCDay())) total += 1;
  }
  return total;
}

/**
 * A data depois de N dias úteis. Zero devolve o próprio dia se ele é útil,
 * senão o próximo útil — a fila vazia entrega hoje, não ontem.
 */
export function somarDiasUteis(de: DataSimples, dias: number, diasDeExpediente: number[]): DataSimples {
  if (diasDeExpediente.length === 0) return dataSimplesDe(utcDe(de, dias));
  let cursor = utcDe(de);
  let restantes = Math.max(0, Math.ceil(dias));
  let guarda = 0;
  while (guarda++ < 5000) {
    const util = diasDeExpediente.includes(new Date(cursor).getUTCDay());
    if (util) {
      if (restantes === 0) break;
      restantes -= 1;
    }
    cursor += 86400000;
  }
  return dataSimplesDe(cursor);
}

export type CapacidadeDaFila = {
  horasFila: number;
  pedidosNaFila: number;
  /* Nulo quando a empresa não informou as horas produtivas do mês. */
  horasMes: number | null;
  diasUteisNoMes: number;
  horasPorDiaUtil: number | null;
  diasUteisNecessarios: number | null;
  /* AAAA-MM-DD: o dia em que a fila de hoje termina, no ritmo da capacidade. */
  prazoSugerido: string | null;
  /* Quanto do mês a fila já compromete. Passa de 100 quando não cabe. */
  ocupacaoDoMesPct: number | null;
};

export function capacidadeDaFila(entrada: {
  minutosFila: number;
  pedidos: number;
  horasProdutivasMes: number | null | undefined;
  diasDeExpediente: number[];
  hoje: DataSimples;
}): CapacidadeDaFila {
  const horasFila = duasCasas(positivo(entrada.minutosFila) / 60);
  const horasMes = positivo(entrada.horasProdutivasMes) || null;
  const diasUteis = diasUteisNoMes(entrada.hoje.ano, entrada.hoje.mes, entrada.diasDeExpediente);

  if (!horasMes || diasUteis <= 0) {
    return {
      horasFila,
      pedidosNaFila: Math.max(0, Math.round(entrada.pedidos) || 0),
      horasMes,
      diasUteisNoMes: diasUteis,
      horasPorDiaUtil: null,
      diasUteisNecessarios: null,
      prazoSugerido: null,
      ocupacaoDoMesPct: null,
    };
  }

  const horasPorDia = duasCasas(horasMes / diasUteis);
  const diasNecessarios = Math.ceil(horasFila / horasPorDia);
  return {
    horasFila,
    pedidosNaFila: Math.max(0, Math.round(entrada.pedidos) || 0),
    horasMes,
    diasUteisNoMes: diasUteis,
    horasPorDiaUtil: horasPorDia,
    diasUteisNecessarios: diasNecessarios,
    prazoSugerido: formatarDataSimples(somarDiasUteis(entrada.hoje, diasNecessarios, entrada.diasDeExpediente)),
    ocupacaoDoMesPct: duasCasas((horasFila / horasMes) * 100),
  };
}
