/**
 * O expediente da produção: o que separa tempo de fila de tempo de trabalho.
 *
 * O cronômetro do pedido corre de relógio. Ninguém pausa de noite nem no fim
 * de semana — 49 pausas em 2.598 pedidos — e o número que sobra mede quanto o
 * pedido ficou na fila, não quanto se trabalhou nele. A correção não é pedir
 * disciplina: é contar só o que cai dentro do expediente da empresa.
 *
 * Fuso horário sem biblioteca: `Intl.DateTimeFormat` já sabe converter um
 * instante para a hora local de qualquer fuso, e é tudo o que a conta precisa.
 * O Brasil não tem horário de verão desde 2019, mas a conta não depende disso
 * — o deslocamento é lido a cada dia percorrido.
 */

export type Expediente = {
  /* 0 = domingo … 6 = sábado. */
  dias: number[];
  /* "HH:MM". */
  inicio: string;
  fim: string;
  /* Nome IANA, ex.: America/Sao_Paulo. */
  fuso: string;
};

export const EXPEDIENTE_PADRAO: Expediente = {
  dias: [1, 2, 3, 4, 5],
  inicio: "08:00",
  fim: "18:00",
  fuso: "America/Sao_Paulo",
};

export const NOME_DO_DIA = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];

const HORA = /^([01]\d|2[0-3]):([0-5]\d)$/;

function fusoValido(fuso: string) {
  try {
    new Intl.DateTimeFormat("en-US", { timeZone: fuso });
    return true;
  } catch {
    return false;
  }
}

/** Normaliza o que veio do banco; qualquer coisa fora do formato cai no padrão. */
export function expedienteValido(raw: unknown): Expediente {
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return EXPEDIENTE_PADRAO;
  const r = raw as Record<string, unknown>;
  const dias = Array.isArray(r.dias)
    ? [...new Set(r.dias.map(Number).filter((d) => Number.isInteger(d) && d >= 0 && d <= 6))].sort((a, b) => a - b)
    : EXPEDIENTE_PADRAO.dias;
  const inicio = typeof r.inicio === "string" && HORA.test(r.inicio) ? r.inicio : EXPEDIENTE_PADRAO.inicio;
  const fim = typeof r.fim === "string" && HORA.test(r.fim) ? r.fim : EXPEDIENTE_PADRAO.fim;
  const fuso = typeof r.fuso === "string" && fusoValido(r.fuso) ? r.fuso : EXPEDIENTE_PADRAO.fuso;
  return { dias, inicio, fim, fuso };
}

type PartesLocais = {
  ano: number;
  mes: number;
  dia: number;
  hora: number;
  minuto: number;
  segundo: number;
  diaDaSemana: number;
};

const formatadores = new Map<string, Intl.DateTimeFormat>();

function formatador(fuso: string) {
  let f = formatadores.get(fuso);
  if (!f) {
    f = new Intl.DateTimeFormat("en-US", {
      timeZone: fuso,
      hourCycle: "h23",
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
      weekday: "short",
    });
    formatadores.set(fuso, f);
  }
  return f;
}

const DIA_DA_SEMANA: Record<string, number> = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };

function partesLocais(ms: number, fuso: string): PartesLocais {
  const partes = formatador(fuso).formatToParts(new Date(ms));
  const valor = (tipo: string) => partes.find((p) => p.type === tipo)?.value ?? "0";
  return {
    ano: Number(valor("year")),
    mes: Number(valor("month")),
    dia: Number(valor("day")),
    hora: Number(valor("hour")) % 24,
    minuto: Number(valor("minute")),
    segundo: Number(valor("second")),
    diaDaSemana: DIA_DA_SEMANA[valor("weekday")] ?? 0,
  };
}

/** A data de um instante no fuso da empresa: ano, mês (1–12), dia e dia da semana (0 = domingo). */
export function dataNoFuso(ms: number, fuso: string): { ano: number; mes: number; dia: number; diaDaSemana: number } {
  const p = partesLocais(ms, fuso);
  return { ano: p.ano, mes: p.mes, dia: p.dia, diaDaSemana: p.diaDaSemana };
}

/* O instante UTC de uma hora local no fuso. Chuta como se fosse UTC, mede o
   deslocamento naquele instante e corrige. */
function msDoLocal(ano: number, mes: number, dia: number, hora: number, minuto: number, fuso: string) {
  const chute = Date.UTC(ano, mes - 1, dia, hora, minuto, 0);
  const p = partesLocais(chute, fuso);
  const comoUtc = Date.UTC(p.ano, p.mes - 1, p.dia, p.hora, p.minuto, p.segundo);
  return chute - (comoUtc - chute);
}

/**
 * Quantos segundos do intervalo caem dentro do expediente.
 *
 * Percorre dia a dia no fuso da empresa: para cada dia útil, a interseção do
 * intervalo com a janela de trabalho daquele dia.
 */
export function segundosUteis(inicioMs: number, fimMs: number, expediente: Expediente): number {
  if (!Number.isFinite(inicioMs) || !Number.isFinite(fimMs) || fimMs <= inicioMs) return 0;
  const [hi, mi] = expediente.inicio.split(":").map(Number);
  const [hf, mf] = expediente.fim.split(":").map(Number);
  if (hf * 60 + mf <= hi * 60 + mi || expediente.dias.length === 0) return 0;

  let total = 0;
  let cursor = inicioMs;
  let guarda = 0;
  while (cursor < fimMs && guarda++ < 20000) {
    const p = partesLocais(cursor, expediente.fuso);
    const proximoDia = msDoLocal(p.ano, p.mes, p.dia + 1, 0, 0, expediente.fuso);
    if (expediente.dias.includes(p.diaDaSemana)) {
      const abre = msDoLocal(p.ano, p.mes, p.dia, hi, mi, expediente.fuso);
      const fecha = msDoLocal(p.ano, p.mes, p.dia, hf, mf, expediente.fuso);
      const de = Math.max(inicioMs, abre);
      const ate = Math.min(fimMs, fecha);
      if (ate > de) total += ate - de;
    }
    if (proximoDia <= cursor) break;
    cursor = proximoDia;
  }
  return Math.floor(total / 1000);
}

export type SessaoParaSoma = { inicio: string | number; fim: string | number | null };

/** Sessões abertas contam até `agoraMs`. */
export function segundosUteisDasSessoes(sessoes: SessaoParaSoma[], expediente: Expediente, agoraMs: number): number {
  return sessoes.reduce((acc, s) => {
    const inicio = typeof s.inicio === "number" ? s.inicio : Date.parse(s.inicio);
    const fim = s.fim == null ? agoraMs : typeof s.fim === "number" ? s.fim : Date.parse(s.fim);
    return acc + segundosUteis(inicio, fim, expediente);
  }, 0);
}
