# app/services/analytics/notas_fora_operacao.rb
#
# NFs que o motorista entregou no período mas que NÃO estão em nenhuma planilha
# de operação (`gade_entregas_*`) — as notas que entram por plano avulso ou de
# inclusão, fora do carregamento original do cliente.
#
# Elas contam no dashboard financeiro (o motorista foi ao local e recebe por
# isso) e SUMIAM do dashboard de operações, porque lá o INNER JOIN com a tabela
# da operação simplesmente as descarta. Era metade da divergência entre as duas
# telas — agora aparece como painel próprio em vez de virar diferença silenciosa.
#
# SEGURANÇA: os nomes das tabelas passam pela whitelist (Operacao.nomes_validos,
# que lê o catálogo) + quote_table_name. Bases SOMENTE LEITURA.
module Analytics
  class NotasForaOperacao
    # Teto da listagem na tela (os totais continuam contando tudo).
    LIMITE = 300

    # Colunas extras do espelho que a tela usa. Nem toda base tem todas — as
    # ausentes viram NULL, mesmo padrão de selects_gade em OperacaoMetricas.
    # Whitelist fixa: nada aqui vem do usuário.
    COLUNAS_EXTRA = %w[title notes comments route_id].freeze

    # Candidatas ao nome do PLANO de origem ("(Avulsa)", "INCLUSÃO"...), na ordem
    # de preferência.
    #
    # ⚠️ `title` NÃO entra aqui: conferido em 24/08/2026 com dado real, ele traz
    # "NF 89096 - KAIQUE TAUAN DA SILVA" — o formato da coluna A da planilha de
    # importação (`NF {nota_fiscal} - {nome_completo}`), ou seja, o destinatário.
    # Agrupar por ele daria um grupo por NF, o que não informa nada.
    COLUNAS_PLANO = %w[notes comments route_id].freeze

    def initialize(inicio:, fim:)
      @inicio = inicio&.to_date
      @fim    = fim&.to_date
    end

    # Visitas cruas (pode haver mais de uma por NF).
    def visitas
      @visitas ||= carregar
    end

    # Uma linha por NF: a última visita dela. Mesmo critério de OperacaoMetricas.
    def linhas
      @linhas ||= visitas.group_by { |r| r['reference_id'].to_s }
                         .values
                         .map { |vs| vs.max_by { |r| ordem_visita(r) } }
                         .sort_by { |r| r['checkout'].to_s }
                         .reverse
    end

    def total
      linhas.size
    end

    def entregues
      linhas.count { |r| r['status'] == 'completed' }
    end

    def nao_entregues
      linhas.count { |r| Entrega::STATUS_FALHA.include?(r['status']) }
    end

    def pendentes
      total - entregues - nao_entregues
    end

    def any?
      total.positive?
    end

    # Nome do plano de origem: primeira coluna de COLUNAS_PLANO preenchida.
    # nil quando o espelho não guarda essa informação.
    def plano(registro)
      COLUNAS_PLANO.each do |coluna|
        valor = registro[coluna].to_s.strip
        return valor if valor.present?
      end
      nil
    end

    # Destinatário/título da visita (coluna `title` do espelho).
    def titulo(registro)
      registro['title'].to_s.strip.presence
    end

    def plano_identificado?
      linhas.any? { |r| plano(r) }
    end

    # Quebra do painel em grupos. Quando o espelho traz o plano, separa
    # "(Avulsa)" de "INCLUSÃO"; quando não traz, agrupar por título daria um
    # grupo por NF — então cai para a unidade, que ainda diz algo útil.
    # Devolve [rótulo do agrupamento, [{ nome:, total: }, ...]].
    def agrupamento
      if plano_identificado?
        ['Plano', agrupar { |r| plano(r) || 'SEM PLANO' }]
      else
        ['Unidade', agrupar { |r| r['contact_name'].to_s.strip.presence || 'SEM UNIDADE' }]
      end
    end

    # Rótulo legível do resultado da última visita.
    RESULTADOS = { 'completed' => 'Entregue', 'failed' => 'Não entregue',
                   'pending' => 'Em aberto', 'in_progress' => 'Em rota' }.freeze

    def resultado(registro)
      status = registro['status'].to_s
      RESULTADOS[status] || status.presence || 'sem status'
    end

    def sucesso?(registro)
      registro['status'] == 'completed'
    end

    def listagem
      linhas.first(LIMITE)
    end

    def truncada?
      total > LIMITE
    end

    private

    def agrupar
      linhas.group_by { |r| yield(r) }
            .map { |nome, rows| { nome: nome, total: rows.size } }
            .sort_by { |h| -h[:total] }
    end

    def ordem_visita(registro)
      t = registro['checkout'].presence&.to_time
      [t ? 1 : 0, t || Time.at(0)]
    rescue ArgumentError, TypeError
      [0, Time.at(0)]
    end

    def carregar
      tabelas = Operacao.nomes_validos
      return [] if tabelas.empty?

      conn     = ActiveRecord::Base.connection
      rastreio = conn.quote_table_name(Entrega.table_name)
      conta    = Entrega.condicao_conta_sql('r')

      # `nota_fiscal IS NOT NULL` é OBRIGATÓRIO: um único NULL na subquery faz o
      # NOT IN devolver ZERO linhas (semântica de três valores do SQL) e o painel
      # apareceria vazio para sempre.
      conhecidas = tabelas.map do |t|
        "SELECT nota_fiscal FROM #{conn.quote_table_name(t)} WHERE nota_fiscal IS NOT NULL"
      end.join(' UNION ')

      sql = <<~SQL
        SELECT r.tracking_id, r.reference_id, r.driver, r.vehicle, r.status, r.observation,
               r.contact_name, r.address, r.checkout, r.planned_date,
               #{selects_plano(conn)}
        FROM #{rastreio} r
        WHERE r.reference_id IS NOT NULL
          #{conta ? "AND #{conta}" : ''}
          #{filtro_periodo(conn)}
          AND r.reference_id::text NOT IN (#{conhecidas})
      SQL

      conn.select_all(sql).to_a
    end

    # Colunas extras que existirem de fato; as demais viram NULL com o mesmo
    # alias, para a leitura da linha não precisar saber quais existem.
    def selects_plano(conn)
      existentes = conn.columns(Entrega.table_name).map(&:name)
      COLUNAS_EXTRA.map do |coluna|
        existentes.include?(coluna) ? "r.#{coluna}" : "CAST(NULL AS text) AS #{coluna}"
      end.join(', ')
    end

    # Mesmo recorte de OperacaoMetricas#filtro_periodo: atendidas pela data real
    # (checkout); em aberto (sem checkout) pela data planejada.
    def filtro_periodo(conn)
      return '' unless @inicio && @fim

      ini      = conn.quote(@inicio)
      fim_excl = conn.quote(@fim + 1)
      fim_dia  = conn.quote(@fim.end_of_day)
      "AND ((r.checkout >= #{ini} AND r.checkout < #{fim_excl})" \
        " OR (r.checkout IS NULL AND r.planned_date >= #{ini} AND r.planned_date <= #{fim_dia}))"
    end
  end
end
