# app/jobs/sincronizar_series_aparelho_job.rb
#
# Busca na API do SimpliRoute o "Número de Série da Base (Aparelho)" das visitas
# e grava em series_aparelho, para a planilha Entregas do cliente sair com essa
# coluna preenchida.
#
# POR QUE EM BACKGROUND: o dado só existe na API (o espelho tem a coluna e nunca
# a preenche — 0 de ~6.000 linhas em fev/mar/mai/jul/2026). A API não filtra por
# intervalo: é uma chamada POR DATA, cada uma com o dia inteiro (~4 MB / ~9 s).
# Um mês são ~25 chamadas — inviável dentro do request do download, tranquilo
# num job noturno.
#
# Roda via Whenever → `rake simpli_route:series` (últimos DIAS_PADRAO dias).
# Para meses antigos, o backfill é manual: `rake simpli_route:series_operacao[tabela]`.
class SincronizarSeriesAparelhoJob < ApplicationJob
  queue_as :default

  # Mesmo limite da varredura do Admin::EdicaoLancamentosController — a API do
  # SimpliRoute é lenta por dia, mas aguenta alguns dias em paralelo.
  MAX_PARALELO = 6

  # Janela padrão da rodada agendada. Cobre o mês corrente inteiro com folga,
  # que é o que o cliente pede em planilha no dia a dia.
  DIAS_PADRAO = 45

  # Nomes conhecidos do campo em `extra_field_values`. O 1º que vier preenchido
  # ganha; se nenhum casar, cai no fallback por /seri/ (ver #serie_de) — foi
  # chutar nome de campo que deixou meia planilha vazia antes.
  CAMPOS_SERIE = %w[
    numero_de_serie_da_base_aparelho numero_de_serie_da_base numero_serie_base
    num_serie_base serie_base numero_de_serie serie
  ].freeze

  # Devolve um resumo { datas:, visitas:, series:, falhas: } para o rake logar.
  def perform(de: nil, ate: nil, operacao: nil, dias: DIAS_PADRAO)
    return resumo_vazio('SIMPLIROUTE_TOKEN não configurado') unless SimpliRoute.configurado?

    datas = datas_alvo(de: de, ate: ate, operacao: operacao, dias: dias)
    return resumo_vazio('nenhuma data com visitas no período') if datas.empty?

    visitas = varrer(datas)
    gravadas = gravar(visitas)

    avisar_se_nada_casou(gravadas, visitas)
    { datas: datas.size, visitas: visitas.size, series: gravadas, falhas: @falhas }
  end

  private

  # Datas que valem uma chamada à API. Sempre saem do espelho (planned_date das
  # visitas que já conhecemos) — varrer dia sem visita é chamada jogada fora.
  def datas_alvo(de:, ate:, operacao:, dias:)
    escopo = Entrega.where.not(planned_date: nil)
    escopo = escopo.da_operacoes([operacao]) if operacao.present?

    inicio, fim = if de.present? && ate.present?
                    [de.to_date, ate.to_date]
                  elsif operacao.present?
                    [nil, nil]   # operação inteira, sem recorte de data
                  else
                    [Date.current - dias.to_i, Date.current]
                  end
    escopo = escopo.no_periodo(inicio, fim) if inicio && fim

    escopo.distinct.pluck(Arel.sql('planned_date::date')).compact.sort
  end

  # Um dia que a API não responde não pode derrubar a rodada: registra e segue
  # (mesma postura da varredura de NF do Admin::EdicaoLancamentosController).
  def varrer(datas)
    @falhas = []
    mutex   = Mutex.new

    datas.each_slice(MAX_PARALELO).flat_map do |lote|
      lote.map { |data| Thread.new { visitas_do_dia(data, mutex) } }.flat_map(&:value)
    end
  end

  def visitas_do_dia(data, mutex)
    client.visitas_da_data(data)
  rescue SimpliRoute::Error => e
    mutex.synchronize { @falhas << { data: data.to_s, erro: e.message } }
    []
  end

  # Grava só as visitas que TÊM série. upsert_all em vez de N inserts: uma
  # operação de um mês passa de 1.000 visitas.
  def gravar(visitas)
    agora = Time.current
    registros = visitas.filter_map do |visita|
      serie = serie_de(visita)
      next if serie.blank? || visita['tracking_id'].blank?

      {
        tracking_id:    visita['tracking_id'],
        reference_id:   visita['reference'].to_s.presence,
        numero_serie:   serie,
        planned_date:   visita['planned_date'],
        sincronizado_em: agora
      }
    end
    # A mesma visita pode vir em duas datas varridas; upsert_all rejeita chave
    # repetida no MESMO lote, então deduplica antes (fica a última).
    registros = registros.index_by { |r| r[:tracking_id] }.values
    return 0 if registros.empty?

    SerieAparelho.upsert_all(registros, unique_by: :tracking_id, record_timestamps: true)
    registros.size
  end

  # A série fica em `extra_field_values`, o mesmo hash das fotos do motorista.
  # Ignora valor que seja URL: ali dentro convivem os campos foto_*.
  def serie_de(visita)
    extras = visita['extra_field_values']
    return nil unless extras.is_a?(Hash)

    chave = CAMPOS_SERIE.find { |c| texto_util?(extras[c]) } ||
            extras.keys.find { |k| k.to_s.match?(/seri/i) && texto_util?(extras[k]) }
    chave && extras[chave].to_s.strip
  end

  def texto_util?(valor)
    v = valor.to_s.strip
    v.present? && !v.match?(%r{\Ahttps?://}i)
  end

  # Varreu visitas e não achou UMA série? Quase certo que o campo mudou de nome
  # na API. Loga as chaves que vieram para não virar falha silenciosa de novo.
  def avisar_se_nada_casou(gravadas, visitas)
    return if gravadas.positive? || visitas.empty?

    chaves = visitas.filter_map { |v| v['extra_field_values'] }
                    .select { |e| e.is_a?(Hash) }.flat_map(&:keys).uniq.sort
    Rails.logger.warn(
      "[series_aparelho] #{visitas.size} visitas varridas e nenhuma série encontrada. " \
      "Campos vistos em extra_field_values: #{chaves.inspect}. " \
      'Se o campo da série estiver nessa lista, acrescente o nome em CAMPOS_SERIE.'
    )
  end

  def client
    @client ||= SimpliRoute::Client.new
  end

  def resumo_vazio(motivo)
    Rails.logger.info("[series_aparelho] nada a fazer — #{motivo}.")
    { datas: 0, visitas: 0, series: 0, falhas: [], motivo: motivo }
  end
end
