# app/services/romaneios/importador.rb
#
# O ÚNICO lugar que sabe reconciliar um plano recebido com o romaneio já editado.
# Os dois caminhos de entrada (API do SimpliRoute e upload do .xlsx) entregam a
# mesma forma de linha e caem aqui — senão a mesma parada importada por caminhos
# diferentes viraria duas.
#
# ── A REGRA (dirty tracking POR CAMPO, não por linha) ───────────────────────
# 1. Identidade = chave_origem (NF, ou digest do título+endereço). Veículo e ordem
#    NÃO entram: eles mudam quando o plano é reotimizado e a parada continua sendo
#    a mesma entrega.
# 2. Linha que já existe: `origem` (o snapshot do plano) é SEMPRE atualizada; os
#    campos são reescritos SÓ se o operador não os tiver tocado.
# 3. Linha nova: insere.
# 4. Linha que sumiu do plano: sem edição humana, apaga; com edição, marca
#    `removido_em` — apagar levaria junto o trabalho sem deixar rastro na tela.
# 5. NF repetida no plano: fica a primeira, as outras são reportadas.
#
# Por que POR CAMPO e não um "bloqueado" na linha inteira: a edição comum é "o
# operador corrigiu um telefone". Travar a linha congelaria um endereço velho quando
# o SimpliRoute regeocodificasse, e o papel sairia com um endereço que o motorista
# não acha. Por campo é a única regra em que "corrigi o telefone" não significa
# "também respondo pelo endereço".
module Romaneios
  class Importador
    Resultado = Struct.new(:novos, :atualizados, :preservados, :saidos, :duplicados,
                           :veiculos, :colunas_ausentes, keyword_init: true) do
      def to_h
        super.transform_values { |v| v.is_a?(Array) ? v : v.to_i }
      end

      # Frase que a tela mostra depois de importar. Números soltos não dizem se
      # deu certo; esta linha diz.
      def resumo
        partes = ["#{novos} nova(s)", "#{atualizados} atualizada(s)"]
        partes << "#{preservados} mantida(s) por edição" if preservados.to_i.positive?
        partes << "#{saidos} saíram do plano"            if saidos.to_i.positive?
        partes << "#{duplicados} NF repetida(s) no plano" if duplicados.to_i.positive?
        partes.join(' · ')
      end
    end

    def initialize(romaneio:, linhas:, usuario: nil, origem: nil)
      @romaneio = romaneio
      @recebidas = Array(linhas)
      @usuario  = usuario
      @origem   = origem
    end

    def aplicar!
      novas, duplicados = deduplicar(@recebidas)
      dados = enriquecer(novas)

      novos = atualizados = preservados = saidos = 0
      vistas = []

      ActiveRecord::Base.transaction do
        existentes = @romaneio.romaneio_linhas.index_by(&:chave_origem)

        dados.each do |linha|
          vistas << linha[:chave_origem]
          registro = existentes[linha[:chave_origem]]

          if registro.nil?
            criar(linha)
            novos += 1
          else
            preservados += atualizar(registro, linha)
            atualizados += 1
          end
        end

        saidos = remover_ausentes(existentes, vistas)
        sincronizar_veiculos(dados)

        @romaneio.update!(
          origem:        @origem || @romaneio.origem,
          importado_em:  Time.current,
          importado_por: @usuario || @romaneio.importado_por
        )
      end

      # Fora do bloco de propósito: `return` dentro de uma transaction do Rails 7
      # COMMITA em silêncio, o que é uma pegadinha esperando um leitor distraído.
      Resultado.new(
        novos: novos, atualizados: atualizados, preservados: preservados,
        saidos: saidos, duplicados: duplicados,
        veiculos: dados.map { |l| l[:veiculo] }.uniq.size,
        colunas_ausentes: @colunas_ausentes || []
      )
    end

    private

    # A primeira ocorrência vence. Plano duplicado no SimpliRoute gera visita nova
    # com a MESMA reference — repetir a parada no papel faria o motorista levar dois
    # pacotes para o mesmo endereço.
    def deduplicar(linhas)
      vistas = {}
      repetidas = 0

      linhas.each do |linha|
        chave = RomaneioLinha.chave_para(nota_fiscal: linha[:nota_fiscal],
                                         titulo: linha[:titulo], endereco: linha[:endereco])
        if vistas.key?(chave)
          repetidas += 1
          next
        end
        vistas[chave] = linha.merge(chave_origem: chave)
      end

      [vistas.values, repetidas]
    end

    # Aplica o que a operação sabe por cima do que o plano trouxe.
    # Precedência (decidida com o usuário): operação > plano.
    def enriquecer(linhas)
      tabela = @romaneio.operacao_tabela.to_s
      return linhas if tabela.empty?

      gade = EnriquecimentoGade.new(tabela)
      @colunas_ausentes = gade.colunas_ausentes
      dados = gade.para_notas(linhas.map { |l| l[:nota_fiscal] })

      linhas.map do |linha|
        extra = dados[linha[:nota_fiscal].to_s] || {}
        linha.merge(
          aparelho:  extra[:aparelho].to_s,
          # A coluna `telefones` da operação é a fonte completa; o "TEL:" das
          # anotações do plano só entra quando ela não tem o número.
          telefones: extra[:telefones].to_s.empty? ? linha[:telefones].to_s : extra[:telefones],
          nome:      extra[:nome].to_s.empty? ? linha[:nome].to_s : extra[:nome]
        )
      end
    rescue ArgumentError
      # Operação removida do banco entre a criação do romaneio e a reimportação:
      # segue com o que o plano trouxe, e a tela avisa.
      @colunas_ausentes = EnriquecimentoGade::COLUNAS.keys
      linhas
    end

    def campos(linha)
      { 'nota_fiscal' => linha[:nota_fiscal].to_s, 'nome' => linha[:nome].to_s,
        'endereco' => linha[:endereco].to_s, 'aparelho' => linha[:aparelho].to_s,
        'telefones' => linha[:telefones].to_s }
    end

    def criar(linha)
      valores = campos(linha)
      @romaneio.romaneio_linhas.create!(
        valores.merge(
          veiculo: linha[:veiculo], ordem: linha[:ordem],
          chave_origem: linha[:chave_origem], origem: valores, campos_editados: []
        )
      )
    end

    # Devolve quantos campos foram PRESERVADOS por terem edição humana.
    def atualizar(registro, linha)
      valores = campos(linha)
      preservados = 0

      valores.each do |campo, valor|
        if registro.editado?(campo)
          preservados += 1
          next
        end
        registro[campo] = valor
      end

      # Veículo e ordem são a verdade do roteiro, não conteúdo do documento —
      # seguem o plano mesmo numa linha editada.
      registro.veiculo     = linha[:veiculo]
      registro.ordem       = linha[:ordem]
      registro.origem      = valores
      registro.removido_em = nil
      registro.save!

      preservados
    end

    def remover_ausentes(existentes, vistas)
      ausentes = existentes.except(*vistas).values
      return 0 if ausentes.empty?

      limpas, editadas = ausentes.partition { |l| l.campos_editados.blank? }
      RomaneioLinha.where(id: limpas.map(&:id)).delete_all if limpas.any?
      RomaneioLinha.where(id: editadas.map(&:id)).update_all(removido_em: Time.current) if editadas.any?

      ausentes.size
    end

    # Cria a folha de cada veículo novo e reordena as páginas. O `motorista` já
    # preenchido NÃO é tocado: ele é digitado na tela, não vem do plano (o export
    # real traz a coluna vazia em todas as linhas).
    def sincronizar_veiculos(linhas)
      ordem = linhas.map { |l| l[:veiculo] }.uniq.sort
      existentes = @romaneio.romaneio_veiculos.index_by(&:veiculo)

      ordem.each_with_index do |veiculo, i|
        registro = existentes[veiculo] ||
                   @romaneio.romaneio_veiculos.build(veiculo: veiculo)
        registro.posicao = i
        motorista = linhas.find { |l| l[:veiculo] == veiculo && l[:motorista].to_s != '' }
        registro.motorista = motorista[:motorista] if motorista && registro.motorista.to_s.empty?
        registro.save!
      end

      # Veículo que saiu do plano e ficou sem nenhuma parada não tem folha para
      # imprimir.
      sobrando = existentes.keys - ordem
      @romaneio.romaneio_veiculos.where(veiculo: sobrando).destroy_all if sobrando.any?
    end
  end
end
