# app/services/romaneios/plano_planilha.rb
#
# Lê o .xlsx do PLANO exportado do SimpliRoute e devolve as linhas na mesma forma
# que SimpliRoute::PlanoDoDia — é o caminho B do romaneio, para quando a API não
# responde ou o plano do dia ainda não foi publicado.
#
# ⚠️ O arquivo NÃO é persistido em lugar nenhum. Ele tem nome, endereço e telefone
# de paciente; o ActiveStorage aqui é configurado condicionalmente e não há política
# de retenção para pendurar isso. Lemos do tempfile e descartamos.
require 'roo'

module Romaneios
  class PlanoPlanilha
    class Erro < StandardError; end

    # Cabeçalhos aceitos, já normalizados (sem acento, minúsculo, espaço colapsado).
    #
    # ⚠️ Casamos por NOME e nunca por posição: o export do cliente varia. Medido no
    # arquivo real (SimpliRoute_Plan_ 2026-08-31.xlsx, 2047 linhas / 72 veículos):
    # a coluna "Motorista" vem VAZIA em todas as linhas e NÃO existem as colunas
    # "Aparelho", "Pessoa de contato" nem "Telefone de contato" — que o programa
    # Python original lia. Faltar qualquer uma delas é o caso NORMAL, não erro.
    #
    # "id de referencua" é um typo do próprio SimpliRoute; aceitamos os dois porque
    # eles podem corrigir a qualquer momento e aí o romaneio pararia de achar a NF.
    COLUNAS = {
      veiculo:     ['veiculo', 'vehicle'],
      motorista:   ['motorista', 'driver'],
      indice:      ['indice de rota', 'route index'],
      ordem:       ['ordem de entrega', 'delivery order'],
      titulo:      ['titulo', 'title'],
      endereco:    ['endereco', 'address'],
      anotacoes:   ['anotacoes', 'notes'],
      nota_fiscal: ['id de referencua', 'id de referencia', 'reference id', 'nota fiscal']
    }.freeze

    TAMANHO_MAXIMO = 20 * 1024 * 1024
    LINHAS_MAXIMO  = 20_000
    ASSINATURA_ZIP = "PK\x03\x04".b.freeze

    # `entrada` é o tempfile do upload (ou um caminho, nos specs).
    def initialize(entrada)
      @caminho = caminho_de(entrada)
      validar_arquivo!
    end

    def linhas
      @linhas ||= montar
    end

    private

    def caminho_de(entrada)
      return entrada.tempfile.path if entrada.respond_to?(:tempfile)
      return entrada.path         if entrada.respond_to?(:path)

      entrada.to_s
    end

    # Guardas antes de entregar o arquivo para a roo: um .xlsx é um zip, e um zip
    # inesperado (ou gigante) vira consumo de memória sem limite dentro do processo
    # que atende a requisição.
    def validar_arquivo!
      raise Erro, 'Envie a planilha do plano (.xlsx).' unless File.exist?(@caminho)
      raise Erro, 'A planilha passa de 20 MB — não parece ser o plano do dia.' if File.size(@caminho) > TAMANHO_MAXIMO

      assinatura = File.binread(@caminho, 4)
      return if assinatura == ASSINATURA_ZIP

      raise Erro, 'O arquivo não é um .xlsx. Baixe o plano do SimpliRoute em Excel e envie de novo.'
    end

    def montar
      planilha = Roo::Excelx.new(@caminho)
      cabecalho = mapear_colunas(planilha.row(1))

      if cabecalho[:veiculo].nil?
        raise Erro, 'A planilha não tem a coluna "Veículo" — confira se é o plano ' \
                    'do SimpliRoute e não a planilha de carga.'
      end

      total = planilha.last_row.to_i
      raise Erro, "A planilha tem #{total} linhas — acima do limite de #{LINHAS_MAXIMO}." if total > LINHAS_MAXIMO

      registros = (2..total).filter_map { |i| montar_linha(planilha.row(i), cabecalho, i) }
      ordenar(registros)
    ensure
      planilha&.close if planilha.respond_to?(:close)
    end

    # { chave => índice da coluna }. Coluna ausente simplesmente não entra no hash.
    def mapear_colunas(cabecalho)
      normalizados = Array(cabecalho).map { |c| normalizar_cabecalho(c) }

      COLUNAS.each_with_object({}) do |(chave, aceitos), memo|
        idx = normalizados.index { |c| aceitos.include?(c) }
        memo[chave] = idx if idx
      end
    end

    # Sem acento, minúsculo, espaço colapsado — o export alterna maiúscula/minúscula
    # e o acento varia com a versão do arquivo.
    def normalizar_cabecalho(valor)
      I18n.transliterate(valor.to_s).downcase.strip.gsub(/\s+/, ' ')
    end

    def montar_linha(linha, cabecalho, numero_da_linha)
      valor = ->(chave) { cabecalho[chave] ? linha[cabecalho[chave]] : nil }

      veiculo = valor.call(:veiculo).to_s.strip
      titulo  = valor.call(:titulo).to_s.strip
      ender   = valor.call(:endereco).to_s.strip
      # Linha sem veículo, ou sem nada que identifique a parada, é lixo de rodapé
      # do próprio export — pular em silêncio é o certo.
      return nil if veiculo.empty? || (titulo.empty? && ender.empty?)

      Normalizador.linha(
        veiculo:     veiculo,
        # A ordem definitiva é atribuída em #ordenar; aqui guardamos o que o
        # arquivo trouxer para poder ordenar depois.
        ordem:       valor.call(:ordem) || valor.call(:indice) || numero_da_linha,
        titulo:      titulo,
        endereco:    ender,
        anotacoes:   valor.call(:anotacoes),
        nota_fiscal: valor.call(:nota_fiscal),
        motorista:   valor.call(:motorista)
      )
    end

    # Ordem final: por veículo, pela ordem de entrega do plano. Renumeramos de 1 a N
    # dentro de cada veículo porque é isso que vira a coluna "#" do papel — e o
    # "Índice de rota" do export não é 1..N por veículo.
    def ordenar(registros)
      registros.group_by { |r| r[:veiculo] }.flat_map do |_veiculo, linhas|
        linhas.sort_by.with_index { |r, i| [r[:ordem].to_i, i] }
              .each_with_index.map { |r, i| r.merge(ordem: i + 1) }
      end
    end
  end
end
