# app/models/consolidacao.rb
class Consolidacao < ApplicationRecord
  self.table_name = 'consolidacoes'

  belongs_to :criador,     class_name: 'User', foreign_key: :created_by
  belongs_to :finalizador, class_name: 'User', foreign_key: :finalizado_por, optional: true
  belongs_to :arquivador,  class_name: 'User', foreign_key: :arquivado_por,  optional: true

  has_many :consolidacao_motoristas, dependent: :destroy
  has_many :consolidacao_entregas,   dependent: :destroy
  has_many :consolidacao_veiculos,   dependent: :destroy

  # Bucket dos lançamentos sem veículo (termo/apontamento manual legado). Mesmo
  # sentinel do controller e de ConsolidacaoVeiculo.
  SEM_VEICULO = ConsolidacaoVeiculo::SEM_VEICULO

  # ── Enums ───────────────────────────────────────────────────
  enum status: { rascunho: 0, finalizada: 1, arquivada: 2 }

  # ── Soft delete ─────────────────────────────────────────────
  scope :ativas,     -> { where(deleted_at: nil) }
  scope :arquivadas, -> { where.not(deleted_at: nil) }

  # ── Validações ──────────────────────────────────────────────
  validates :nome,        presence: true
  validates :data_inicio, presence: true
  validates :data_fim,    presence: true
  validates :created_by,  presence: true
  validate  :periodo_valido

  # ── Callbacks ───────────────────────────────────────────────
  before_save :recalcular_valor_total

  # ── Escopos ─────────────────────────────────────────────────
  scope :recentes,           -> { order(created_at: :desc) }
  # ⚠️ `no_periodo` exige a consolidação INTEIRA dentro da faixa. Não é o recorte
  # das telas (elas usam `cruzando_periodo`, abaixo) — um fechamento de 01/08 a
  # 31/08 sumia ao filtrar 01/08–26/08. Só use se o "caber dentro" for mesmo o
  # que se quer.
  scope :no_periodo, ->(i, f) { where('data_inicio >= ? AND data_fim <= ?', i, f) }

  # Consolidações que CRUZAM o período (não precisam caber inteiras dentro dele).
  # É o recorte do dashboard financeiro (DashboardController#carregar_dados_
  # financeiro) e, por isso, o de qualquer tela que precise bater com ele: um
  # fechamento de 01/08 a 31/08 tem que aparecer ao filtrar 01/08–26/08.
  scope :cruzando_periodo, ->(i, f) { where('data_inicio <= ? AND data_fim >= ?', f, i) }

  # ── Filtro por status de pagamento (derivado dos motoristas) ─
  # pago_em NÃO-nulo conta como pago; COUNT(pago_em) ignora os NULL.
  scope :pagamento_pago, -> {
    where(id: ConsolidacaoMotorista.ativos.group(:consolidacao_id)
                                   .having('COUNT(*) > 0 AND COUNT(*) = COUNT(pago_em)')
                                   .select(:consolidacao_id))
  }
  scope :pagamento_pendente, -> {
    where(id: ConsolidacaoMotorista.ativos.group(:consolidacao_id)
                                   .having('COUNT(pago_em) = 0')
                                   .select(:consolidacao_id))
  }
  scope :pagamento_parcial, -> {
    where(id: ConsolidacaoMotorista.ativos.group(:consolidacao_id)
                                   .having('COUNT(pago_em) > 0 AND COUNT(pago_em) < COUNT(*)')
                                   .select(:consolidacao_id))
  }

  # Consolidações que contêm QUALQUER das operações informadas (jsonb).
  # SEGURANÇA: os nomes passam por whitelist (Operacao.sanitizar) e por quote.
  scope :com_operacoes, ->(ops) {
    validas = Operacao.sanitizar(ops)
    next all if validas.empty?

    lista = validas.map { |o| connection.quote(o) }.join(',')
    where("EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(operacoes, '[]'::jsonb)) e WHERE e IN (#{lista}))")
  }

  # ── Métodos ─────────────────────────────────────────────────

  # Arquivar é um soft-delete administrativo: deve SEMPRE funcionar, mesmo numa
  # consolidação zerada ou em rascunho com dados incompletos. Por isso usamos
  # update_columns — pula validações (periodo_valido, presença) e o callback
  # recalcular_valor_total, que não fazem sentido ao apenas mover para o arquivo.
  def arquivar!(user)
    update_columns(
      deleted_at:    Time.current,
      status:        self.class.statuses[:arquivada],
      arquivado_por: user&.id,
      arquivado_em:  Time.current,
      updated_at:    Time.current
    )
  end

  # Restaura uma consolidação arquivada. Como o status original não é guardado,
  # inferimos: se já tinha sido finalizada (finalizado_em presente) volta para
  # :finalizada, senão para :rascunho. Limpa os campos de arquivamento.
  # Também via update_columns para não depender da validade do conteúdo.
  def reativar!(_user)
    update_columns(
      deleted_at:    nil,
      status:        self.class.statuses[finalizado_em? ? :finalizada : :rascunho],
      arquivado_por: nil,
      arquivado_em:  nil,
      updated_at:    Time.current
    )
  end

  # ── Pagamento (status derivado dos motoristas da consolidação) ──
  # :pendente (nenhum pago) · :pago (todos pagos) · :parcial (alguns).
  def status_pagamento
    total = consolidacao_motoristas.ativos.count
    return :pendente if total.zero?

    pagos = consolidacao_motoristas.ativos.pagos.count
    return :pendente if pagos.zero?

    pagos >= total ? :pago : :parcial
  end

  def valor_pago
    consolidacao_motoristas.ativos.pagos.sum(:valor_total)
  end

  def valor_pendente
    consolidacao_motoristas.ativos.pendentes.sum(:valor_total)
  end

  def finalizar!(user)
    return false unless todas_entregas_classificadas?

    transaction do
      # Finalizar a consolidação fecha (consolida) os veículos que ainda estavam
      # abertos — mantém o estado por veículo coerente com o todo finalizado.
      consolidacao_veiculos.ativos.abertos.update_all(
        fechado_em: Time.current, fechado_por: user.id, updated_at: Time.current
      )
      update!(
        status:         :finalizada,
        finalizado_por: user.id,
        finalizado_em:  Time.current
      )
    end
  end

  # Só está completa quando CADA motorista ativo está completo (nada por
  # classificar, descontados os veículos arquivados). Fechar veículo é um passo
  # independente; ao finalizar, os veículos abertos são consolidados juntos.
  def todas_entregas_classificadas?
    consolidacao_motoristas.ativos.all? { |cm| motorista_completo?(cm.motorista_nome) }
  end

  # Completude de UM motorista: não sobra entrega elegível por classificar. Os
  # veículos ARQUIVADOS saem da conta (suas entregas deixam de ser exigidas),
  # permitindo finalizar deixando um carro problemático de fora. Fechar um veículo
  # é um passo independente (lock) — a finalização em si fecha os que restarem.
  def motorista_completo?(motorista)
    total = entregas_elegiveis_count(motorista) - eligible_veiculos_arquivados(motorista)
    total.zero? || classificadas_count(motorista) >= total
  end

  def percentual_classificado
    return 0 if total_entregas_estimadas.zero?
    ((consolidacao_entregas.count.to_f / total_entregas_estimadas) * 100).round
  end

  def total_entregas_estimadas
    @total_entregas_estimadas ||= consolidacao_motoristas.ativos.sum do |cm|
      entregas_elegiveis_count(cm.motorista_nome)
    end
  end

  # Entregas elegíveis para um motorista no período (atendidas: sucesso +
  # insucesso, pois o motorista foi ao local), respeitando o filtro de veículos
  # da consolidação (quando houver). Fonte única usada pelo wizard, pela revisão
  # e pelo gate de finalização.
  def entregas_elegiveis_count(motorista)
    Entrega.contar_atendidas(
      inicio:    data_inicio,
      fim:       data_fim,
      motorista: motorista,
      vehicle:   vehicle_ids.presence,
      operacoes: operacoes.presence
    )
  end

  # Elegíveis de UM veículo específico do motorista (respeita filtros da
  # consolidação). O bucket "(sem veículo)" casa as entregas com vehicle NULL/vazio.
  def entregas_elegiveis_count_veiculo(motorista, vehicle)
    rel = Entrega.atendidas.da_conta_gade
                 .no_periodo_checkout(data_inicio, data_fim)
                 .do_motorista(motorista)
    rel = rel.da_veiculo(vehicle_ids)  if vehicle_ids.present?
    rel = rel.da_operacoes(operacoes)  if operacoes.present?
    rel = vehicle == SEM_VEICULO ? rel.where(vehicle: [nil, '']) : rel.where(vehicle: vehicle)
    rel.count
  end

  # Total de elegíveis dos veículos ARQUIVADOS do motorista — descontado do gate
  # para que arquivar um carro problemático libere a finalização. Só toca a base
  # externa quando existe algum veículo arquivado (mantém os testes isolados).
  def eligible_veiculos_arquivados(motorista)
    arquivados = consolidacao_veiculos.arquivados.where(motorista_nome: motorista)
    return 0 if arquivados.empty?

    arquivados.sum { |cv| entregas_elegiveis_count_veiculo(motorista, cv.vehicle) }
  end

  # Rótulos amigáveis das operações vinculadas (para exibição).
  def operacoes_label
    Array(operacoes).map { |t| Operacao.label(t) }
  end

  # Entrega conta como classificada se tiver ≥1 pilar (distinct por tracking_id,
  # já que agora pode haver vários pilares por entrega).
  # Apontamentos manuais (manual: true) NÃO entram aqui: eles vivem fora do
  # conjunto elegível do período, então contá-los inflaria o progresso para
  # além de 100% (e quebraria o gate classificadas >= elegíveis).
  def classificadas_count(motorista)
    consolidacao_entregas.where(motorista_nome: motorista, manual: false)
                         .distinct.count(:tracking_id)
  end

  # Status por veículo do motorista: array de { vehicle, eligible, classificadas,
  # completo, fechado, arquivado }. `completo` = 100% classificado (pode consolidar);
  # `fechado` = veículo consolidado (🔒); `arquivado` = fora do gate. Une os veículos
  # com entregas elegíveis (rastreio) com os que só têm linha em consolidacao_veiculos
  # (arquivados / buckets manuais). Fonte única do filtro de carros e do wizard.
  def veiculos_do_motorista(motorista)
    return [] unless motorista.present?

    rel = Entrega.atendidas.da_conta_gade
                 .no_periodo_checkout(data_inicio, data_fim)
                 .do_motorista(motorista)
    rel = rel.da_veiculo(vehicle_ids)  if vehicle_ids.present?
    rel = rel.da_operacoes(operacoes)  if operacoes.present?

    por_veiculo = rel.pluck(:tracking_id, :vehicle).group_by { |(_id, v)| v.presence || SEM_VEICULO }
    classificadas = consolidacao_entregas.where(motorista_nome: motorista, manual: false)
                                         .distinct.pluck(:tracking_id).to_set
    estados = consolidacao_veiculos.where(motorista_nome: motorista).index_by(&:vehicle)

    (por_veiculo.keys + estados.keys).uniq.map do |veiculo|
      ids   = (por_veiculo[veiculo] || []).map(&:first)
      feito = ids.count { |id| classificadas.include?(id) }
      cv    = estados[veiculo]
      { vehicle: veiculo, eligible: ids.size, classificadas: feito,
        completo:  ids.size.positive? && feito >= ids.size,
        fechado:   cv&.fechado?   || false,
        arquivado: cv&.arquivado? || false }
    end.sort_by { |v| v[:vehicle] }
  end

  # Arquiva os veículos PENDENTES (com entregas elegíveis mas não 100% classificadas)
  # de todos os motoristas ativos, deixando-os de fora da finalização. Usado pelo
  # atalho "arquivar pendentes e finalizar" — os prontos/consolidados seguem no todo.
  # Retorna a lista "Motorista · Veículo" arquivada.
  def arquivar_veiculos_pendentes!(user)
    arquivados = []
    consolidacao_motoristas.ativos.each do |cm|
      veiculos_do_motorista(cm.motorista_nome).each do |v|
        next if v[:fechado] || v[:arquivado] || v[:completo] || v[:eligible].zero?

        cv = consolidacao_veiculos.find_or_create_by!(motorista_nome: cm.motorista_nome, vehicle: v[:vehicle])
        cv.arquivar!(user)
        arquivados << "#{cm.motorista_nome} · #{v[:vehicle]}"
      end
    end
    arquivados
  end

  # ── Apontamento manual ──────────────────────────────────────
  # Adiciona uma entrega "por fora" (NF que chegou depois) com um ou mais pilares
  # (tipos). O motorista vem da própria entrega e é incluído na consolidação se
  # ainda não estiver. Retorna o nome do motorista. Idempotente por (tracking, tipo).
  def adicionar_apontamento!(entrega, tipos, user)
    tipos_validos = Array(tipos).map(&:to_s).select { |t| ConsolidacaoEntrega.tipos.key?(t) }.uniq
    raise ArgumentError, 'Selecione ao menos uma classificação.' if tipos_validos.empty?

    motorista = entrega.driver.to_s.strip
    raise ArgumentError, 'Entrega sem motorista — não é possível apontar.' if motorista.blank?

    transaction do
      consolidacao_motoristas.find_or_create_by!(motorista_nome: motorista)

      tipos_validos.each do |tipo|
        ce = consolidacao_entregas.find_or_initialize_by(tracking_id: entrega.tracking_id, tipo: tipo)
        next if ce.persisted? # pilar já existe — mantém

        ce.assign_attributes(
          motorista_nome: motorista,
          valor_aplicado: ConsolidacaoEntrega.valor_para(tipo),
          manual:         true,
          vehicle:        entrega.try(:vehicle).presence, # snapshot do veículo da rastreio
          created_by:     user&.id
        )
        ce.save!
      end

      recalcular_motorista!(motorista)
    end

    motorista
  end

  # ── Apontamento 100% manual ─────────────────────────────────
  # Lança uma entrega à mão SEM consultar a base de rastreio. Caso de uso: um
  # motorista foi até o local mas a NF ficou registrada com outro motorista no
  # rastreio — então a busca por NF nunca o encontra, mas ele também recebe.
  # O motorista vem digitado (não de uma Entrega) e é incluído se ainda não
  # estiver. tracking_id é sintético (não há Entrega), e a NF/observação ficam
  # guardadas em nf_manual/obs_manual.
  def adicionar_apontamento_manual!(motorista:, tipos:, user:, nf: nil, obs: nil, valor_extra: nil, vehicle: nil)
    tipos_validos = Array(tipos).map(&:to_s).select { |t| ConsolidacaoEntrega.tipos.key?(t) }.uniq
    raise ArgumentError, 'Selecione ao menos uma classificação.' if tipos_validos.empty?

    nome = motorista.to_s.strip
    raise ArgumentError, 'Informe o motorista do apontamento.' if nome.blank?

    tracking = "MAN-#{SecureRandom.uuid}" # sintético: agrupa os pilares como UMA entrega

    transaction do
      consolidacao_motoristas.find_or_create_by!(motorista_nome: nome)

      tipos_validos.each do |tipo|
        valor = if tipo == 'extraordinaria' && valor_extra.present?
                  valor_extra
                else
                  ConsolidacaoEntrega.valor_para(tipo)
                end
        consolidacao_entregas.create!(
          tracking_id:    tracking,
          tipo:           tipo,
          motorista_nome: nome,
          valor_aplicado: valor,
          manual:         true,
          vehicle:        vehicle.presence, # veículo escolhido no lançamento (Q3)
          nf_manual:      nf.presence,
          obs_manual:     obs.presence,
          created_by:     user&.id
        )
      end

      recalcular_motorista!(nome)
    end

    nome
  end

  # ── Entrega de termo ────────────────────────────────────────
  # Lança um LOTE de N "entregas de termo" — entregas sem NF nem código de rastreio
  # para atrelar (só motorista + quantidade, a um preço fixo configurável). Fica
  # numa única linha (manual: true): quantidade guarda o N e valor_aplicado guarda
  # o total do lote, então recalcular_motorista! e demais somas seguem inalteradas.
  # `tipo` distingue termo normal ('termo') de termo especial ('termo_especial'),
  # cada um com seu preço configurável.
  def adicionar_termos!(motorista:, quantidade:, user:, vehicle: nil, tipo: 'termo')
    nome = motorista.to_s.strip
    raise ArgumentError, 'Informe o motorista do termo.' if nome.blank?

    tipo = tipo.to_s
    raise ArgumentError, 'Tipo de termo inválido.' unless %w[termo termo_especial].include?(tipo)

    qtd = quantidade.to_i
    raise ArgumentError, 'Informe uma quantidade válida de termos.' if qtd < 1

    transaction do
      consolidacao_motoristas.find_or_create_by!(motorista_nome: nome)

      consolidacao_entregas.create!(
        tracking_id:    "TERMO-#{SecureRandom.uuid}", # sintético: não há Entrega/NF
        tipo:           tipo,
        motorista_nome: nome,
        quantidade:     qtd,
        valor_aplicado: qtd * ConsolidacaoEntrega.valor_para(tipo), # total do lote
        manual:         true,
        vehicle:        vehicle.presence, # veículo escolhido no lançamento (Q3)
        created_by:     user&.id
      )

      recalcular_motorista!(nome)
    end

    nome
  end

  # Recalcula o valor de um motorista (soma dos pilares, desconto subtrai) e o
  # total da consolidação. Fonte única usada pelo wizard e pelos apontamentos.
  def recalcular_motorista!(nome)
    cm = consolidacao_motoristas.find_by(motorista_nome: nome)
    return unless cm

    anterior = cm.valor_total
    total = consolidacao_entregas.where(motorista_nome: nome).sum do |e|
      e.desconto? ? -e.valor_aplicado : e.valor_aplicado
    end
    cm.update_column(:valor_total, total)
    update_column(:valor_total, consolidacao_motoristas.ativos.sum(:valor_total))
    sincronizar_veiculos!(nome)

    # Funil ÚNICO de mudança de valor do motorista — todo caminho que altera
    # pilar, desconto ou lançamento termina aqui. O gatilho só vale depois do
    # fechamento: em rascunho o valor muda a cada clique do wizard e avisar ali
    # seria spam, não informação. Nunca levanta (ver Notificacao::Gatilhos).
    Notificacao::Gatilhos.valor_alterado(self, cm, anterior: anterior)
  end

  # Mantém as linhas de consolidacao_veiculos do motorista em dia a partir dos
  # pilares já classificados (100% DB — não toca a base externa). Cria/atualiza uma
  # linha por veículo presente nas classificações e remove as que ficaram sem
  # pilares e ainda não foram fechadas/arquivadas. Veículos elegíveis mas 100% sem
  # classificar não geram linha aqui — o gate já os pega por classificadas < total.
  def sincronizar_veiculos!(nome)
    entregas  = consolidacao_entregas.where(motorista_nome: nome)
    presentes = entregas.pluck(:vehicle).map { |v| v.presence || SEM_VEICULO }.uniq

    presentes.each do |veic|
      cv = consolidacao_veiculos.find_or_initialize_by(motorista_nome: nome, vehicle: veic)
      cv.save! # before_save :recalcular_valor atualiza o valor_total do veículo
    end

    consolidacao_veiculos.where(motorista_nome: nome)
                         .where(fechado_em: nil, arquivado_em: nil)
                         .where.not(vehicle: presentes)
                         .destroy_all
  end

  private

  def periodo_valido
    return unless data_inicio && data_fim
    errors.add(:data_fim, 'deve ser após a data de início') if data_fim < data_inicio
  end

  def recalcular_valor_total
    self.valor_total = consolidacao_motoristas.ativos.sum(:valor_total)
  end
end
