# app/services/romaneios/plano_do_dia.rb
#
# Busca o plano de UMA data e devolve as paradas na forma canônica do
# Romaneios::Normalizador — a mesma que Romaneios::PlanoPlanilha entrega.
#
# ⚠️ NÃO está verificado se a visita de /v1/routes/visits/ carrega veículo/ordem, e
# isso não é testável na máquina de desenvolvimento (sem token, sem .env). Por isso
# aqui existe uma ESCADA DE QUEDA explícita, e não um caminho feliz único:
#
#   a) rotas da API trazem vehicle + lista ordenada de visitas  -> origem :api
#   b) as visitas trazem alguma chave de veículo/ordem          -> origem :api
#   c) nada disso (ou sem token)                                -> ESPELHO local
#   d) espelho vazio na data                                    -> NotFound, com a
#      mensagem que manda o operador usar o .xlsx do plano
#
# A rung (c) não é consolo: o romaneio é impresso ANTES do dia começar, e ela cobre
# "plano publicado e já sincronizado, mas a API fora do ar" — a falha mais provável.
# Rode bin/sondar_plano_do_dia no servidor para fechar (a)/(b).
module Romaneios
  class PlanoDoDia
    # Chaves candidatas nos hashes da API. Só as usamos se vierem PREENCHIDAS —
    # a API devolve chave existente com valor nil o tempo todo.
    #
    # ⚠️ A ORDEM AQUI É O CONSERTO DE UM BUG REAL, não estilo. Na API o campo
    # `vehicle` da rota é o **ID numérico** do veículo (630011), e o nome que a
    # operação usa — o mesmo que a planilha do plano traz na coluna "Veículo" —
    # é GADE_038. Com `vehicle` na frente, o ID ganhava sempre e o romaneio saía
    # impresso "CONTROLE DE ENTREGA — 630011", com o motorista sem saber que
    # carro pegar. Nome primeiro; ID só como último recurso, porque veículo
    # vazio faz a linha ser descartada e o romaneio sair sem paradas.
    CHAVES_VEICULO   = %w[vehicle_name vehicle_label vehicle vehicle_id].freeze
    # Dentro do objeto do veículo (quando a API aninha em vez de mandar o id).
    CHAVES_NOME_VEIC = %w[name label display_name plate].freeze
    CHAVES_MOTORISTA = %w[driver driver_name].freeze
    CHAVES_ORDEM     = %w[order sequence position route_order].freeze

    attr_reader :origem

    # Duas formas de pedir o plano, e a primeira é a boa:
    #
    #   PlanoDoDia.new(plano: {...})  -> o PLANO escolhido na lista (o hash que
    #                                    SimpliRoute::Client#planos devolve)
    #   PlanoDoDia.new(data: ...)     -> o jeito antigo, por data
    #
    # POR QUE O PLANO VENCE: o operador pensa por operação ("lançou a UBS
    # Sudeste"), não por data — e a data era mesmo impossível de acertar, porque
    # o plano tem uma JANELA e o dia das rotas ora é o começo dela, ora o fim
    # (ver SimpliRoute::Client#rota). Com o plano em mãos a data deixa de ser
    # chute: pergunta-se à API.
    #
    # A forma por data continua porque o "Reimportar" de um romaneio antigo só
    # tem a data, e porque ela é o caminho para o espelho local quando a API cai.
    def initialize(data: nil, plano: nil, client: nil)
      @plano  = plano
      @data   = data&.to_date
      @client = client
      @origem = nil
    end

    def linhas
      @linhas ||= carregar
    end

    # O dia REAL do plano — o que o Romaneio grava em `planned_date`. Uma
    # chamada: todas as rotas de um plano dividem o mesmo dia.
    def data_resolvida
      @data ||= begin
        rota = cliente.rota(Array(@plano && @plano['routes']).first)
        rota && rota['planned_date'] ? Date.parse(rota['planned_date']) : nil
      rescue Date::Error, SimpliRoute::Error
        nil
      end
    end

    private

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

    def carregar
      linhas = @plano ? da_api_por_plano : da_api
      return linhas if linhas.any?

      # O espelho é por DATA. Vindo de um plano, ele só entra depois que a data
      # foi resolvida — sem ela não há o que consultar, e chamar `do_espelho`
      # com @data nil traria o período inteiro.
      linhas = do_espelho if @data
      return linhas if linhas&.any?

      raise SimpliRoute::NotFound, mensagem_de_ausencia
    end

    # A mensagem diz o que a pessoa escolheu, não um dado interno: quem clicou
    # num plano na lista não reconhece a data resolvida, e quem digitou a data
    # não sabe de plano nenhum.
    def mensagem_de_ausencia
      if @plano && @data.nil?
        "Não consegui descobrir o dia do plano \"#{@plano['name']}\" no SimpliRoute. " \
          'Escolha outro plano ou envie a planilha do plano (.xlsx).'
      elsif @plano
        "O plano \"#{@plano['name']}\" (#{@data.strftime('%d/%m/%Y')}) não tem paradas no " \
          'SimpliRoute nem no espelho de rastreio. Envie a planilha do plano (.xlsx).'
      else
        "O plano de #{@data.strftime('%d/%m/%Y')} não está disponível no SimpliRoute " \
          'nem no espelho de rastreio. Envie a planilha do plano (.xlsx).'
      end
    end

    # ── Caminho do PLANO ESCOLHIDO (o normal desde 28/08/2026) ───────────────
    # Três chamadas, todas verificadas contra a API real:
    #
    #   1. a 1ª rota do plano  -> o DIA real (Client#rota)
    #   2. visitas do dia      -> 2047 visitas, 3 MB, ~9 s (Client#visitas_da_data)
    #   3. veículos da conta   -> traduz 634185 em GADE_057 (dentro de por_visitas)
    #
    # O FILTRO PELAS ROTAS DO PLANO É O PONTO DA COISA, não uma otimização: dois
    # planos podem cair no MESMO dia (uma operação normal e uma de avulsas, por
    # exemplo), e sem ele o romaneio de um sairia com as paradas do outro. A
    # visita traz `route`, e o plano traz a lista de uuids de rota — o cruzamento
    # é exato. Medido: 2047 visitas do dia, 73 rotas, todas com `route`,
    # `vehicle` e `order` preenchidos (2047/2047).
    #
    # Depois do filtro é o mesmo `por_visitas` de sempre: ele já agrupa por
    # veículo, resolve o nome (GADE_057) e ordena por `order`.
    def da_api_por_plano
      return [] unless SimpliRoute.configurado?

      data = data_resolvida
      return [] if data.nil?

      do_plano = Array(@plano['routes']).map(&:to_s).to_set
      visitas  = cliente.visitas_da_data(data).select { |v| do_plano.include?(v['route'].to_s) }
      return [] if visitas.empty?

      linhas = por_visitas(visitas, cliente)
      @origem = 'api' if linhas.any?
      linhas
    rescue SimpliRoute::Error
      []
    end

    # ── (a) e (b) ────────────────────────────────────────────────────────────
    def da_api
      return [] unless SimpliRoute.configurado?

      cliente = self.cliente
      visitas = cliente.visitas_da_data(@data)
      return [] if visitas.empty?

      linhas = por_rotas(cliente, visitas)
      linhas = por_visitas(visitas, cliente) if linhas.empty?
      @origem = 'api' if linhas.any?
      linhas
    rescue SimpliRoute::Error
      # Token ausente/expirado ou API fora: cai para o espelho, que é a mesma
      # informação já sincronizada.
      []
    end

    # (a) A rota traz o veículo e a lista de visitas na ordem do roteiro.
    def por_rotas(cliente, visitas)
      rotas = cliente.rotas_da_data(@data)
      return [] if rotas.empty?

      por_id = visitas.index_by { |v| v['id'] }
      linhas = []

      rotas.each do |rota|
        veiculo = nome_do_veiculo(rota, cliente)
        next if veiculo.empty?

        ids = Array(rota['visits']).map { |v| v.is_a?(Hash) ? v['id'] : v }
        ids = visitas.select { |v| v['route'].to_s == rota['id'].to_s }.map { |v| v['id'] } if ids.empty?

        ids.each_with_index do |id, i|
          visita = por_id[id]
          next if visita.nil?

          linhas << monta(visita, veiculo, primeiro_valor(rota, CHAVES_MOTORISTA), i + 1)
        end
      end

      linhas
    end

    # (b) A visita traz o veículo direto; a ordem sai de `order`/`sequence` ou,
    # na falta, do horário estimado de chegada.
    def por_visitas(visitas, cliente)
      com_veiculo = visitas.reject { |v| nome_do_veiculo(v, cliente).empty? }
      return [] if com_veiculo.empty?

      com_veiculo.group_by { |v| nome_do_veiculo(v, cliente) }.flat_map do |veiculo, lista|
        ordenadas(lista).each_with_index.map do |visita, i|
          monta(visita, veiculo, primeiro_valor(visita, CHAVES_MOTORISTA), i + 1)
        end
      end
    end

    def ordenadas(lista)
      lista.sort_by.with_index do |v, i|
        chave = CHAVES_ORDEM.filter_map { |k| v[k] }.first
        [chave.nil? ? Float::INFINITY : chave.to_i, v['estimated_time_arrival'].to_s, i]
      end
    end

    def monta(visita, veiculo, motorista, ordem)
      Normalizador.linha(
        veiculo: veiculo, ordem: ordem, motorista: motorista,
        titulo: visita['title'], endereco: visita['address'],
        anotacoes: visita['notes'], nota_fiscal: visita['reference'],
        tracking_id: visita['tracking_id']
      )
    end

    # O NOME do veículo como a operação o conhece (GADE_038), nunca o id.
    #
    # Três formas já vistas/possíveis no mesmo campo, por isso as três camadas:
    #   { "vehicle_name" => "GADE_038" }        -> nome direto
    #   { "vehicle" => { "name" => "GADE_038" } } -> aninhado
    #   { "vehicle" => 630011 }                  -> só o id, precisa resolver
    #
    # Quando sobra só o id, procuramos o nome na lista de veículos da conta. Se
    # essa busca não responder, devolvemos o id mesmo: um romaneio com o número
    # do carro é ruim, um romaneio VAZIO (que é o que acontece devolvendo "") é
    # pior — a linha sem veículo é descartada.
    def nome_do_veiculo(hash, cliente)
      aninhado = hash['vehicle']
      return primeiro_valor(aninhado, CHAVES_NOME_VEIC) if aninhado.is_a?(Hash)

      valor = primeiro_valor(hash, CHAVES_VEICULO)
      return valor unless valor.match?(/\A\d+\z/)

      nomes_de_veiculo(cliente)[valor].presence || valor
    end

    # id (string) => nome. Uma chamada por importação, memoizada: são ~70 veículos
    # e a alternativa seria uma consulta por rota.
    def nomes_de_veiculo(cliente)
      @nomes_de_veiculo ||= cliente.veiculos.each_with_object({}) do |v, memo|
        next unless v.is_a?(Hash)

        id   = v['id'].to_s.strip
        nome = primeiro_valor(v, CHAVES_NOME_VEIC)
        memo[id] = nome unless id.empty? || nome.empty?
      end
    end

    def primeiro_valor(hash, chaves)
      chaves.filter_map { |k| hash[k] }.map(&:to_s).find { |v| !v.strip.empty? }.to_s.strip
    end

    # ── (c) Espelho local ────────────────────────────────────────────────────
    # `db_reem_simplerout_2026` já tem driver, vehicle, title, address, notes e eta.
    # Só leitura, como sempre.
    def do_espelho
      registros = Entrega.da_conta_gade
                         .no_periodo(@data, @data)
                         .where.not(vehicle: [nil, ''])
                         .order(:vehicle, :eta)
                         .to_a
      return [] if registros.empty?

      @origem = 'espelho'
      registros.group_by(&:vehicle).flat_map do |veiculo, lista|
        lista.each_with_index.map do |e, i|
          Normalizador.linha(
            veiculo: veiculo, ordem: i + 1, motorista: e.driver,
            titulo: e.title, endereco: e.address, anotacoes: e.notes,
            nota_fiscal: e.reference_id, tracking_id: e.tracking_id
          )
        end
      end
    end
  end
end
