# app/services/simpli_route/client.rb
#
# Cliente HTTP da API do SimpliRoute (base de ESCRITA). A tabela de rastreio
# local (model Entrega) é só leitura e sincroniza a partir daqui; para corrigir
# um lançamento o ADM grava direto por este cliente.
#
# Usa Net::HTTP (stdlib) — o projeto não tem Faraday/HTTParty.
#
require 'net/http'
require 'json'

module SimpliRoute
  class Error < StandardError; end

  # Erro esperado/apresentável ao usuário (NF não achada, ambiguidade, etc.).
  class NotFound < Error; end

  class Client
    # Motivos de insucesso são fixos (todos type=failed). Cache curto porque
    # muda raríssimo, mas evita bater na API a cada abertura de tela.
    OBSERVATIONS_CACHE_KEY = 'simpliroute/observations'.freeze
    OBSERVATIONS_TTL       = 1.hour

    def initialize(token: SimpliRoute.token, base_url: SimpliRoute.base_url)
      raise Error, 'SIMPLIROUTE_TOKEN não configurado no ambiente.' if token.blank?

      @token    = token
      @base_uri = URI.parse(base_url)
    end

    # Lista de motivos [{ 'id' => uuid, 'type' => 'failed', 'label' => 'ÓBITO' }, ...]
    def observations
      Rails.cache.fetch(OBSERVATIONS_CACHE_KEY, expires_in: OBSERVATIONS_TTL) do
        Array(get('/v1/routes/observations/'))
      end
    end

    # Visita única (hash) pelo id numérico.
    def visita(id)
      get("/v1/routes/visits/#{id}/")
    end

    # Todas as visitas de uma data (Array de hashes).
    #
    # `busca` vira `&search=` — parâmetro NÃO documentado, mas verificado contra
    # a API real em 21/07/2026: filtra de verdade e derruba a resposta de um dia
    # de ~3,8 MB / ~9 s (1879 visitas) para ~1 KB / ~0,6 s. Sem ele, varrer um
    # mês é inviável.
    #
    # ⚠️ NÃO confiar só nele: `reference`, `reference_id`, `q` e `title` são
    # ignorados em SILÊNCIO pela API (devolvem 200 com o dia inteiro). Se o
    # `search` mudar de nome, cai no mesmo comportamento — por isso quem chama
    # continua filtrando por `reference`, e a perda é só de desempenho.
    #
    # ⚠️ SEM `planned_date` a API responde um conjunto padrão (~2469 visitas,
    # medido) que NÃO cobre o histórico: a NF 82891 voltava só com a visita de
    # 21/07, escondendo a de 17/07. Todo filtro de intervalo testado
    # (planned_date_from/to, since/until, __gte/__lte, date_from/to) é ignorado.
    # Por isso a data continua obrigatória e a varredura é dia a dia.
    def visitas_da_data(data, busca: nil)
      caminho = "/v1/routes/visits/?planned_date=#{data.to_date.iso8601}"
      caminho += "&search=#{URI.encode_www_form_component(busca.to_s)}" if busca.present?

      Array(get(caminho))
    end

    # UM plano pelo uuid. Existe separado de `planos` porque a busca do plano
    # ESCOLHIDO não pode depender de recorte nenhum: o operador pode ter filtrado
    # por período e escolhido um plano fora dos mais recentes, e procurá-lo numa
    # lista cortada devolveria "plano não encontrado" para um plano que existe.
    def plano(id)
      Array(get('/v1/routes/plans/')).find { |pl| pl['id'].to_s == id.to_s }
    rescue Error
      nil
    end

    # UMA rota pelo uuid. Serve para descobrir o DIA REAL de um plano: o registro
    # do plano só traz a JANELA (`start_date`/`end_date`), e o dia das rotas nem
    # sempre é o começo dela — medido em 28/08/2026: EMAD SETEMBRO 2026 (31/08 a
    # 08/09) tem rotas em 31/08 (= início), mas UBS OESTE AGOSTO 2026 (18/08 a
    # 20/08) e UBS NORTE AGOSTO 2026 (17/08 a 20/08) têm rotas em 20/08 (= FIM).
    # Era isto que tornava a data impossível de adivinhar de fora; agora se
    # pergunta à API. Todas as rotas de um plano dividem o mesmo dia (amostra de
    # 8 em 3 planos), então basta a primeira.
    def rota(id)
      get("/v1/routes/routes/#{id}/")
    rescue Error
      nil
    end

    # Rotas PLANEJADAS da data (Array de hashes). É aqui que mora o par
    # veículo/motorista, e o romaneio precisa do veículo para saber quantas folhas
    # imprimir.
    #
    # ✔ VERIFICADO em 28/08/2026 contra a API real: `?planned_date=2026-08-31`
    # devolve 73 rotas (83 KB, 0,59 s), cada uma com `vehicle` (id numérico),
    # `plan` (uuid) e `total_visits`. ⚠️ O parâmetro é OBRIGATÓRIO: sem ele a rota
    # responde 200 com **lista vazia**, não com tudo — então "veio vazio" aqui
    # pode ser data errada, nunca "a conta não tem rotas". A rota NÃO traz array
    # de visitas (`visits` não existe); quem liga visita→rota é o campo `route`
    # da própria visita.
    def rotas_da_data(data)
      Array(get("/v1/routes/routes/?planned_date=#{data.to_date.iso8601}"))
    rescue Error
      []
    end

    # Os N planos mais recentes da conta — nome, período e as rotas de cada um.
    #
    # ⚠️ A CONCLUSÃO ANTIGA ("a API não expõe listagem de planos, só dá para
    # buscar por data") ESTAVA ERRADA. `/v1/routes/plans/` existe, não é
    # documentado, e devolve TUDO com o nome que a operação usa — "EMAD SETEMBRO
    # 2026", "UBS OESTE AGOSTO 2026". Verificado em 28/08/2026 contra a API real:
    # 138 planos, 185 KB, 0,86 s. Ver bin/sondar_planos.
    #
    # POR QUE ORDENAR E CORTAR AQUI, e não na URL: a API IGNORA todo filtro nesta
    # rota. `ordering`, `limit`, `page`, `page_size`, `search`, `name`,
    # `start_date__gte` — os dez candidatos testados devolveram os mesmos 138
    # itens do controle sem parâmetro. Então não existe "pedir 5": pede-se tudo
    # (uma chamada, menos de 1 s) e corta-se em Ruby.
    #
    # `created` é o critério: `start_date` de um plano é o PRIMEIRO dia da janela
    # (o EMAD SETEMBRO 2026 vai de 31/08 a 08/09), e é justamente essa janela que
    # tornava impossível adivinhar "a data do plano". Quem publicou por último é
    # quem o operador quer — ele abre a ferramenta logo depois de lançar.
    #
    # COM PERÍODO (`inicio`/`fim`) o corte muda de propósito: em vez dos N mais
    # recentes, devolve TODOS os planos da faixa — é assim que se alcança um
    # plano antigo sem saber o dia dele, e é o mesmo recorte que a tela do
    # SimpliRoute usa. O critério é a JANELA DO PLANO CRUZAR o período
    # (`start_date <= fim AND end_date >= inicio`), a mesma semântica de
    # `Consolidacao.cruzando_periodo`: um plano de 31/08 a 08/09 pertence a
    # agosto E a setembro, e some da lista se a regra for "está dentro".
    def planos(limite: 5, inicio: nil, fim: nil)
      lista = Array(get('/v1/routes/plans/')).sort_by { |p| p['created'].to_s }.reverse
      return lista.first(limite) if inicio.blank? || fim.blank?

      i = inicio.to_date
      f = fim.to_date
      lista.select { |p| cruza_periodo?(p, i, f) }
    rescue Error, Date::Error
      []
    end

    # Veículos da conta, para traduzir o id numérico que vem nas rotas
    # (`vehicle: 630011`) no nome que a operação usa (`GADE_038`).
    #
    # Best-effort pelo mesmo motivo de `rotas_da_data`: sem token na máquina de
    # desenvolvimento não dá para verificar o caminho, então endpoint ausente
    # devolve [] e Romaneios::PlanoDoDia segue com o id em vez de quebrar a
    # importação inteira.
    def veiculos
      Array(get('/v1/routes/vehicles/'))
    rescue Error
      []
    end

    # Resolve o `id` numérico (usado na URL de escrita) a partir de uma Entrega
    # do espelho local, que só tem tracking_id (SR...) + reference_id (NF) +
    # planned_date. Casa pelo tracking_id; se não achar, tenta pela NF.
    # Levanta NotFound (nada) ou Error (ambiguidade sem tracking_id).
    #
    # ⚠️ Ambiguidade por NF é REAL (plano duplicado gera duas visitas com a mesma
    # NF). Quem precisa lidar com isso — a tela de edição — usa
    # `visitas_da_data` + `casa_entrega?` e deixa o ADM escolher; este método
    # continua servindo os fluxos que exigem resposta única.
    def resolver_id(entrega)
      data = entrega.planned_date&.to_date
      raise NotFound, 'Entrega sem planned_date — impossível localizar na API.' if data.blank?

      visitas = visitas_da_data(data)

      if entrega.tracking_id.present?
        achada = visitas.find { |v| v['tracking_id'].to_s == entrega.tracking_id.to_s }
        return achada['id'] if achada
      end

      nf = entrega.reference_id.to_s
      por_nf = visitas.select { |v| v['reference'].to_s == nf }
      raise NotFound, "Visita da NF #{nf} não encontrada na API na data #{data.strftime('%d/%m/%Y')}." if por_nf.empty?
      if por_nf.size > 1
        raise Error, "Mais de uma visita para a NF #{nf} em #{data.strftime('%d/%m/%Y')} — não é possível resolver com segurança."
      end

      por_nf.first['id']
    end

    # Atualiza campos da visita (PATCH — só o que vier em `attrs`). Retorna a
    # visita atualizada (hash). Preferimos PATCH ao checkout para não sobrescrever
    # assinatura/geo originais sem intenção.
    def atualizar_visita(id, attrs)
      patch("/v1/routes/visits/#{id}/", attrs)
    end

    # Histórico da visita (best-effort — a API pode devolver []).
    def historico(id)
      Array(get("/v1/routes/visits/#{id}/history/"))
    rescue Error
      []
    end

    # Detalhe do comprovante de entrega (POD): traz foto, assinatura, hora e GPS.
    # É uma fonte MAIS COMPLETA de imagens que `visita(id)` — a visita crua às
    # vezes vem com `pictures` vazio mesmo havendo foto registrada no checkout.
    # Best-effort: se o endpoint não existir/responder, devolve {} e a tela segue
    # com o que a visita tiver.
    def detalhe_visita(id)
      get("/v1/plans/visits/#{id}/detail/") || {}
    rescue Error
      {}
    end

    private

    # Plano sem data em um dos lados não some da lista por isso: usa a que tem.
    # Sumir em silêncio é o pior resultado possível numa lista de escolha.
    def cruza_periodo?(plano, inicio, fim)
      comeco = Date.parse(plano['start_date'].to_s) rescue nil
      termino = Date.parse(plano['end_date'].to_s) rescue nil
      comeco ||= termino
      termino ||= comeco
      return false if comeco.nil?

      comeco <= fim && termino >= inicio
    end

    def get(path)
      requisicao(Net::HTTP::Get.new(caminho(path)))
    end

    def patch(path, body)
      req = Net::HTTP::Patch.new(caminho(path))
      req.body = body.to_json
      requisicao(req)
    end

    def caminho(path)
      URI.join(@base_uri.to_s, path)
    end

    def requisicao(req)
      req['Authorization'] = "Token #{@token}"
      req['Content-Type']  = 'application/json'
      req['Accept']        = 'application/json'

      res = Net::HTTP.start(@base_uri.host, @base_uri.port, use_ssl: @base_uri.scheme == 'https',
                                                            open_timeout: 10, read_timeout: 30) do |http|
        http.request(req)
      end

      unless res.is_a?(Net::HTTPSuccess)
        raise Error, "SimpliRoute respondeu #{res.code}: #{res.body.to_s.truncate(300)}"
      end

      res.body.present? ? JSON.parse(res.body) : nil
    rescue JSON::ParserError => e
      raise Error, "Resposta inválida do SimpliRoute: #{e.message}"
    rescue Net::OpenTimeout, Net::ReadTimeout
      raise Error, 'Tempo esgotado ao falar com o SimpliRoute. Tente de novo.'
    end
  end
end
