# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  MAX_DIAS_GRAFICO = 92  # limita a granularidade diária do gráfico (≈ 1 trimestre)

  # Tipos lançados EM LOTE: uma linha só de consolidacao_entregas representa N
  # entregas (quantidade = N, valor_aplicado = N × preço — ver
  # Consolidacao#adicionar_termos!). Contá-los por tracking_id valeria 1.
  TIPOS_EM_LOTE = %w[termo termo_especial].freeze

  # Cor de cada tipo no donut "Composição do custo". Fica AQUI, junto de quem
  # monta @fin_por_tipo, para label, valor e cor viajarem no MESMO hash: a view
  # tinha uma lista fixa de 5 cores para os 7 tipos de
  # ConsolidacaoEntrega::TIPO_CORES e o Chart.js reciclava o array — "Entrega de
  # Termo" saía com a mesma laranja de "Entrega Normal", no mesmo gráfico.
  # `desconto` NÃO entra: ver o comentário em #carregar_dados_financeiro.
  CORES_TIPO = {
    'entrega_normal' => '#f97316',
    'retirada'       => '#9a3412',
    'bonus'          => '#fed7aa',
    'extraordinaria' => '#9333ea',
    'termo'          => '#2563eb',
    'termo_especial' => '#06b6d4'
  }.freeze

  def index
    # Motorista tem painel próprio — não enxerga o dashboard administrativo
    return redirect_to(motorista_dashboard_path) if current_user.motorista?

    authorize :dashboard, :ver?

    # Custo, pagamento e ticket médio são permissão à parte: um perfil de
    # acompanhamento vê a operação sem ver quanto ela custa. Antes o controller
    # usava skip_authorization e o papel `externo` enxergava tudo.
    @ver_financeiro = current_user.pode?('dashboard.financeiro')

    @periodo_inicio, @periodo_fim = periodo_selecionado
    carregar_dados_dashboard
  end

  # GET /dashboard/relatorio_financeiro?inicio=&fim=&operacoes[]=
  # Relatório financeiro do período (PDF) — mesmos números da tela.
  def relatorio_financeiro
    return redirect_to(motorista_dashboard_path) if current_user.motorista?

    authorize :dashboard, :financeiro?
    @ver_financeiro = true

    @periodo_inicio, @periodo_fim = periodo_selecionado
    carregar_dados_dashboard

    op_label = @operacao_filtro.present? ? @operacao_filtro.map { |t| Operacao.label(t) }.join(', ') : 'Todas'

    dados = {
      custo_total:          @fin_custo_total,
      ticket_medio:         @fin_ticket_medio,
      pago:                 @fin_pago,
      pendente:             @fin_pendente,
      por_operacao:         @fin_por_operacao,
      por_motorista:        @fin_por_motorista,
      por_tipo:             @fin_por_tipo,
      pagamentos_feitos:    @pagamentos_feitos,
      pagamentos_pendentes: @pagamentos_pendentes,
      pag_pago_valor:       @pag_pago_valor,
      pag_pago_qtd:         @pag_pago_qtd,
      pag_pend_valor:       @pag_pend_valor,
      pag_pend_qtd:         @pag_pend_qtd
    }

    pdf = Pdf::RelatorioFinanceiroPeriodoPdf.new(
      periodo_inicio: @periodo_inicio, periodo_fim: @periodo_fim,
      operacao_label: op_label, dados: dados
    )

    send_data pdf.render,
              filename:    "financeiro_#{@periodo_inicio.strftime('%Y%m%d')}_#{@periodo_fim.strftime('%Y%m%d')}.pdf",
              type:        'application/pdf',
              disposition: 'attachment'
  end

  # GET /dashboard/pendentes?inicio=&fim=&operacoes[]=
  # Quais são as entregas EM ABERTO do período — o card do dashboard mostrava só
  # o número e não havia tela nenhuma listando as linhas por trás dele.
  #
  # Em aberto = nem concluída nem falhada (Entrega.pendentes), pela data
  # PLANEJADA: é o único recorte sem checkout, então planned_date é o eixo
  # possível. Mesmo recorte do card, para o total bater.
  def pendentes
    return redirect_to(motorista_dashboard_path) if current_user.motorista?

    authorize :dashboard, :pendentes?

    @periodo_inicio, @periodo_fim = periodo_selecionado
    @operacoes_agrupadas = Operacao.agrupadas_por_mes
    @operacao_filtro     = Operacao.sanitizar(params[:operacoes])

    escopo = Entrega.da_conta_gade.pendentes.no_periodo(@periodo_inicio, @periodo_fim)
    escopo = escopo.da_operacoes(@operacao_filtro) if @operacao_filtro.present?

    @total_em_aberto = escopo.count
    begin
      @pagy, @em_aberto = pagy(escopo.order(planned_date: :desc), limit: 50)
    rescue Pagy::OverflowError
      @pagy, @em_aberto = pagy(escopo.order(planned_date: :desc), limit: 50, page: 1)
    end

    # Contexto de cada NF listada: em que operação ela está (se estiver) e
    # quantas visitas o rastreio tem para ela. As duas respostas juntas dizem por
    # que a entrega ficou em aberto — e por que o dashboard de Operações não a
    # mostra: ou é nota fora da operação, ou tem outra visita que a substituiu.
    nfs = @em_aberto.map { |e| e.reference_id.to_s }.reject(&:empty?)
    @operacao_por_nf = Operacao.por_notas(nfs)
    @visitas_por_nf  = nfs.any? ? Entrega.where(reference_id: nfs).group(:reference_id).count : {}
  end

  private

  # Faixa de datas vinda do filtro de calendário (params inicio/fim).
  # Default: do início do mês corrente até hoje. Datas inválidas caem no default
  # e a ordem é normalizada caso venham invertidas.
  def periodo_selecionado
    inicio = parse_data(params[:inicio]) || Date.current.beginning_of_month
    fim    = parse_data(params[:fim])    || Date.current
    inicio, fim = fim, inicio if fim < inicio
    [inicio, fim]
  end

  # Parse tolerante: data inválida/ausente vira nil (para o default assumir).
  def parse_data(str)
    return nil if str.blank?
    Date.parse(str)
  rescue ArgumentError, TypeError
    nil
  end

  def carregar_dados_dashboard
    # Filtro de operação (multi-seleção) — aplicado a todo o dashboard.
    @operacoes           = Operacao.todas
    @operacoes_agrupadas = Operacao.agrupadas_por_mes
    @operacao_filtro     = Operacao.sanitizar(params[:operacoes])

    # ATENDIDAS pela DATA REAL (checkout) — base de toda a análise financeira.
    #
    # Atendida = o motorista FOI ao local e fechou a visita, com sucesso
    # (`completed`) OU insucesso (`failed`). É exatamente o conjunto que a
    # consolidação considera elegível e paga (ver Entrega::STATUS_ATENDIDO,
    # Entrega.contar_atendidas e Consolidacao#veiculos_do_motorista), por isso o
    # dashboard usa o MESMO recorte — usava só `pagas` (concluídas) e mostrava
    # um valor menor que o do fechamento.
    atendidas = Entrega.da_conta_gade.atendidas.no_periodo_checkout(@periodo_inicio, @periodo_fim)
    atendidas = atendidas.da_operacoes(@operacao_filtro) if @operacao_filtro.present?

    # PENDENTES (em aberto, nem concluídas nem falhadas) pela data planejada —
    # é o único recorte sem checkout, então planned_date é o eixo correto aqui.
    pendentes = Entrega.da_conta_gade.pendentes.no_periodo(@periodo_inicio, @periodo_fim)
    pendentes = pendentes.da_operacoes(@operacao_filtro) if @operacao_filtro.present?

    # Quebra do total entre sucesso e insucesso (ambos já dentro de `atendidas`).
    # ⚠️ Falhada TEM checkout: o motorista foi ao local e fechou a visita com
    # motivo de insucesso. Por isso o eixo é checkout, igual às concluídas — antes
    # as falhas eram filtradas por planned_date e caíam no período errado.
    @entregas_pagas     = atendidas.concluidas.count
    @entregas_falhadas  = atendidas.falhadas.count
    @entregas_atendidas = @entregas_pagas + @entregas_falhadas
    @entregas_pendentes = pendentes.count
    @total_entregas     = @entregas_atendidas + @entregas_pendentes

    # Quantas NOTAS FISCAIS distintas estão por trás das visitas atendidas. É o
    # número que o dashboard de Operações mostra (e que o cliente paga/confere
    # nos documentos); aqui contamos VISITAS, porque é por ida ao local que o
    # motorista recebe. Exibir os dois lado a lado evita a leitura de que uma das
    # telas está errada quando elas divergem — a diferença são retentativas.
    @notas_atendidas = atendidas.distinct.count(:reference_id)

    # Configurações de preço
    config = Configuracao.mapa_de_precos

    # Valor estimado total
    @valor_estimado = (@entregas_atendidas * config[:entrega]).round(2)

    # Por motorista (todos)
    @motoristas = atendidas.group(:driver)
                           .count
                           .sort_by { |_, v| -v }
                           .map do |driver, qtd|
      {
        nome: driver,
        entregas: qtd,
        valor: (qtd * config[:entrega]).round(2)
      }
    end

    # Por local de entrega (contact_name)
    @por_operacao = atendidas.group(:contact_name)
                             .count
                             .sort_by { |_, v| -v }
                             .first(6)
                             .to_h

    # Evolução diária no período (para Chart.js) — por data real (checkout)
    @grafico_diario = build_grafico_diario(atendidas, config[:entrega])

    # Consolidações cujo período (data_inicio..data_fim) cruza o período do dashboard
    # — independe de quando foram criadas (created_at).
    @consolidacoes_mes = Consolidacao.ativas
                                     .where('data_inicio <= ? AND data_fim >= ?', @periodo_fim, @periodo_inicio)
    @consolidacoes_mes = @consolidacoes_mes.com_operacoes(@operacao_filtro) if @operacao_filtro.present?
    @consolidacoes_abertas = @consolidacoes_mes.where(status: :rascunho).count
    @consolidacoes_fechadas = @consolidacoes_mes.where(status: :finalizada).count

    # Pagamentos e visão financeira só são consultados por quem pode vê-los: sem a
    # permissão, o dado não chega à página (a view esconde os blocos, mas o certo
    # é nem carregar — é consulta cara e é informação de custo).
    if @ver_financeiro
      carregar_dados_pagamentos
      carregar_dados_financeiro
    end

    # Histórico estimado mais recente
    @historico_recente = HistoricoEstimado.order(created_at: :desc).limit(5)
  end

  def carregar_dados_pagamentos
    intervalo = @periodo_inicio.beginning_of_day..@periodo_fim.end_of_day

    # PAGOS: filtrados pela DATA DO PAGAMENTO (pago_em) dentro do período do topo
    # — "quanto foi pago neste período".
    pagos = ConsolidacaoMotorista.ativos
              .joins(:consolidacao)
              .where(consolidacoes: { status: :finalizada, deleted_at: nil })
              .where(pago_em: intervalo)
    if @operacao_filtro.present?
      pagos = pagos.where(consolidacao_id: Consolidacao.com_operacoes(@operacao_filtro).select(:id))
    end
    @pag_pago_qtd   = pagos.count
    @pag_pago_valor = pagos.sum(:valor_total)

    # PENDENTES: consolidações finalizadas cujo PERÍODO cruza o filtro, ainda sem
    # pagamento (pendente não tem data de pagamento para filtrar).
    finalizadas_ids = @consolidacoes_mes.where(status: :finalizada).select(:id)
    pendentes = ConsolidacaoMotorista.ativos.where(consolidacao_id: finalizadas_ids).pendentes
    @pag_pend_qtd   = pendentes.count
    @pag_pend_valor = pendentes.sum(:valor_total)

    @pagamentos_feitos    = pagos.includes(:consolidacao, :pagador).order(pago_em: :desc)
    @pagamentos_pendentes = pendentes.includes(:consolidacao).order(consolidacao_id: :desc)

    # Fluxo de pagamentos por dia (valor pago + acumulado) para o gráfico
    @grafico_pagamentos_dia = build_grafico_pagamentos(pagos)
  end

  # Visão financeira (custo) das consolidações finalizadas do período, já com o
  # filtro de operação aplicado.
  def carregar_dados_financeiro
    intervalo = @periodo_inicio.beginning_of_day..@periodo_fim.end_of_day

    base = Consolidacao.ativas.where(status: :finalizada)
    base = base.com_operacoes(@operacao_filtro) if @operacao_filtro.present?

    # Consolidações relevantes ao período: as que CRUZAM o período OU que tiveram
    # algum pagamento feito dentro do período. Sem o 2º caso, pagar uma
    # consolidação de outro mês não mexia nos gráficos (só na tabela de pagamentos).
    ids_periodo = base.where('data_inicio <= ? AND data_fim >= ?', @periodo_fim, @periodo_inicio).ids
    ids_pagos   = ConsolidacaoMotorista.ativos.where(pago_em: intervalo)
                                       .where(consolidacao_id: base.select(:id))
                                       .distinct.pluck(:consolidacao_id)

    consols = Consolidacao.where(id: (ids_periodo + ids_pagos).uniq).to_a
    ids = consols.map(&:id)
    cms = ConsolidacaoMotorista.ativos.where(consolidacao_id: ids)

    # ⚠️ O MESMO RECORTE DE `cms`, NO NÍVEL DA LINHA. Motorista arquivado sai do
    # dinheiro (ConsolidacaoMotorista.ativos), mas as linhas dele CONTINUAM em
    # consolidacao_entregas: `arquivar_motorista` é reversível e preserva tudo —
    # quem apaga é o `excluir_motorista`. Sem este EXISTS, o card "Custo total"
    # mostrava R$ de um conjunto e "N entregas classificadas" de outro, o ticket
    # médio dividia um pelo outro e o donut de composição não fechava com o total.
    #
    # O par é (consolidacao_id, motorista_nome), não só o nome: o mesmo motorista
    # pode estar ativo numa consolidação e arquivado em outra, ambas no período.
    ces = ConsolidacaoEntrega.where(consolidacao_id: ids).where(<<~SQL.squish)
      EXISTS (SELECT 1 FROM consolidacao_motoristas cm
               WHERE cm.consolidacao_id = consolidacao_entregas.consolidacao_id
                 AND cm.motorista_nome  = consolidacao_entregas.motorista_nome
                 AND cm.arquivado_em IS NULL)
    SQL

    # KPIs
    @fin_custo_total  = cms.sum(:valor_total)
    @fin_qtd_entregas = contar_entregas(ces)
    @fin_ticket_medio = @fin_qtd_entregas.zero? ? 0 : (@fin_custo_total / @fin_qtd_entregas)
    @fin_pago         = cms.pagos.sum(:valor_total)
    @fin_pendente     = cms.pendentes.sum(:valor_total)

    # Custo por operação — rateia o valor da consolidação igualmente entre suas
    # operações (mantém o total consistente; exato no caso de 1 operação).
    por_op = Hash.new(0)
    consols.each do |c|
      ops = c.operacoes.presence || ['(sem operação)']
      fatia = c.valor_total / ops.size
      ops.each { |o| por_op[o == '(sem operação)' ? o : Operacao.label(o)] += fatia }
    end
    @fin_por_operacao = por_op.sort_by { |_, v| -v }

    # Composição por tipo (normal/retirada/bônus/…)
    #
    # ⚠️ DESCONTO NÃO É FATIA. No dinheiro ele SUBTRAI (ConsolidacaoMotorista
    # #recalcular_valor e Consolidacao#recalcular_motorista! usam
    # `-valor_aplicado`). Como fatia positiva de um DONUT — que se lê como
    # "partes de um todo" — a soma das fatias ficava maior que o Custo total
    # mostrado logo acima, e a diferença era exatamente o desconto. Ele sai do
    # gráfico e vira a linha "já abatidos" embaixo: a divergência é legítima,
    # então é explicada no lugar onde ela aparece (diretriz 1).
    soma_tipo = ces.group(:tipo).sum(:valor_aplicado)
    @fin_descontos = soma_tipo['desconto'].to_f
    @fin_por_tipo = CORES_TIPO.filter_map do |tipo, cor|
      valor = soma_tipo[tipo].to_f
      next if valor.zero?
      { label: ConsolidacaoEntrega::TIPO_CORES.dig(tipo, :label) || tipo, valor: valor, cor: cor }
    end

    # Custo por motorista (todos)
    @fin_por_motorista = cms.group(:motorista_nome).sum(:valor_total)
                            .sort_by { |_, v| -v }

    # Ranking CONSOLIDADO (aba "Consolidado" do card Motoristas): o valor REAL
    # fechado por motorista + quantas entregas entraram nesse fechamento.
    #
    # ⚠️ A quantidade é DISTINCT tracking_id, NÃO a contagem de linhas: a mesma
    # entrega pode ter vários pilares (Normal + Bônus + Retirada = 3 linhas em
    # consolidacao_entregas), e contar linhas inflaria o número de entregas.
    #
    # Partimos de @fin_por_motorista (que vem de `cms`, já .ativos) e só buscamos
    # a quantidade: assim a aba lista exatamente os motoristas do fechamento —
    # arquivado não aparece — e a soma bate com o KPI "Custo total" do topo.
    qtd_por_motorista = contar_entregas_por_motorista(ces)

    @ranking_consolidado = @fin_por_motorista.map do |nome, valor|
      { nome: nome, valor: valor, entregas: qtd_por_motorista[nome].to_i }
    end
  end

  # Quantas ENTREGAS existem num conjunto de linhas de consolidacao_entregas.
  # São DUAS correções na mesma conta, e as duas precisam conviver:
  #
  #   • DISTINCT tracking_id — a MESMA entrega vira várias linhas quando tem mais
  #     de um pilar (Normal + Bônus + Retirada = 3 linhas). Contar linhas inflava
  #     o número de entregas. Esta parte já estava certa.
  #   • termo soma `quantidade` — um lote de termo é UMA linha com quantidade N
  #     (Consolidacao#adicionar_termos!), então por tracking_id um lote de 20
  #     valia 1: o custo entrava inteiro no numerador e 1 no denominador, e o
  #     ticket médio saía 20× maior. É a mesma unidade que
  #     Analytics::TotaisPorMotorista já usa ("um termo com quantidade 5 vale 5").
  def contar_entregas(escopo)
    escopo.where.not(tipo: TIPOS_EM_LOTE).distinct.count(:tracking_id) +
      escopo.where(tipo: TIPOS_EM_LOTE).sum(:quantidade)
  end

  # Mesma conta de #contar_entregas, quebrada por motorista — o ranking e o KPI
  # do topo têm que somar a mesma coisa, senão viram dois números na mesma tela.
  def contar_entregas_por_motorista(escopo)
    normais = escopo.where.not(tipo: TIPOS_EM_LOTE).group(:motorista_nome).distinct.count(:tracking_id)
    lotes   = escopo.where(tipo: TIPOS_EM_LOTE).group(:motorista_nome).sum(:quantidade)
    normais.merge(lotes) { |_nome, a, b| a + b }
  end

  # `atendidas` já vem filtrado por checkout no período (concluídas + falhadas).
  # Agrupa pela DATA REAL (DATE(checkout)) — DATE() sobre timestamp não faz
  # conversão de fuso.
  def build_grafico_diario(atendidas, preco_entrega)
    dias = (@periodo_inicio..@periodo_fim).to_a

    if dias.size <= MAX_DIAS_GRAFICO
      # Granularidade diária (períodos curtos)
      contagem = atendidas.group("DATE(checkout)").count.transform_keys(&:to_s)
      labels  = dias.map { |d| d.strftime('%d/%m') }
      valores = dias.map { |d| ((contagem[d.to_s] || 0) * preco_entrega).round(2) }
      qtds    = dias.map { |d| contagem[d.to_s] || 0 }
    else
      # Período longo: agrega por mês para o gráfico não ficar vazio nem poluído.
      contagem = atendidas.group("TO_CHAR(checkout, 'YYYY-MM')").count
      meses   = meses_no_periodo
      labels  = meses.map { |m| Date.strptime(m, '%Y-%m').strftime('%m/%Y') }
      valores = meses.map { |m| ((contagem[m] || 0) * preco_entrega).round(2) }
      qtds    = meses.map { |m| contagem[m] || 0 }
    end

    { labels: labels, valores: valores, qtds: qtds }
  end

  # Valor pago por dia (pela data do pagamento) + acumulado no período,
  # para o gráfico de fluxo de pagamentos.
  def build_grafico_pagamentos(pagos)
    dias = (@periodo_inicio..@periodo_fim).to_a

    if dias.size <= MAX_DIAS_GRAFICO
      soma   = pagos.group("DATE(pago_em)").sum(:valor_total).transform_keys(&:to_s)
      labels = dias.map { |d| d.strftime('%d/%m') }
      chaves = dias.map(&:to_s)
    else
      soma   = pagos.group("TO_CHAR(pago_em, 'YYYY-MM')").sum(:valor_total)
      meses  = meses_no_periodo
      labels = meses.map { |m| Date.strptime(m, '%Y-%m').strftime('%m/%Y') }
      chaves = meses
    end

    valores   = chaves.map { |k| soma[k].to_f.round(2) }
    acumulado = 0
    { labels: labels, valores: valores,
      acumulado: valores.map { |v| (acumulado += v).round(2) } }
  end

  # Meses ("YYYY-MM") do início ao fim do período selecionado, inclusive.
  def meses_no_periodo
    meses  = []
    cursor = @periodo_inicio.beginning_of_month
    while cursor <= @periodo_fim
      meses << cursor.strftime('%Y-%m')
      cursor = cursor.next_month
    end
    meses
  end
end
