/**
 * Horário de funcionamento do agente — a regra que a tela oferecia e ninguém lia.
 *
 * ## O defeito que este arquivo existe para consertar
 *
 * `TriggerEditor.tsx` mostra "Só atender em horário de funcionamento" com fuso,
 * início, fim e dias da semana, e grava tudo em
 * `ai_agent_versions.trigger_config.filters.business_hours`. O ÚNICO leitor
 * desse campo era `lib/ai/dispatcher/triggers.ts` — o dispatcher legado, que
 * hoje é NO-OP permanente (`app/api/v1/cron/agent-dispatcher/route.ts`). O
 * runtime vivo (`lib/agent-engine`) nunca soube que o campo existia.
 *
 * Resultado medido numa instalação real (2026-08-18): a versão publicada dizia
 * 08:00–18:00, seg–sex, e o agente respondia 21:55 de uma terça — porque nada
 * consultava a janela. Um controle que a tela oferece e o código ignora mente
 * para quem configurou; é o mesmo defeito que o `HANDOFF-followup-vivo.md`
 * cataloga como "o tempo adaptativo é decorativo".
 *
 * ## Por que ADIAR, e não descartar
 *
 * O dispatcher morto tratava "fora da janela" como `no_match`: a mensagem
 * simplesmente não era respondida, nunca. Aqui o turno é REAGENDADO para a
 * abertura da janela — quem escreveu às 22h é atendido às 8h, e não no
 * esquecimento. Silêncio permanente é o modo de falha que este repo já pagou
 * caro; a janela pode atrasar a resposta, não pode sumir com ela.
 *
 * ## Falha ABERTA, sempre
 *
 * `trigger_config` é jsonb livre: fuso inválido, horário torto, dias vazios,
 * shape estranho — tudo devolve `null` (sem janela), e sem janela o agente
 * responde. A direção segura aqui é o contrário da doutrina de tools: uma
 * config quebrada não pode virar mordaça.
 */

export interface JanelaDeAtendimento {
  /** IANA (`America/Sao_Paulo`). Inválido ⇒ a janela inteira é descartada. */
  timezone: string;
  /** `HH:MM` local. */
  start: string;
  /** `HH:MM` local, sempre MAIOR que `start` (ver `lerJanelaDeAtendimento`). */
  end: string;
  /** 0=domingo … 6=sábado, sem repetição, ao menos um. */
  weekdays: number[];
}

const HORA_RX = /^([01][0-9]|2[0-3]):([0-5][0-9])$/;

function minutosDe(hhmm: string): number | null {
  const m = HORA_RX.exec(hhmm);
  if (m === null) return null;
  return Number(m[1]) * 60 + Number(m[2]);
}

/**
 * Extrai a janela de `trigger_config` (jsonb livre da versão publicada).
 * `null` = sem janela declarada OU declarada de forma que não dá para obedecer.
 */
export function lerJanelaDeAtendimento(triggerConfig: unknown): JanelaDeAtendimento | null {
  if (typeof triggerConfig !== 'object' || triggerConfig === null) return null;
  const filters = (triggerConfig as { filters?: unknown }).filters;
  if (typeof filters !== 'object' || filters === null) return null;
  const bh = (filters as { business_hours?: unknown }).business_hours;
  if (typeof bh !== 'object' || bh === null) return null;

  const { timezone, start, end, weekdays } = bh as {
    timezone?: unknown;
    start?: unknown;
    end?: unknown;
    weekdays?: unknown;
  };
  if (typeof timezone !== 'string' || timezone.trim() === '') return null;
  if (typeof start !== 'string' || typeof end !== 'string') return null;

  const inicio = minutosDe(start);
  const fim = minutosDe(end);
  if (inicio === null || fim === null) return null;
  // Janela que vira a meia-noite (22:00–02:00) não é suportada, e recusá-la é
  // melhor que interpretá-la ao contrário: `fim <= inicio` viraria "fechado
  // sempre", que é justamente a mordaça que este arquivo não pode criar.
  if (fim <= inicio) return null;

  if (!Array.isArray(weekdays) || weekdays.length === 0) return null;
  const dias = [
    ...new Set(
      weekdays.filter((d): d is number => Number.isInteger(d) && (d as number) >= 0 && (d as number) <= 6),
    ),
  ];
  if (dias.length === 0) return null;

  // Fuso que o ambiente não conhece ⇒ sem janela (falha aberta).
  try {
    relogioLocal(timezone, new Date());
  } catch {
    return null;
  }

  return { timezone, start, end, weekdays: dias };
}

/** Dia da semana (0–6) e minutos desde a meia-noite NO FUSO da janela. */
function relogioLocal(timezone: string, agora: Date): { dia: number; minutos: number } {
  const partes = new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    hour12: false,
    weekday: 'short',
    hour: '2-digit',
    minute: '2-digit',
  }).formatToParts(agora);

  const valor = (tipo: Intl.DateTimeFormatPartTypes): string =>
    partes.find((p) => p.type === tipo)?.value ?? '';

  const DIAS: Record<string, number> = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
  const dia = DIAS[valor('weekday')] ?? 0;
  // 24 é a meia-noite em algumas ICUs com hour12:false — normaliza para 0.
  const hora = Number(valor('hour')) % 24;
  return { dia, minutos: hora * 60 + Number(valor('minute')) };
}

/**
 * `null` = a janela está ABERTA agora (o turno segue). Caso contrário, quantos
 * milissegundos faltam para a próxima abertura — sempre > 0.
 *
 * ponytail: a conta é feita em minutos de relógio de parede, então uma virada de
 * horário de verão dentro do intervalo desloca o alvo em até 1h (o job acorda,
 * reavalia e reagenda — a janela nunca é pulada, só reconferida). Brasil não tem
 * DST hoje; se algum fuso de cliente tiver e a hora importar, o upgrade é somar
 * o offset do dia-alvo, não uma biblioteca inteira.
 */
export function msAteAJanelaAbrir(janela: JanelaDeAtendimento, agora: Date): number | null {
  let local: { dia: number; minutos: number };
  try {
    local = relogioLocal(janela.timezone, agora);
  } catch {
    return null; // falha aberta
  }

  const inicio = minutosDe(janela.start);
  const fim = minutosDe(janela.end);
  if (inicio === null || fim === null) return null;

  const aberta =
    janela.weekdays.includes(local.dia) && local.minutos >= inicio && local.minutos < fim;
  if (aberta) return null;

  for (let offset = 0; offset <= 7; offset += 1) {
    const dia = (local.dia + offset) % 7;
    if (!janela.weekdays.includes(dia)) continue;
    if (offset === 0 && local.minutos >= inicio) continue; // hoje a janela já passou
    const minutosAteAbrir = offset * 24 * 60 + inicio - local.minutos;
    if (minutosAteAbrir > 0) return minutosAteAbrir * 60_000;
  }

  return null; // inalcançável com weekdays não-vazio; falha aberta por precaução
}

/**
 * Início do PERÍODO FECHADO vigente — o instante em que a janela passou de
 * aberta para fechada, o mais recente que já aconteceu (#1926).
 *
 * É a régua de "uma vez por contato por período fechado": o aviso de fora do
 * horário só vale de novo quando a janela ABRIU e FECHOU outra vez. Sem esta
 * função o critério viraria "desde a última mensagem", que não é período nenhum
 * — quem escreve às 22h01 receberia outro aviso às 22h40.
 *
 * A conta é em minutos de relógio de parede, como `msAteAJanelaAbrir`: o Brasil
 * não tem DST hoje e o job reavalia a cada turno, então uma virada de horário
 * desloca o alvo em no máximo 1h em vez de pular o período.
 *
 * `null` = não deu para determinar (fuso/torto/sem candidato em 8 dias) — quem
 * chama NÃO envia aviso, porque sem régua não dá para prometer "só uma vez".
 */
export function inicioDoPeriodoFechado(janela: JanelaDeAtendimento, agora: Date): Date | null {
  let local: { dia: number; minutos: number };
  try {
    local = relogioLocal(janela.timezone, agora);
  } catch {
    return null; // falha aberta: sem relógio local não há período
  }

  const fim = minutosDe(janela.end);
  if (fim === null) return null;

  // Volta até 8 dias (um ciclo inteiro + 1) procurando o último dia da janela
  // cujo FECHAMENTO (end) já passou. `minutosDesdeOFechamento < 0` no primeiro
  // candidato = a janela fecha HOJE mais tarde (ou ainda não abriu hoje).
  for (let voltas = 0; voltas <= 7; voltas += 1) {
    const dia = (local.dia - voltas + 14) % 7;
    if (!janela.weekdays.includes(dia)) continue;
    const minutosDesdeOFechamento = local.minutos - fim + voltas * 24 * 60;
    if (minutosDesdeOFechamento < 0) continue;
    // Base no MINUTO cheio: `relogioLocal` conta minutos inteiros, e somar isso
    // a um `agora` com segundos fazia o início herdar os segundos — a chave do
    // aviso mudava a cada mensagem e o "uma vez por período" nunca casava.
    const agoraNoMinuto = Math.floor(agora.getTime() / 60_000) * 60_000;
    return new Date(agoraNoMinuto - minutosDesdeOFechamento * 60_000);
  }

  return null;
}

/**
 * "Não atender em feriado nacional" (#3877, doc de decisão 316, alternativa A):
 * `trigger_config.filters.business_hours.fechado_em_feriado_nacional`.
 *
 * DESLIGADA de fábrica: só `=== true` liga. Chave ausente, de outro tipo ou sem
 * `business_hours` ⇒ `false`, e o feriado é um dia comum da janela semanal —
 * exatamente o comportamento de antes da opção existir. Muita operação atende
 * em feriado (delivery, plantão, loja de shopping); calar o agente delas por
 * padrão seria a mordaça que este arquivo não pode criar.
 */
export function lerFechadoEmFeriadoNacional(triggerConfig: unknown): boolean {
  if (typeof triggerConfig !== 'object' || triggerConfig === null) return false;
  const filters = (triggerConfig as { filters?: unknown }).filters;
  if (typeof filters !== 'object' || filters === null) return false;
  const bh = (filters as { business_hours?: unknown }).business_hours;
  if (typeof bh !== 'object' || bh === null) return false;
  return (bh as { fechado_em_feriado_nacional?: unknown }).fechado_em_feriado_nacional === true;
}

/**
 * Como `msAteAJanelaAbrir`, mas uma abertura que cai em FERIADO não conta: o
 * turno espera a abertura do próximo dia da janela que não seja feriado.
 *
 * `ehFeriado` recebe um INSTANTE e responde pelo dia civil dele — quem chama
 * escolhe a régua (no turno: `ehFeriadoNacionalNoFuso` no fuso da janela). A
 * função continua pura e testável com relógio fixo.
 *
 * Dentro da janela num feriado ⇒ espera até a próxima abertura que não é
 * feriado (nunca `null`). Fora de feriado ⇒ idêntica a `msAteAJanelaAbrir`.
 *
 * ponytail: salta de abertura em abertura, no máximo 31 vezes (feriados
 * nacionais seguidos são no máximo dois — segunda e terça de Carnaval). Sem
 * achar dia útil em 31 aberturas, falha ABERTA para a janela semanal pura.
 */
export function msAteAJanelaAbrirPulandoFeriado(
  janela: JanelaDeAtendimento,
  agora: Date,
  ehFeriado: (instante: Date) => boolean,
): number | null {
  const semFeriado = msAteAJanelaAbrir(janela, agora);
  const fim = minutosDe(janela.end);
  if (fim === null) return semFeriado;

  let referencia = agora;
  for (let tentativas = 0; tentativas < 31; tentativas += 1) {
    const espera = msAteAJanelaAbrir(janela, referencia);
    const abertura = espera === null ? referencia : new Date(referencia.getTime() + espera);
    if (!ehFeriado(abertura)) {
      const ms = abertura.getTime() - agora.getTime();
      return ms > 0 ? ms : null;
    }
    // A abertura é num feriado: o próximo candidato vem depois do FECHAMENTO
    // da janela daquele dia (o `end` no relógio local da janela).
    let local: { dia: number; minutos: number };
    try {
      local = relogioLocal(janela.timezone, abertura);
    } catch {
      return semFeriado; // falha aberta
    }
    referencia = new Date(abertura.getTime() + (fim - local.minutos) * 60_000);
  }
  return semFeriado;
}
