# app/services/romaneios/casador_de_operacao.rb
#
# Casa o NOME DO PLANO do SimpliRoute com a tabela da operação do mês.
#
# ── POR QUE EXISTE ──────────────────────────────────────────────────────────
# A operação não trabalha por data — ela identifica o plano pelo nome ("ENTREGAS
# EMAD 09.2026", "UBS SUL E LESTE AGOSTO 2026"). E é a operação do mês que diz
# quem é paciente NOVO: sem ela vinculada, `Romaneios::EnriquecimentoGade` nem
# roda e a coluna APARELHO sai vazia para todos — um romaneio que parece certo e
# só revela o erro no papel, com aparelho não entregue.
#
# A API pública do SimpliRoute NÃO tem endpoint que liste planos (a documentação
# só expõe criação, /v1/plans/{data}/vehicles/ e /v1/plans/routes/{id}/visits/);
# a lista que aparece na tela deles é da interface web. Então o nome chega aqui
# DIGITADO ou COLADO pelo operador, e o casamento tem que aguentar variação.
#
# ── AS DIFERENÇAS SÃO SISTEMÁTICAS, NÃO ALEATÓRIAS ──────────────────────────
#   plano  "UBS SUL E LESTE AGOSTO 2026"  x  tabela  ubs_sul_leste_ago_2026
#                   ↑ conectivo "E"                        ↑ mês abreviado
#   plano  "ENTREGAS EMAD 09.2026"        x  tabela  emad_set_2026
#           ↑ prefixo genérico    ↑ mês NUMÉRICO
# Por isso a comparação é por CONJUNTO DE PALAVRAS + (mês, ano), e não por
# igualdade de texto — que erraria em todos os casos reais acima.
module Romaneios
  class CasadorDeOperacao
    # Palavras que aparecem no nome do plano e nunca no nome da tabela. "ENTREGAS"
    # entra aqui porque é o prefixo genérico que o cliente usa nos planos; o
    # prefixo das tabelas (`gade_entregas_`) já é removido antes.
    RUIDO = %w[entregas entrega de da do das dos e o a as os plano planos].freeze

    Resultado = Struct.new(:tabela, :confianca, :motivo, keyword_init: true)

    def self.casar(nome, tabelas: nil)
      new(nome, tabelas: tabelas).casar
    end

    def initialize(nome, tabelas: nil)
      @nome    = nome.to_s
      @tabelas = tabelas
    end

    # Devolve o nome da tabela, ou nil quando não dá para ter certeza.
    #
    # ⚠️ Empate devolve NIL de propósito. Vincular a operação errada é pior do que
    # não vincular: com a errada, APARELHO e TELEFONE saem preenchidos com dados
    # de OUTRO mês e ninguém desconfia; sem nenhuma, a tela avisa que está sem
    # operação e o operador escolhe.
    def casar
      resultado.tabela
    end

    def resultado
      @resultado ||= calcular
    end

    private

    def calcular
      alvo = decompor(@nome)
      return Resultado.new(confianca: :nenhuma, motivo: 'nome vazio') if alvo[:palavras].empty?

      candidatas = (@tabelas || Operacao.nomes_validos).map do |tabela|
        [tabela, pontuar(alvo, decompor(Operacao.label(tabela)))]
      end.reject { |_, ponto| ponto.nil? }

      return Resultado.new(confianca: :nenhuma, motivo: 'nenhuma operação com o mesmo mês/ano') if candidatas.empty?

      melhor = candidatas.max_by(&:last)
      empate = candidatas.count { |_, ponto| ponto == melhor.last }
      if empate > 1
        return Resultado.new(confianca: :ambigua,
                             motivo: "#{empate} operações batem igualmente — escolha na lista")
      end

      Resultado.new(tabela: melhor.first, confianca: :alta, motivo: 'mês, ano e palavras batem')
    end

    # "UBS SUL E LESTE AGOSTO 2026" -> { palavras: #{ubs sul leste}, mes: 8, ano: 2026 }
    def decompor(texto)
      tokens = normalizar(texto)
      ano    = tokens.find { |t| t.match?(/\A(19|20)\d{2}\z/) }
      mes    = mes_de(tokens, ano)

      palavras = tokens - [ano, mes[:token]].compact
      palavras = palavras.reject { |t| RUIDO.include?(t) }

      { palavras: palavras.to_set, mes: mes[:numero], ano: ano&.to_i }
    end

    # Aceita "agosto", "ago" e o mês NUMÉRICO de "09.2026" — este último só quando
    # vem colado ao ano, senão o "3" de "AVULSAS 3 EMAD" viraria março.
    def mes_de(tokens, ano)
      por_nome = tokens.find { |t| Operacao.mes_numero(t) }
      return { token: por_nome, numero: Operacao.mes_numero(por_nome) } if por_nome

      return { token: nil, numero: nil } unless ano

      idx = tokens.index(ano)
      anterior = idx&.positive? ? tokens[idx - 1] : nil
      if anterior&.match?(/\A\d{1,2}\z/) && (1..12).cover?(anterior.to_i)
        { token: anterior, numero: anterior.to_i }
      else
        { token: nil, numero: nil }
      end
    end

    def normalizar(texto)
      ActiveSupport::Inflector.transliterate(texto.to_s)
                              .downcase
                              .split(/[^a-z0-9]+/)   # "09.2026" e "EMAD/SUDESTE" quebram aqui
                              .reject(&:empty?)
    end

    # nil = descartada. Mês e ano são ELIMINATÓRIOS: é o que impede "EMAD 09.2026"
    # de casar com a tabela de agosto, que tem exatamente as mesmas palavras.
    def pontuar(alvo, candidata)
      return nil if alvo[:mes] && candidata[:mes] && alvo[:mes] != candidata[:mes]
      return nil if alvo[:ano] && candidata[:ano] && alvo[:ano] != candidata[:ano]
      return nil if alvo[:palavras].empty? || candidata[:palavras].empty?

      comuns     = (alvo[:palavras] & candidata[:palavras]).size
      return nil if comuns.zero?

      sobrando   = (alvo[:palavras] ^ candidata[:palavras]).size
      # Palavra em comum soma 2; palavra que só um lado tem tira 1. "UBS SUL LESTE"
      # contra "UBS SUL" ainda casa, mas perde para "UBS SUL LESTE" exato.
      (comuns * 2) - sobrando
    end
  end
end
