# app/controllers/admin/romaneios_controller.rb
#
# Romaneio "CONTROLE DE ENTREGA": importa o plano do dia, deixa o operador revisar a
# tabela com a prévia do PDF real ao lado, e imprime uma folha A4 paisagem por
# veículo.
#
# As bases externas (espelho de rastreio e gade_entregas_*) continuam SOMENTE
# LEITURA — o que gravamos são as tabelas `romaneio*`, nossas.
class Admin::RomaneiosController < ApplicationController
  include Auditavel

  before_action :carregar_romaneio, except: %i[index create importar_planilha]

  def index
    authorize Romaneio
    # includes: a listagem mostra a contagem de veículos de cada romaneio.
    @romaneios = Romaneio.recentes.includes(:romaneio_veiculos).limit(50)
    @operacoes_agrupadas = Operacao.agrupadas_por_mes
    @data_padrao = Date.current

    # Os planos para o operador ESCOLHER em vez de adivinhar a data.
    #
    # Sem período: os 5 mais recentes — a ferramenta é usada logo depois de
    # lançar uma operação, então o que ele quer está sempre entre os últimos, e
    # o caso comum fica em zero clique.
    # Com período: TODOS os planos da faixa. É assim que se alcança um plano
    # antigo sem saber o dia dele — e é o mesmo recorte da tela do SimpliRoute,
    # para o operador não ter que traduzir nada entre as duas.
    @plano_inicio = params[:plano_inicio].presence
    @plano_fim    = params[:plano_fim].presence
    @planos = SimpliRoute::Client.new.planos(limite: 5, inicio: @plano_inicio, fim: @plano_fim)
    @planos_filtrados = @plano_inicio.present? && @plano_fim.present?

    # O logo escolhido sumiu do disco? A tela precisa dizer — foi exatamente
    # assim que o PDF saiu com o logo antigo sem ninguém entender (28/08/2026).
    _, @logo_problema = Pdf::RomaneioPdf.logo_configurado
    # Nome ORIGINAL do logo escolhido (a descricao), para a tela mostrar o mesmo
    # que o operador enviou; nil = ainda usando o logo da GADE do repositório.
    @logo_atual = Configuracao.find_by(chave: 'romaneio_logo')&.descricao
  end

  def show
    authorize @romaneio
    @veiculos = @romaneio.romaneio_veiculos.ordenados.to_a
    @veiculo  = veiculo_selecionado
    @linhas   = @romaneio.linhas_vigentes.do_veiculo(@veiculo).to_a if @veiculo
    @removidas = @romaneio.linhas_removidas.to_a
    @colunas_ausentes = colunas_ausentes_da_operacao
    # Mesma lista da tela de criação: o vínculo manual da operação acontece aqui
    # também, porque é aqui que se descobre que APARELHO saiu vazio.
    @operacoes_agrupadas = Operacao.agrupadas_por_mes
    # A prévia aberta é ESTADO NA URL (?previa=1), não estado de navegador. Ver o
    # comentário em _previa.html.erb: hash (#previa) não sobrevive ao Turbo e
    # checkbox não sobrevive à troca de folha.
    @previa = params[:previa].present?
  end

  # Caminho A: busca o plano no SimpliRoute (API, com queda para o espelho).
  def create
    authorize Romaneio
    @romaneio = preparar_romaneio
    return if performed?

    # `@plano_escolhido` vem de #preparar_romaneio quando o operador clicou num
    # plano da lista; sem ele o caminho continua sendo por data.
    # Os DOIS: a data já foi resolvida em #preparar_romaneio (não paga a chamada
    # de novo) e ainda destrava a queda para o espelho local se a API cair no
    # meio; o plano é quem delimita as rotas.
    plano = Romaneios::PlanoDoDia.new(data: @romaneio.planned_date, plano: @plano_escolhido)
    importar(plano.linhas, origem: plano.origem)
  rescue SimpliRoute::Error => e
    # NotFound herda de Error: a mensagem dele já diz para enviar o .xlsx.
    descartar_romaneio_vazio
    redirect_to admin_romaneios_path, alert: e.message
  end

  # Caminho B: upload do .xlsx do plano, para quando o plano do dia ainda não foi
  # publicado ou a API não responde.
  def importar_planilha
    authorize Romaneio
    @romaneio = preparar_romaneio
    return if performed?

    arquivo = params[:arquivo]
    if arquivo.blank?
      descartar_romaneio_vazio
      return redirect_to(admin_romaneios_path, alert: 'Selecione a planilha do plano (.xlsx).')
    end

    importar(Romaneios::PlanoPlanilha.new(arquivo).linhas, origem: 'planilha')
  rescue Romaneios::PlanoPlanilha::Erro => e
    descartar_romaneio_vazio
    redirect_to admin_romaneios_path, alert: e.message
  end

  # ── Logo do cabeçalho ──────────────────────────────────────────────────────
  # O romaneio é documento do CLIENTE, não da Reem: quando a operação muda de
  # contratante, trocar o logo não pode exigir deploy.
  #
  # PNG/JPG apenas porque é o que o Prawn embute — um SVG aceito aqui sairia como
  # folha SEM logo, sem erro nenhum, e ninguém descobriria antes de imprimir.
  # ⚠️ O FORMATO É DECIDIDO PELO CONTEÚDO DO ARQUIVO, não pelo `content_type`
  # que o navegador manda. O content_type é entrada do usuário: Windows e alguns
  # navegadores mandam `application/octet-stream` para um PNG perfeitamente
  # válido, e a versão antiga (`LOGO_TIPOS[arquivo.content_type]`) RECUSAVA esse
  # arquivo dizendo "precisa ser PNG ou JPG" — para um PNG. Assinatura de arquivo
  # não mente.
  #
  # A lista é curta porque quem manda aqui é o Prawn: ele embute **PNG e JPEG**,
  # e só. Formato que ele não embute não pode ser aceito "para ver no que dá" —
  # viraria um PDF quebrado na hora de imprimir, ou um logo que some sem aviso.
  # Os outros formatos são RECONHECIDOS mesmo assim, para a mensagem poder dizer
  # o que a pessoa mandou e o que fazer, em vez de repetir "PNG ou JPG".
  ASSINATURAS = [
    ["\x89PNG\r\n\x1a\n".b, '.png', 'PNG',  true],
    ["\xFF\xD8\xFF".b,        '.jpg', 'JPEG', true],
    ['RIFF'.b,                  nil,    'WebP', false],   # confirmado abaixo pelo 'WEBP'
    ['GIF8'.b,                  nil,    'GIF',  false],
    ['BM'.b,                    nil,    'BMP',  false],
    ['%PDF'.b,                  nil,    'PDF',  false],
    ['<?xml'.b,                 nil,    'SVG',  false],
    ['<svg'.b,                  nil,    'SVG',  false]
  ].freeze
  LOGO_TAMANHO_MAX = 2.megabytes

  def atualizar_logo
    authorize Romaneio

    arquivo = params[:logo]
    return redirect_to(admin_romaneios_path, alert: 'Escolha o arquivo do logo (PNG ou JPG).') if arquivo.blank?

    if arquivo.size > LOGO_TAMANHO_MAX
      return redirect_to(admin_romaneios_path,
                         alert: 'Logo acima de 2 MB. Reduza a imagem antes de enviar.')
    end

    # Lê tudo UMA vez: o mesmo conteúdo serve para identificar o formato e para
    # gravar. Reler o upload depois de `read` devolveria vazio.
    conteudo = arquivo.read.to_s.b
    extensao, formato, aceito = identificar_imagem(conteudo)

    if extensao.nil?
      recado = if formato
                 "O arquivo é #{formato}, e o PDF só embute PNG ou JPG. " \
                   'Salve o logo como PNG (qualquer editor exporta) e envie de novo.'
               else
                 'Não reconheci esse arquivo como imagem. O logo precisa ser PNG ou JPG.'
               end
      return redirect_to(admin_romaneios_path, alert: recado)
    end
    # `aceito` existe para o dia em que alguém adicionar conversão: hoje só
    # chega aqui o que o Prawn embute.
    return redirect_to(admin_romaneios_path, alert: 'Formato não suportado.') unless aceito

    # Nome GERADO por nós: o nome que vem do navegador é entrada do usuário e não
    # entra em caminho de arquivo. O original fica só na descrição, para a tela
    # mostrar "Logo: LOGOGADE.png" como o operador reconhece.
    nome    = "logo-#{Time.current.strftime('%Y%m%d%H%M%S')}#{extensao}"
    destino = Pdf::RomaneioPdf.pasta_logos
    FileUtils.mkdir_p(destino)
    File.binwrite(destino.join(nome), conteudo)

    config = Configuracao.find_or_initialize_by(chave: 'romaneio_logo')
    config.update!(valor: nome, descricao: arquivo.original_filename.to_s.first(120),
                   updated_by: current_user&.id)

    redirect_to admin_romaneios_path,
                notice: "Logo atualizado: #{arquivo.original_filename}. Vale para os próximos PDFs."
  end

  # Rebusca o plano e reconcilia com o que já está editado (a regra está em
  # Romaneios::Importador).
  def reimportar
    authorize @romaneio

    # Reimportar com arquivo em mãos usa o arquivo; sem arquivo, volta ao
    # SimpliRoute — inclusive para um romaneio que nasceu de planilha (é justamente
    # assim que o operador troca o plano B pelo A quando a API volta).
    if params[:arquivo].present?
      plano  = Romaneios::PlanoPlanilha.new(params[:arquivo])
      linhas = plano.linhas
      origem = 'planilha'
    else
      plano  = Romaneios::PlanoDoDia.new(data: @romaneio.planned_date)
      linhas = plano.linhas
      origem = plano.origem
    end

    resultado = aplicar(linhas, origem: origem)
    auditar!(:reimportar, @romaneio, dados_novos: resultado.to_h)
    redirect_to admin_romaneio_path(@romaneio, veiculo: params[:veiculo]),
                notice: "Plano reimportado — #{resultado.resumo}."
  rescue SimpliRoute::Error, Romaneios::PlanoPlanilha::Erro => e
    redirect_to admin_romaneio_path(@romaneio, veiculo: params[:veiculo]), alert: e.message
  end

  # Vincula a operação do mês à mão e preenche APARELHO/TELEFONES/NOME a partir
  # dela. É a saída para quando o casamento pelo nome do plano não acontece — sem
  # isso o único caminho era reimportar, que depende da API responder ou de o
  # operador ainda ter o .xlsx do plano.
  def vincular_operacao
    authorize @romaneio

    resultado = Romaneios::VinculadorDeOperacao.new(romaneio: @romaneio,
                                                    tabela: params[:operacao_tabela]).aplicar!
    auditar!(:vincular_operacao, @romaneio,
             dados_novos: resultado.to_h.merge(operacao_tabela: @romaneio.operacao_tabela))

    # Sem a coluna STATUS a operação não sabe quem é NOVO: o vínculo deu certo e
    # mesmo assim ninguém recebe aparelho. Dizer isso aqui evita a leitura de que
    # "vinculei e continua vazio, logo o sistema está errado".
    aviso = if resultado.colunas_ausentes.include?('status')
              " Atenção: esta operação não tem a coluna STATUS, então APARELHO continua vazio e precisa ser preenchido à mão."
            else
              ''
            end

    redirect_to admin_romaneio_path(@romaneio, veiculo: params[:veiculo]),
                notice: "Operação #{@romaneio.operacao_label} vinculada — #{resultado.resumo}.#{aviso}"
  rescue ArgumentError
    redirect_to admin_romaneio_path(@romaneio, veiculo: params[:veiculo]),
                alert: 'Selecione uma operação válida.'
  rescue Romaneios::VinculadorDeOperacao::Conflito => e
    redirect_to admin_romaneio_path(@romaneio, veiculo: params[:veiculo]),
                alert: "#{e.message} Abra o romaneio de " \
                       "#{l e.existente.planned_date, format: '%d/%m/%Y'} (#{e.existente.rotulo_plano.presence || 'sem rótulo'}) " \
                       'em vez de duplicar o documento do dia.'
  end

  # Com ?veiculo= sai INLINE (é a prévia do editor, uma folha só, sub-100ms);
  # sem veiculo sai o romaneio inteiro como download.
  def pdf
    authorize @romaneio

    veiculo = params[:veiculo].to_s
    binario = Pdf::RomaneioPdf.new(romaneio: @romaneio,
                                   veiculos: veiculo.presence && [veiculo]).render

    # A prévia é regerada a cada edição: cache do navegador aqui devolveria o PDF
    # anterior e o operador acharia que a alteração não gravou.
    expires_now if veiculo.present?

    send_data binario, type: 'application/pdf',
                       disposition: veiculo.present? ? 'inline' : 'attachment',
                       filename: nome_do_arquivo(veiculo)
  end

  # JSON — grava UM campo de UMA parada. `valor: null` restaura o que o plano
  # trouxe (e destrava o campo para a próxima reimportação).
  def atualizar_linha
    authorize @romaneio

    linha = @romaneio.romaneio_linhas.find(params[:linha_id])
    campo = params[:campo].to_s
    unless RomaneioLinha::CAMPOS_EDITAVEIS.include?(campo)
      return render json: { ok: false, erro: 'Campo não editável.' }, status: :unprocessable_entity
    end

    if params[:valor].nil?
      linha[campo] = linha.valor_do_plano(campo)
      linha.desmarcar_editado!(campo)
    else
      linha[campo] = params[:valor].to_s
      linha.marcar_editado!(campo)
    end
    linha.save!

    # Devolvemos o valor GRAVADO e o JS o reescreve no input: a gravação é
    # last-write-wins, e assim uma sobreposição de duas abas fica visível em vez
    # de silenciosa.
    render json: { ok: true, valor: linha[campo], editado: linha.editado?(campo) }
  end

  def atualizar_veiculo
    authorize @romaneio

    registro = @romaneio.romaneio_veiculos.find_by!(veiculo: params[:veiculo].to_s)
    registro.update!(motorista: params[:motorista].to_s)
    render json: { ok: true, valor: registro.motorista }
  end

  def atualizar_rotulo
    authorize @romaneio

    @romaneio.update!(rotulo_plano: params[:rotulo_plano].to_s)
    render json: { ok: true, valor: @romaneio.rotulo_plano }
  end

  def destroy
    authorize @romaneio

    # Auditar ANTES: depois do destroy não há mais atributos para registrar.
    auditar!(:excluir, @romaneio, dados_anteriores: @romaneio.attributes)
    @romaneio.destroy!
    redirect_to admin_romaneios_path, notice: 'Romaneio excluído.'
  end

  private

  def carregar_romaneio
    @romaneio = Romaneio.find(params[:id])
  end

  # Quais colunas a operação NÃO tem. Perguntamos ao serviço em vez de deduzir de
  # "a coluna APARELHO saiu toda vazia": vazio também é o resultado legítimo de uma
  # operação só com pacientes recorrentes, e um aviso que erra o motivo é pior do
  # que nenhum.
  def colunas_ausentes_da_operacao
    return [] if @romaneio.operacao_tabela.blank?

    Romaneios::EnriquecimentoGade.new(@romaneio.operacao_tabela).colunas_ausentes
  rescue ArgumentError
    # Operação removida do banco depois que o romaneio foi criado.
    Romaneios::EnriquecimentoGade::COLUNAS.keys
  end

  # Acha (ou cria) o romaneio da data + operação. `find_or_create_by!` é o que faz
  # "importar de novo o mesmo dia" cair na reconciliação em vez de criar um segundo
  # romaneio — a chave única (planned_date, operacao_tabela) é a garantia disso.
  def preparar_romaneio
    # ── Plano escolhido na lista (caminho normal desde 28/08/2026) ──────────
    # O plano resolve de uma vez os três campos que antes eram digitados: a
    # DATA (perguntada à API, porque a janela do plano não a revela), o NOME do
    # plano (que ia colado à mão e é o que descobre a operação do mês) e as
    # rotas, que delimitam o romaneio quando dois planos caem no mesmo dia.
    @plano_escolhido = nil
    data = params[:planned_date].presence

    if params[:plano_id].present?
      @plano_escolhido = SimpliRoute::Client.new.plano(params[:plano_id])
      if @plano_escolhido.nil?
        redirect_to admin_romaneios_path,
                    alert: 'Plano não encontrado no SimpliRoute — recarregue a página e escolha de novo.'
        return nil
      end

      # A data REAL do plano, não a janela dele. Sem isso o romaneio nasceria
      # com `planned_date` errado e o "Reimportar" (que é por data) buscaria o
      # dia errado depois.
      resolvida = Romaneios::PlanoDoDia.new(plano: @plano_escolhido).data_resolvida
      if resolvida.nil?
        redirect_to admin_romaneios_path,
                    alert: "Não consegui descobrir o dia do plano \"#{@plano_escolhido['name']}\". " \
                           'Informe a data ou envie a planilha do plano (.xlsx).'
        return nil
      end
      data = resolvida.to_s
      params[:rotulo_plano] = @plano_escolhido['name'] if params[:rotulo_plano].blank?
    end

    if data.blank?
      redirect_to admin_romaneios_path, alert: 'Escolha um plano ou informe a data.'
      return nil
    end

    tabela = params[:operacao_tabela].to_s
    if tabela.present? && !Operacao.valida?(tabela)
      redirect_to admin_romaneios_path, alert: 'Selecione uma operação válida.'
      return nil
    end

    # Sem operação escolhida, tenta deduzi-la do NOME DO PLANO ("ENTREGAS EMAD
    # 09.2026" → gade_entregas_emad_set_2026).
    #
    # POR QUE ISSO É IMPORTANTE E NÃO CONVENIÊNCIA: é a operação que diz quem é
    # paciente NOVO. Importar sem ela produz um romaneio de aparência normal com a
    # coluna APARELHO vazia para TODOS — o erro só aparece no papel, com aparelho
    # não entregue. A operação não é campo opcional na prática; era só difícil de
    # acertar, porque o nome do plano e o nome da tabela nunca são iguais.
    #
    # Empate/nenhum casamento devolve nil e segue sem operação: vincular a errada
    # preencheria APARELHO e TELEFONE com dados de OUTRO mês, sem ninguém
    # desconfiar. A tela avisa quando ficou sem.
    @operacao_deduzida = nil
    if tabela.blank? && params[:rotulo_plano].present?
      tabela = Romaneios::CasadorDeOperacao.casar(params[:rotulo_plano]).to_s
      @operacao_deduzida = tabela.presence
    end

    data = Date.parse(data)
    Romaneio.find_or_create_by!(planned_date: data, operacao_tabela: tabela) do |r|
      r.rotulo_plano = params[:rotulo_plano].presence || Romaneio.rotulo_sugerido(tabela, data)
      r.importado_por = current_user
    end
  rescue Date::Error
    redirect_to admin_romaneios_path, alert: 'Data do plano inválida.'
    nil
  end

  # Formato pela ASSINATURA do arquivo. Devolve [extensao, nome_do_formato,
  # aceito?] — com nome preenchido mesmo para o que não serve, para a mensagem
  # poder dizer "é WebP" em vez de "não é PNG ou JPG".
  def identificar_imagem(conteudo)
    achado = ASSINATURAS.find { |assinatura, _, _, _| conteudo.start_with?(assinatura) }
    return [nil, nil, false] if achado.nil?

    _, extensao, formato, aceito = achado
    # RIFF é contêiner (WebP, WAV, AVI): o que diz que é WebP são os bytes 8..11.
    return [nil, nil, false] if formato == 'WebP' && conteudo[8, 4] != 'WEBP'

    [extensao, formato, aceito]
  end

  def importar(linhas, origem:)
    resultado = aplicar(linhas, origem: origem)
    auditar!(:criar, @romaneio, dados_novos: resultado.to_h)

    # Dizer que a operação foi deduzida, e qual: dedução silenciosa que acerta é
    # invisível, mas dedução silenciosa que erra é indefensável — o operador
    # precisa ver de qual mês vieram os dados de APARELHO.
    aviso = if @operacao_deduzida
              " Operação #{Operacao.label(@operacao_deduzida)} vinculada pelo nome do plano."
            else
              ''
            end

    redirect_to admin_romaneio_path(@romaneio),
                notice: "Plano importado — #{resultado.resumo}.#{aviso}"
  end

  def aplicar(linhas, origem:)
    Romaneios::Importador.new(romaneio: @romaneio, linhas: linhas,
                              usuario: current_user, origem: origem).aplicar!
  end

  # Um romaneio recém-criado que não chegou a receber nenhuma parada é lixo: some
  # da lista para o operador não achar que a importação funcionou.
  def descartar_romaneio_vazio
    @romaneio&.destroy! if @romaneio&.persisted? && @romaneio.romaneio_linhas.none?
  end

  def veiculo_selecionado
    pedido = params[:veiculo].to_s
    return pedido if @veiculos.any? { |v| v.veiculo == pedido }

    @veiculos.first&.veiculo
  end

  def nome_do_arquivo(veiculo)
    partes = ['romaneio', @romaneio.rotulo_plano.presence || @romaneio.planned_date.to_s, veiculo]
    "#{partes.compact_blank.join('_').parameterize(separator: '_')}.pdf"
  end
end
