#!/usr/bin/env ruby
# frozen_string_literal: true
#
# Procura um jeito de ACHAR O PLANO sem adivinhar a data.
#
# ── O PROBLEMA ──────────────────────────────────────────────────────────────
# Hoje Romaneios::PlanoDoDia pergunta "qual plano existe NESTA data?", então
# alguém tem que acertar o dia em que o plano foi publicado. Quem usa a
# ferramenta pensa por OPERAÇÃO ("lançou a UBS Sudeste"), não por data — e a
# data do lançamento raramente é a que a pessoa lembra.
#
# O que queremos, na ordem de preferência:
#   A) listar os ÚLTIMOS N PLANOS (nome + data + id) e deixar o operador
#      escolher numa lista — é o que resolve de verdade;
#   B) na falta disso, ir da NF ao plano: o operador já tem uma nota do
#      romaneio, e `?search=<NF>` em /v1/routes/visits/ já funciona. Se o objeto
#      da visita carregar uma FK de rota/plano, a cadeia
#      NF -> visita -> rota -> plano resolve o mesmo problema sem endpoint novo.
#
# ── RESULTADO DA RODADA DE 28/08/2026 (contra a API real) ───────────────────
#   ✔ `GET /v1/routes/plans/` EXISTE e resolve o problema: 138 planos, 185 KB,
#     0,86 s, cada um com `name` — "EMAD SETEMBRO 2026", "UBS OESTE AGOSTO
#     2026" — que é exatamente como o operador pensa. NÃO é documentado.
#     ⇒ A conclusão anterior, registrada no README ("não é possível buscar o
#       plano por nome"), ESTAVA ERRADA. O endpoint estava na lista de
#       candidatos do bin/sondar_plano_do_dia; o que faltou foi RODAR.
#   ✔ Campos do plano: id (uuid), name, start_date, end_date, created, modified,
#     status, is_cluster, fleet, routes[] (uuids), start_time, end_time.
#   ✔ CADEIA COMPLETA, verificada ponta a ponta:
#       /v1/routes/plans/            -> plano: name + routes[]
#       /v1/routes/routes/{uuid}/    -> vehicle (id), planned_date, total_visits
#       /v1/plans/routes/{uuid}/visits/ -> order, vehicle_id, reference (NF), title
#       /v1/routes/vehicles/         -> 634185 = GADE_057 (199 na frota)
#     É tudo o que o romaneio precisa, sem adivinhar data nenhuma.
#   ✔ Caminho pela NF também funciona: a visita traz `route` (uuid), e a rota
#     traz `plan` (uuid). NF -> visita -> rota -> plano fecha.
#   ✘ NENHUM filtro funciona em /v1/routes/plans/: `ordering`, `limit`, `page`,
#     `page_size`, `search`, `name`, `planned_date`, `planned_date__gte`,
#     `created_at__gte`, `status` — os dez devolveram os MESMOS 138 itens do
#     controle. ⇒ "os 5 mais recentes" se faz em Ruby, depois de baixar tudo
#     (SimpliRoute::Client#planos). Uma chamada de menos de 1 s, é barato.
#   ✘ `/v1/plans/`, `/v1/plans/list/`, `/v1/plans/plans/`, `/v1/plans/routes/`
#     -> 404. O prefixo é `/v1/routes/`, não `/v1/plans/` (que só serve
#     `/v1/plans/routes/{uuid}/visits/`).
#   ⚠️ `OPTIONS /v1/routes/visits/` devolve 500 — não insista, não é seu token.
#
# Rode de novo se desconfiar que a API mudou.
#
# ── MÉTODO (as três regras que este script segue) ───────────────────────────
# 1. PARÂMETRO QUE NÃO EXISTE É IGNORADO EM SILÊNCIO. A API é Django REST:
#    filtro desconhecido devolve 200 + a lista inteira. "Veio 200 com
#    resultados" NÃO prova nada — já custou caro aqui (ver bin/sondar_busca_nf).
#    Por isso todo filtro é comparado com um CONTROLE sem o parâmetro.
# 2. VALOR INVÁLIDO COMO ORÁCULO. É mais rápido e mais conclusivo que a
#    contagem: filtro que EXISTE reclama do valor (400 nomeando o campo);
#    filtro que não existe ignora e devolve 200. Uma chamada por candidato.
# 3. OPTIONS ANTES DE CHUTAR. O DRF responde OPTIONS com os métodos permitidos
#    e, quando a rota é navegável, com o schema dos campos — é a forma mais
#    barata de saber o formato de um "plano" sem inventar nome de campo.
#
# ⚠️ SÓ LEITURA. Este script faz apenas GET e OPTIONS — não existe POST/PATCH/
#    PUT/DELETE aqui, de propósito. A API é a de PRODUÇÃO do cliente.
#
# Uso (o token vive no ambiente, NUNCA no git):
#
#   SIMPLIROUTE_TOKEN=xxx bin/sondar_planos --data 2026-08-31
#   SIMPLIROUTE_TOKEN=xxx bin/sondar_planos --data 2026-08-31 --nf 90158
#   SIMPLIROUTE_TOKEN=xxx bin/sondar_planos --data 2026-08-31 --limite 5
#
# --data  um dia em que EXISTE plano publicado (é a âncora de tudo).
# --nf    opcional: testa o caminho (B) partindo de uma nota conhecida.
# --limite quantos planos queremos na listagem final (padrão 5).

require 'net/http'
require 'json'
require 'uri'
require 'date'

BASE  = ENV.fetch('SIMPLIROUTE_BASE_URL', 'https://api.simpliroute.com')
TOKEN = ENV['SIMPLIROUTE_TOKEN'].to_s

def sair(msg)
  warn msg
  exit 1
end

sair('Defina SIMPLIROUTE_TOKEN no ambiente.') if TOKEN.empty?

data = nil
nf = nil
limite = 5
ARGV.each_with_index do |a, i|
  data   = ARGV[i + 1]         if a == '--data'
  nf     = ARGV[i + 1]         if a == '--nf'
  limite = ARGV[i + 1].to_i    if a == '--limite'
end
sair('Uso: bin/sondar_planos --data <YYYY-MM-DD> [--nf <numero>] [--limite 5]') if data.to_s.empty?
limite = 5 if limite <= 0

# ── HTTP (GET e OPTIONS, só) ────────────────────────────────────────────────
def requisicao(caminho, metodo: :get, aceita: 'application/json')
  uri = URI.join(BASE, caminho)
  req = metodo == :options ? Net::HTTP::Options.new(uri) : Net::HTTP::Get.new(uri)
  req['Authorization'] = "Token #{TOKEN}"
  req['Accept']        = aceita

  t0 = Time.now
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
                                            open_timeout: 10, read_timeout: 120) { |h| h.request(req) }
  corpo = begin
    res.body.to_s.empty? ? nil : JSON.parse(res.body)
  rescue JSON::ParserError
    res.body.to_s[0, 300]
  end
  [res.code.to_i, corpo, res.body.to_s.bytesize, Time.now - t0, res]
rescue StandardError => e
  [0, "ERRO: #{e.class}: #{e.message}", 0, Time.now - t0, nil]
end

# A resposta pode ser lista crua OU paginada ({count, next, results}).
def itens(corpo)
  return corpo                   if corpo.is_a?(Array)
  return Array(corpo['results']) if corpo.is_a?(Hash) && corpo.key?('results')

  []
end

def total(corpo, lista)
  corpo.is_a?(Hash) && corpo['count'] ? corpo['count'] : lista.size
end

def titulo(t)
  puts "\n#{'─' * 74}\n#{t}\n#{'─' * 74}"
end

def linha(caminho, codigo, bytes, seg, extra = nil)
  puts format('    %-58s HTTP %-3s %7s  %5.2fs%s',
              caminho[0, 58], codigo, "#{bytes}B", seg, extra ? "  #{extra}" : '')
end

CHAVES_NOME  = /name|title|label|descri/i.freeze
CHAVES_ELO   = /^(route|plan|plan_id|route_id|plan_uuid)$/i.freeze

puts "SimpliRoute · sondagem de PLANOS   base=#{BASE}   data-âncora=#{data}"
puts 'Somente GET/OPTIONS. Nada é alterado.'

# ════════════════════════════════════════════════════════════════════════════
# [1] A VISITA APONTA PARA O PLANO?  (caminho B — o mais promissor)
# ════════════════════════════════════════════════════════════════════════════
# Se a visita carregar uma FK de rota/plano, o operador nunca mais precisa saber
# a data: ele cola uma NF do romaneio e o sistema descobre o plano.
titulo('[1] a VISITA carrega elo para rota/plano?')

caminho_visitas = "/v1/routes/visits/?planned_date=#{data}"
caminho_visitas += "&search=#{nf}" if nf
cod, corpo, bytes, seg = requisicao(caminho_visitas)
linha(caminho_visitas, cod, bytes, seg)

visita = itens(corpo).first
elos = {}
if visita.is_a?(Hash)
  puts "    chaves da visita: #{visita.keys.sort.join(', ')}"
  elos = visita.select { |k, v| k.to_s.match?(CHAVES_ELO) && !v.nil? && v.to_s.strip != '' }
  if elos.empty?
    puts '    => NENHUM campo route/plan preenchido. O caminho (B) morre aqui;'
    puts '       o que vale é a listagem de planos do bloco [3].'
  else
    puts '    => ELOS ENCONTRADOS (é por aqui que se chega ao plano):'
    elos.each { |k, v| puts "         #{k} = #{v.inspect[0, 90]}" }
  end
else
  puts '    => nenhuma visita nessa data/NF — escolha uma --data com plano publicado.'
end

# ── [1b] segue cada elo ─────────────────────────────────────────────────────
# Não sabemos o prefixo da rota do recurso, então testamos os candidatos e
# deixamos o HTTP responder. Quem devolver 200 com campo de nome resolve tudo.
unless elos.empty?
  titulo('[1b] seguindo os elos — quem devolve nome/data do plano?')
  elos.each_value do |id|
    ["/v1/routes/routes/#{id}/", "/v1/plans/#{id}/", "/v1/plans/routes/#{id}/",
     "/v1/routes/#{id}/", "/v1/plans/routes/#{id}/visits/"].each do |c|
      cod, corpo, bytes, seg = requisicao(c)
      obj = corpo.is_a?(Hash) ? corpo : itens(corpo).first
      nomes = obj.is_a?(Hash) ? obj.select { |k, v| k.to_s.match?(CHAVES_NOME) && v.to_s.strip != '' } : {}
      linha(c, cod, bytes, seg, nomes.empty? ? nil : "nome: #{nomes.first.inspect[0, 60]}")
      next unless cod == 200 && obj.is_a?(Hash)

      puts "        chaves: #{obj.keys.sort.join(', ')}" if nomes.any?
    end
  end
end

# ════════════════════════════════════════════════════════════════════════════
# [2] OPTIONS — o que cada rota admite, sem chutar campo
# ════════════════════════════════════════════════════════════════════════════
titulo('[2] OPTIONS nos candidatos (métodos + schema, quando o DRF entrega)')

%w[
  /v1/plans/
  /v1/plans/routes/
  /v1/routes/plans/
  /v1/routes/routes/
  /v1/routes/visits/
].each do |c|
  cod, corpo, bytes, seg, res = requisicao(c, metodo: :options)
  linha(c, cod, bytes, seg, res && res['Allow'] ? "Allow: #{res['Allow']}" : nil)
  next unless corpo.is_a?(Hash)

  campos = corpo.dig('actions', 'GET') || corpo.dig('actions', 'POST')
  puts "        campos: #{campos.keys.sort.join(', ')}" if campos.is_a?(Hash)
  puts "        filtros: #{corpo['filters'].inspect[0, 200]}" if corpo['filters']
end

# ════════════════════════════════════════════════════════════════════════════
# [3] EXISTE LISTAGEM DE PLANOS?  (caminho A — o que o operador quer)
# ════════════════════════════════════════════════════════════════════════════
titulo('[3] procurando uma LISTAGEM de planos')

candidatas = [
  '/v1/plans/',
  '/v1/plans/list/',
  '/v1/plans/plans/',
  '/v1/routes/plans/',
  '/v1/plans/routes/',
  "/v1/plans/?planned_date=#{data}",
  "/v1/routes/plans/?planned_date=#{data}"
]

listagens = []
candidatas.each do |c|
  cod, corpo, bytes, seg = requisicao(c)
  lista = itens(corpo)
  linha(c, cod, bytes, seg, lista.empty? ? nil : "#{total(corpo, lista)} itens")
  next if lista.empty?

  item = lista.first
  next unless item.is_a?(Hash)

  listagens << [c, corpo, lista]
  puts "        chaves: #{item.keys.sort.join(', ')}"
  nomes = item.select { |k, v| k.to_s.match?(CHAVES_NOME) && v.to_s.strip != '' }
  if nomes.empty?
    puts '        => nenhum campo de nome PREENCHIDO (chave existir não basta)'
  else
    puts "        => nomes: #{lista.first(5).filter_map { |i| i.is_a?(Hash) ? i[nomes.keys.first] : nil }.inspect}"
  end
end

# ════════════════════════════════════════════════════════════════════════════
# [4] ORÁCULO DO VALOR INVÁLIDO — quais filtros são REAIS
# ════════════════════════════════════════════════════════════════════════════
# Filtro que existe reclama do valor (400 e costuma nomear o campo). Filtro que
# não existe é ignorado: 200 e a mesma lista do controle. Uma chamada por
# candidato — e o controle serve de régua para os que voltarem 200.
if listagens.any?
  alvo = listagens.first[0].split('?').first
  titulo("[4] filtros REAIS em #{alvo} (oráculo do valor inválido + controle)")

  cod, corpo, bytes, seg = requisicao(alvo)
  controle = total(corpo, itens(corpo))
  linha("#{alvo}  (CONTROLE)", cod, bytes, seg, "#{controle} itens")

  %w[ordering limit page_size page planned_date planned_date__gte name search
     created_at__gte status].each do |param|
    c = "#{alvo}?#{param}=%25%25%25zz"
    cod, corpo, bytes, seg = requisicao(c)
    qtd = total(corpo, itens(corpo))
    veredito = if cod == 400 then 'FILTRO EXISTE (reclamou do valor)'
               elsif cod == 200 && qtd == controle then 'ignorado (igual ao controle)'
               elsif cod == 200 then "mudou a contagem (#{qtd}) — investigar"
               else "HTTP #{cod}"
               end
    linha("?#{param}=<inválido>", cod, bytes, seg, veredito)
  end

  # ── [4b] os ÚLTIMOS N PLANOS numa chamada só ─────────────────────────────
  # É o formato que a tela precisa: o operador usa a ferramenta quando lançou
  # uma operação, então o plano que ele quer está entre os últimos — uma lista
  # curta de nomes vence qualquer campo de data.
  titulo("[4b] dá para pedir os #{limite} planos mais recentes de uma vez?")
  [
    "#{alvo}?ordering=-planned_date&limit=#{limite}",
    "#{alvo}?ordering=-created_at&limit=#{limite}",
    "#{alvo}?ordering=-id&page_size=#{limite}"
  ].each do |c|
    cod, corpo, bytes, seg = requisicao(c)
    lista = itens(corpo)
    resumo = lista.first(limite).filter_map do |i|
      next unless i.is_a?(Hash)

      nome = i.values_at(*i.keys.select { |k| k.to_s.match?(CHAVES_NOME) }).compact.first
      "#{nome || i['id']} (#{i['planned_date'] || i['created_at']})"
    end
    linha(c, cod, bytes, seg, "#{total(corpo, lista)} itens")
    puts "        #{resumo.join(' | ')}" if resumo.any?
  end
else
  titulo('[4] sem listagem de planos — nada para filtrar')
  puts '    Se o bloco [1] achou elo na visita, o caminho é a cadeia NF -> visita'
  puts '    -> rota -> plano. Se não achou nenhum dos dois, a conclusão antiga'
  puts '    ("não há como listar planos") se confirma e a saída é a planilha.'
end

# ════════════════════════════════════════════════════════════════════════════
# [5] API NAVEGÁVEL — o DRF às vezes desenha o formulário de filtros
# ════════════════════════════════════════════════════════════════════════════
# Quando a rota é navegável, o HTML traz os nomes de filtro válidos escritos na
# página. Só olhamos se aparece algo com cara de filtro; não baixamos a página.
titulo('[5] API navegável (Accept: text/html) — pistas de filtro no HTML')
['/v1/plans/', '/v1/routes/visits/'].each do |c|
  cod, corpo, bytes, seg = requisicao(c, aceita: 'text/html')
  html = corpo.is_a?(String) ? corpo : corpo.to_s
  pistas = html.scan(/name="([a-z_]{3,30})"/i).flatten.uniq.first(12)
  linha(c, cod, bytes, seg, pistas.empty? ? 'sem formulário' : "campos: #{pistas.join(', ')}")
end

titulo('COMO LER ISTO')
puts <<~FIM
  • Bloco [3] com itens e nome preenchido  -> caminho (A): a tela vira uma lista
    dos #{limite} planos mais recentes, e a data some do formulário.
  • Bloco [1] com elo (route/plan) na visita -> caminho (B): o operador cola uma
    NF do romaneio e o sistema acha o plano. Resolve sem endpoint de listagem.
  • Os dois vazios -> a conclusão anterior está certa: não há como buscar plano
    por nome com token de conta, e o .xlsx continua sendo o plano B.

  NÃO altere Romaneios::PlanoDoDia antes de ter esta saída: a escada de queda de
  lá foi escrita justamente por não haver este dado.
FIM
