# app/services/pdf/romaneio_pdf.rb
#
# "CONTROLE DE ENTREGA" — a folha que o motorista assina ao retirar a carga.
# Uma página A4 PAISAGEM por veículo, no layout que a GADE já recebe hoje
# (referência: Imagens para implantação/Romaneiro_EMAD_SETEMBRO_2026.pdf).
#
# NÃO herda de Pdf::BasePdf de propósito: aquela classe é retrato, chumba o
# cabeçalho preto/laranja da REEM e instala um rodapé cinza repetido em todas as
# páginas — os três errados aqui. Compartilham só Pdf::FonteUtf8.
#
# ── Por que desenho manual e não prawn-table ────────────────────────────────
# A geometria aqui é RÍGIDA: altura de linha exata, sem quebra de texto, fonte que
# encolhe por célula até caber. No prawn-table, `height` é PISO e não valor exato —
# uma célula que quebrasse em duas linhas empurraria a tabela além de bounds.height
# e o prawn-table dividiria em outra página, quebrando "uma folha por veículo" e
# dessincronizando o cabeçalho (é exatamente o bug latente do programa Python que
# originou esta tela: o contador de página avançava por PÁGINA, então um veículo de
# duas páginas carimbava o veículo errado na segunda e em todas as seguintes).
# O Prawn nativo resolve com `overflow: :shrink_to_fit, single_line: true` numa
# caixa de altura fixa: depois que o corpo trava em MIN_FONTE o texto CORTA, em vez
# de estourar a página.
module Pdf
  class RomaneioPdf
    include Pdf::FonteUtf8

    MM = 72 / 25.4

    MARGEM   = 10 * MM
    HEADER_H = 28 * MM   # faixa do cabeçalho = margem superior do documento
    FOOTER_H = 22 * MM   # bloco DECLARO + assinatura
    SPACER_H =  8 * MM   # respiro entre a tabela e o DECLARO

    # O ReportLab põe 6pt de topPadding no Frame do SimpleDocTemplate por padrão,
    # e o programa original nunca desconta isso. Medido contra o PDF de referência:
    # sem esses 6pt a tabela inteira sobe 9px (a 110 dpi) em relação ao documento
    # que a GADE conhece. O Prawn não tem esse padding, então ele entra à mão.
    PADDING_TOPO = 6

    # Máximo de paradas por folha. Junto com as 2 linhas em branco e a linha de
    # cabeçalho, é o que faz a tabela ocupar exatamente uma página.
    MAX_LINHAS    = 33
    LINHAS_BRANCO = 2
    TOTAL_LINHAS  = MAX_LINHAS + LINHAS_BRANCO + 1

    AZUL  = '1B3A6B'   # cabeçalho da tabela e títulos
    ZEBRA = 'EAF0FB'   # fundo das linhas alternadas
    GRADE = 'AAAAAA'

    # nil = "o resto da largura" (a coluna ENDEREÇO absorve a sobra)
    LARGURAS_MM = [8, 22, 50, nil, 22, 42].freeze
    CABECALHOS  = ['#', 'NOTA FISCAL', 'NOME', 'ENDEREÇO', 'APARELHO', 'TELEFONES'].freeze
    CENTRALIZADAS = [0, 1, 4].freeze   # #, NOTA FISCAL, APARELHO

    MIN_FONTE = 5

    # Espaçamentos do rodapé, CALIBRADOS contra o PDF de referência (render a 110 dpi
    # de Romaneiro_EMAD_SETEMBRO_2026.pdf, página 1). No original eles saem de três
    # tabelas do ReportLab com paddings próprios (TOPPADDING 4, 17 e 2 + o padding
    # padrão de 3 que ele não sobrescreve); reproduzir as tabelas só para chegar às
    # mesmas três linhas de texto não pagaria o custo — os números medidos, sim.
    RODAPE_TOPO           = 5.3   # da base da tabela (após o SPACER_H) até o DECLARO
    RODAPE_ATE_ASSINATURA = 31.5  # do DECLARO até a linha de assinatura
    RODAPE_ATE_ROTULO     = 17.2  # da linha de assinatura até "ASSINATURA DO MOTORISTA"
    RODAPE_ULTIMA_LINHA   = 9     # altura da linha de "ASSINATURA DO MOTORISTA" (7,5pt)

    DECLARACAO = 'DECLARO QUE RECEBI OS PRODUTOS PARA REALIZAR AS ENTREGAS NOS ENDEREÇOS ACIMA.'
    CAMPO_DATA = 'DATA: _______/_______/__________'
    CAMPO_MOT  = 'MOTORISTA: ______________________________'

    # `romaneio` só precisa responder a #rotulo_plano e #linhas_vigentes — assim o
    # arquivo carrega num container Ruby pelado (sem Rails) para a conferência de
    # layout em PNG, que é o único jeito de validar PDF neste repositório.
    # `veiculos: nil` = todos, na ordem do romaneio.
    def initialize(romaneio:, veiculos: nil, logo: nil)
      @romaneio = romaneio
      # Ruby puro (sem ActiveSupport) daqui para baixo: esta classe precisa
      # carregar num container sem Rails para a conferência de layout em PNG.
      lista     = Array(veiculos).map(&:to_s).reject(&:empty?)
      @veiculos = lista.empty? ? nil : lista
      @logo     = logo || self.class.logo_padrao

      @pdf = Prawn::Document.new(page_size: 'A4', page_layout: :landscape,
                                 margin: [HEADER_H, MARGEM, MARGEM, MARGEM])
      registrar_fonte_utf8(@pdf)

      altura_util  = @pdf.bounds.height - SPACER_H - FOOTER_H
      @linha_h     = altura_util / TOTAL_LINHAS
      @pad         = [1.0, (@linha_h - 9) / 2].max
      @fonte_dados = [6.0, @linha_h - 2 * @pad - 2].max
      @fonte_hdr   = [8.0, @fonte_dados + 0.5].min
      @larguras    = calcular_larguras

      # Quantas linhas (dados + branco) cabem numa folha SEM empurrar a última
      # linha do rodapé para fora da área útil.
      #
      # ⚠️ Não é decorativo: a conta do programa original (33 + 2 + 1 = 36 linhas)
      # ignora tanto o topPadding de 6pt do Frame do ReportLab quanto os 15% a mais
      # da linha de cabeçalho. Com 33 paradas de verdade o total estoura o orçamento
      # em ~8pt e o "ASSINATURA DO MOTORISTA" some da folha — sem erro nenhum, o
      # papel simplesmente sai sem o rótulo da assinatura. Medido aqui com 34
      # paradas; nunca disparou em produção porque o maior veículo real tem 31.
      altura_rodape = RODAPE_TOPO + RODAPE_ATE_ASSINATURA + RODAPE_ATE_ROTULO +
                      RODAPE_ULTIMA_LINHA
      sobra = @pdf.bounds.height - SPACER_H - altura_rodape - PADDING_TOPO - @linha_h * 1.15
      @max_linhas_folha = (sobra / @linha_h).floor
    end

    # O logo é ESCOLHÍVEL na tela (Configuracao 'romaneio_logo') porque o romaneio é
    # documento do CLIENTE: quando a operação muda de contratante, trocar o
    # cabeçalho não pode exigir deploy. Sem escolha, cai no logo da GADE que está
    # no repositório.
    def self.logo_padrao
      return File.expand_path('../../../public/logo-gade.png', __dir__) unless dentro_do_rails?

      logo_escolhido || Rails.root.join('public', 'logo-gade.png').to_s
    end

    # Pasta dos logos enviados pela tela. Fora de public/ de propósito: o que o
    # operador sobe não vira arquivo servido pela web sem passar por autorização.
    def self.pasta_logos
      Rails.root.join('storage', 'logos')
    end

    def self.dentro_do_rails?
      defined?(Rails) && Rails.respond_to?(:root) && !Rails.root.nil?
    end

    def self.logo_escolhido
      caminho, = logo_configurado
      caminho
    end

    # O logo escolhido E o motivo de ele não estar valendo — os dois juntos,
    # porque quem chama precisa saber a diferença entre "ninguém escolheu logo"
    # e "o logo que o operador subiu SUMIU do disco".
    #
    # ⚠️ ISTO ERA UM `File.exist?(x) ? x : nil` MUDO, e foi um bug real
    # (relatado em 28/08/2026): o operador subiu outro logo, a tela disse "Logo
    # atualizado", e o PDF continuou saindo com o logo antigo — porque o arquivo
    # não estava mais na pasta e o método caía no padrão sem dizer nada a
    # ninguém. Falha silenciosa em cima de documento que vai IMPRESSO para o
    # cliente é o pior tipo: ninguém procura o que não reclama.
    #
    # A causa mais provável do arquivo sumir: `storage/` é gitignorado
    # (`/storage/*`) e o deploy é cópia de pasta pelo share SMB do NAS — a cópia
    # leva junto a pasta vazia da máquina de quem publicou. Se acontecer de
    # novo, o log e a tela agora dizem.
    #
    # Devolve [caminho, problema]:
    #   [nil, nil]        -> ninguém escolheu logo (usa o da GADE, normal)
    #   [caminho, nil]    -> logo escolhido e presente
    #   [nil, :sumiu]     -> escolhido MAS o arquivo não está lá
    #   [nil, :sem_banco] -> sem Rails/tabela (o container pelado do layout)
    def self.logo_configurado
      nome = Configuracao.valor('romaneio_logo').to_s
      return [nil, nil] if nome.strip.empty?

      caminho = pasta_logos.join(nome).to_s
      return [caminho, nil] if File.exist?(caminho)

      if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
        Rails.logger.error("[romaneio] logo '#{nome}' configurado mas AUSENTE em #{pasta_logos} — " \
                           'o PDF saiu com o logo padrão')
      end
      [nil, :sumiu]
    rescue StandardError
      # Sem banco (a conferência de layout roda num container Ruby pelado) ou
      # tabela ainda não migrada: o PDF continua saindo, com o logo padrão.
      [nil, :sem_banco]
    end

    def render
      paginas = montar_paginas
      return @pdf.render if paginas.empty?

      paginas.each_with_index do |pagina, i|
        @pdf.start_new_page unless i.zero?
        desenhar_pagina(pagina)
      end

      @pdf.render
    end

    private

    # [{ veiculo:, folha:, total_folhas:, primeira_ordem:, linhas: [...] }, ...]
    #
    # Veículo com mais de MAX_LINHAS paradas vira N folhas. As alternativas eram
    # descartar a parada #34 (pacote sem papel nenhum) ou deixar vazar para a página
    # seguinte sob o cabeçalho do veículo ERRADO — corrupção silenciosa de um
    # documento assinado. Cada folha leva o próprio bloco de assinatura, e a
    # numeração continua entre folhas para bater com a ordem de parada no celular
    # do motorista.
    def montar_paginas
      agrupadas.flat_map do |veiculo, linhas|
        fatias = linhas.each_slice(MAX_LINHAS).to_a
        fatias.each_with_index.map do |fatia, i|
          { veiculo: veiculo, folha: i + 1, total_folhas: fatias.size,
            primeira_ordem: i * MAX_LINHAS + 1, linhas: fatia }
        end
      end
    end

    def agrupadas
      linhas = @romaneio.linhas_vigentes.to_a
      linhas = linhas.select { |l| @veiculos.include?(l.veiculo.to_s) } if @veiculos
      linhas.group_by { |l| l.veiculo.to_s }
    end

    def desenhar_pagina(pagina)
      desenhar_cabecalho(pagina)
      @pdf.move_down PADDING_TOPO
      desenhar_tabela(pagina)
      @pdf.move_down SPACER_H
      desenhar_rodape
    end

    # ── Cabeçalho ────────────────────────────────────────────────────────────
    # Desenhado no `canvas` (coordenadas absolutas da página): a faixa vive DENTRO
    # da margem superior, acima da área de conteúdo.
    def desenhar_cabecalho(pagina)
      titulo = "CONTROLE DE ENTREGA  —  #{pagina[:veiculo]}"
      titulo += " (FOLHA #{pagina[:folha]}/#{pagina[:total_folhas]})" if pagina[:total_folhas] > 1
      base_y = @pdf.bounds.absolute_top + HEADER_H - MARGEM - 11 * MM

      @pdf.canvas do
        if @logo && File.exist?(@logo.to_s)
          @pdf.image @logo, at: [MARGEM, @pdf.bounds.absolute_top - MARGEM],
                            fit: [50 * MM, 17 * MM]
        end

        # draw_text (e não text_box) porque o `at` dele é a BASELINE, igual ao
        # drawCentredString do ReportLab — text_box ancoraria pelo topo da caixa e
        # o título subiria ~4pt em relação ao documento que a GADE conhece.
        @pdf.fill_color AZUL
        largura_titulo = @pdf.width_of(titulo, size: 15, style: :bold)
        @pdf.draw_text titulo, size: 15, style: :bold,
                       at: [(@pdf.bounds.absolute_right - largura_titulo) / 2.0, base_y]

        desenhar_rotulo_plano(base_y)
        @pdf.fill_color '000000'
      end
    end

    # "PLANO:" em negrito azul + o rótulo em preto normal, ambos encostados na
    # direita. O rótulo é medido primeiro para o label parar exatamente antes dele.
    def desenhar_rotulo_plano(base_y)
      rotulo = @romaneio.rotulo_plano.to_s.strip
      return if rotulo.empty?

      direita = @pdf.bounds.absolute_right - MARGEM
      largura = @pdf.width_of(rotulo, size: 9)

      @pdf.fill_color '000000'
      @pdf.draw_text rotulo, at: [direita - largura, base_y], size: 9
      @pdf.fill_color AZUL
      @pdf.draw_text 'PLANO: ', at: [direita - largura - @pdf.width_of('PLANO: ', size: 9, style: :bold),
                                     base_y], size: 9, style: :bold
    end

    # ── Tabela ───────────────────────────────────────────────────────────────
    def desenhar_tabela(pagina)
      topo   = @pdf.cursor
      alt_h  = @linha_h * 1.15
      y      = topo

      desenhar_linha_cabecalho(y, alt_h)
      y -= alt_h

      pagina[:linhas].each_with_index do |linha, i|
        # No original o zebrado começa em EAF0FB: a primeira linha de DADOS é a
        # colorida. Inverter isso troca a paridade da tabela inteira.
        cor = i.even? ? ZEBRA : 'FFFFFF'
        desenhar_linha(y, cor, celulas(linha, pagina[:primeira_ordem] + i))
        y -= @linha_h
      end

      # As linhas em branco existem para o operador anotar uma parada à mão. Numa
      # folha cheia não há espaço para elas — e insistir empurraria o rodapé para
      # fora do papel. Então elas entram até onde couberem.
      brancas = [LINHAS_BRANCO, @max_linhas_folha - pagina[:linhas].size].min
      brancas = 0 if brancas.negative?

      brancas.times do |i|
        cor = (pagina[:linhas].size + i).even? ? ZEBRA : 'FFFFFF'
        desenhar_linha(y, cor, Array.new(6, ''))
        y -= @linha_h
      end

      desenhar_grade(topo, y, alt_h, pagina[:linhas].size + brancas)
      @pdf.move_cursor_to y
    end

    def celulas(linha, numero)
      [numero.to_s, linha.nota_fiscal.to_s, linha.nome.to_s,
       linha.endereco.to_s, linha.aparelho.to_s, linha.telefones.to_s]
    end

    def desenhar_linha_cabecalho(y, altura)
      @pdf.fill_color AZUL
      @pdf.fill_rectangle [0, y], @pdf.bounds.width, altura
      @pdf.fill_color 'FFFFFF'
      escrever_celulas(y, altura, CABECALHOS, size: @fonte_hdr, style: :bold, tudo_centro: true)
      @pdf.fill_color '000000'
    end

    def desenhar_linha(y, cor, valores)
      @pdf.fill_color cor
      @pdf.fill_rectangle [0, y], @pdf.bounds.width, @linha_h
      @pdf.fill_color '000000'
      escrever_celulas(y, @linha_h, valores, size: @fonte_dados)
    end

    def escrever_celulas(y, altura, valores, size:, style: :normal, tudo_centro: false)
      x = 0
      valores.each_with_index do |valor, i|
        largura = @larguras[i]
        texto   = valor.to_s
        unless texto.empty?
          alinhamento = tudo_centro || CENTRALIZADAS.include?(i) ? :center : :left
          @pdf.text_box texto, at: [x + 3, y], width: largura - 6, height: altura,
                               size: size, style: style, valign: :center,
                               align: alinhamento, overflow: :shrink_to_fit,
                               min_font_size: MIN_FONTE, single_line: true
        end
        x += largura
      end
    end

    def desenhar_grade(topo, base, altura_cabecalho, total_linhas)
      @pdf.line_width 0.4
      @pdf.stroke_color GRADE

      y = topo - altura_cabecalho
      (total_linhas + 1).times do
        @pdf.stroke_horizontal_line 0, @pdf.bounds.width, at: y
        y -= @linha_h
      end

      x = 0
      @larguras.each do |largura|
        @pdf.stroke_vertical_line base, topo, at: x
        x += largura
      end
      @pdf.stroke_vertical_line base, topo, at: @pdf.bounds.width

      @pdf.line_width 0.8
      @pdf.stroke_color '000000'
      @pdf.stroke_rectangle [0, topo], @pdf.bounds.width, topo - base
      @pdf.stroke_color '000000'
      @pdf.line_width 1
    end

    # ── Rodapé ───────────────────────────────────────────────────────────────
    # O nome do motorista NÃO entra: a linha "MOTORISTA: ____" é preenchida à mão
    # na retirada — é assim que o documento é assinado hoje.
    def desenhar_rodape
      col_l = @pdf.bounds.width * 0.62
      col_r = @pdf.bounds.width * 0.38

      y = @pdf.cursor - RODAPE_TOPO
      @pdf.text_box DECLARACAO, at: [0, y], width: col_l, size: 7.5, style: :bold,
                                overflow: :shrink_to_fit, single_line: true
      @pdf.text_box CAMPO_DATA, at: [col_l, y], width: col_r, size: 8, style: :bold,
                                align: :right, single_line: true

      y -= RODAPE_ATE_ASSINATURA
      @pdf.text_box '_' * 55, at: [0, y], width: col_l, size: 8, single_line: true
      @pdf.text_box CAMPO_MOT, at: [col_l, y], width: col_r, size: 8, style: :bold,
                               align: :right, single_line: true

      y -= RODAPE_ATE_ROTULO
      @pdf.text_box 'ASSINATURA DO MOTORISTA', at: [0, y], width: col_l,
                                               size: 7.5, style: :bold, single_line: true
    end

    # A coluna nil (ENDEREÇO) recebe o que sobrar da largura útil.
    def calcular_larguras
      fixas = LARGURAS_MM.compact.sum * MM
      resto = @pdf.bounds.width - fixas
      LARGURAS_MM.map { |mm| mm.nil? ? resto : mm * MM }
    end
  end
end
