# frozen_string_literal: true

# Notifica excepciones a una instancia de Gitlab, como incidencias
# nuevas o como comentarios a las incidencias pre-existentes.
class GitlabNotifierJob < ApplicationJob
  class GitlabNotifierError < StandardError; end

  include ExceptionNotifier::BacktraceCleaner

  # Variables que vamos a acceder luego
  attr_reader :exception, :options, :issue_data, :cached

  queue_as :low_priority

  # @param [Exception] la excepción lanzada
  # @param [Hash] opciones de ExceptionNotifier
  def perform(exception, **options)
    @exception = exception
    @options = fix_options options
    @issue_data = { count: 1 }
    # Necesitamos saber si el issue ya existía
    @cached = false
    @issue = {}

    # Traemos los datos desde la caché si existen, sino generamos un
    # issue nuevo e inicializamos la caché
    @issue_data = Rails.cache.fetch(cache_key) do
      @issue = client.new_issue confidential: true, title: title, description: description, issue_type: 'incident'
      @cached = true

      {
        count: 1,
        issue: @issue['iid'],
        user_agents: [user_agent].compact,
        params: [request&.filtered_parameters].compact,
        urls: [url].compact
      }
    end

    if @issue['iid'].blank? && issue_data[:issue].blank?
      Rails.cache.delete(cache_key)
      raise GitlabNotifierError, @issue.dig('message', 'title')&.join(', ')
    end

    # No seguimos actualizando si acabamos de generar el issue
    return if cached

    # Incrementar la cuenta de veces que ocurrió
    issue_data[:count] += 1
    # Guardar información útil
    issue_data[:urls] << url unless issue_data[:urls].include? url
    issue_data[:user_agents] << user_agent unless issue_data[:user_agents].include? user_agent

    # Editar el título para que incluya la cuenta de eventos
    client.edit_issue(iid: issue_data[:issue], title: title, state_event: 'reopen')

    # Agregar un comentario con la información posiblemente nueva
    client.new_note(iid: issue_data[:issue], body: body)

    # Guardar para después
    Rails.cache.write(cache_key, issue_data)
  # Si este trabajo genera una excepción va a entrar en un loop, así que
  # la notificamos por correo
  rescue StandardError => e
    email_notification.call(e, data: @issue)
    email_notification.call(exception, data: @options)
  end

  private

  # Notificar por correo
  #
  # @return [ExceptionNotifier::EmailNotifier]
  def email_notification
    @email_notification ||= ExceptionNotifier::EmailNotifier.new(email_prefix: '[ERROR] ', sender_address: ENV['DEFAULT_FROM'], exception_recipients: ENV['EXCEPTION_TO'])
  end

  # La llave en la cache tiene en cuenta la excepción, el mensaje, la
  # ruta del backtrace y los errores de JS
  #
  # @return [String]
  def cache_key
    @cache_key ||= [
      exception.class.name,
      Digest::SHA1.hexdigest(exception.message),
      Digest::SHA1.hexdigest(backtrace&.first.to_s),
      Digest::SHA1.hexdigest(errors.to_s)
    ].join('/')
  end

  # @return [Array]
  def errors
    options.dig(:data, :params, 'errors') || []
  end

  # Define si es una excepción de javascript o local
  #
  # @see BacktraceJob
  # @return [Boolean]
  def javascript?
    @javascript ||= options.dig(:data, :javascript_backtrace).present?
  end

  # Título
  #
  # @return [String]
  def title
    @title ||= ''.dup.tap do |t|
      t << "[#{exception.class}] " unless javascript?
      t << exception.message
      t << " [#{issue_data[:count]}]"
    end
  end

  # Descripción
  #
  # @return [String]
  def description
    @description ||= ''.dup.tap do |d|
      d << log_section
      d << request_section
      d << javascript_section
      d << javascript_footer
      d << backtrace_section
      d << data_section
    end
  end

  # Comentario
  #
  # @return [String]
  def body
    @body ||= ''.dup.tap do |b|
      b << log_section
      b << request_section
      b << javascript_footer
      b << data_section
    end
  end

  # Cadena de archivos donde se produjo el error
  #
  # @return [Array,Nil]
  def backtrace
    @backtrace ||= exception.backtrace ? clean_backtrace(exception) : nil
  end

  # Entorno del error
  #
  # @return [Hash]
  def env
    options[:env]
  end

  # Genera una petición a partir del entorno
  #
  # @return [ActionDispatch::Request]
  def request
    @request ||= ActionDispatch::Request.new(env) if env.present?
  end

  # Cliente de la API de Gitlab
  #
  # @return [GitlabApiClient]
  def client
    @client ||= GitlabApiClient.new
  end

  # @return [String]
  def log_section
    return '' unless options.dig(:data, :log)

    <<~LOG

    # Build log

    ```
    #{options[:data].delete(:log)}
    ```

    LOG
  end

  # Muestra información de la petición
  #
  # @return [String]
  def request_section
    return '' unless request

    <<~REQUEST

      # Request

      ```
      #{request.request_method} #{url}

      #{pp request.filtered_parameters}
      ```

    REQUEST
  end

  # Muestra información de JavaScript
  #
  # @return [String]
  def javascript_section
    return '' unless javascript?

    options.dig(:data, :params, 'errors')&.map do |error|
      # Algunos errores no son excepciones (?)
      error['type'] = 'undefined' if error['type'].blank?

      <<~JAVASCRIPT

        ## #{error['type']}: #{error['message']}

        ```
        #{Terminal::Table.new headings: error['backtrace'].first.keys, rows: error['backtrace'].map(&:values)}
        ```

      JAVASCRIPT
    end&.join
  end

  # Muestra información de la visita que generó el error en JS
  #
  # @return [String]
  def javascript_footer
    return '' unless javascript?

    <<~JAVASCRIPT

      #{user_agent}

      <#{url}>

    JAVASCRIPT
  end

  # Muestra el historial del error en Ruby
  #
  # @return [String]
  def backtrace_section
    return '' if javascript?
    return '' unless backtrace

    <<~BACKTRACE

      ## Backtrace

      ```
      #{backtrace.join("\n")}
      ```

    BACKTRACE
  end

  # Muestra datos extra de la visita
  #
  # @return [String]
  def data_section
    return '' unless options[:data]

    <<~DATA

      ## Data

      ```yaml
      #{options[:data].to_yaml}
      ```

    DATA
  end

  # Obtiene el UA de este error
  #
  # @return [String]
  def user_agent
    @user_agent ||= options.dig(:data, :params, 'context', 'userAgent') if javascript?
    @user_agent ||= request.headers['user-agent'] if request
    @user_agent
  end

  # Obtiene la URL actual
  #
  # @return [String]
  def url
    @url ||= request&.url || options.dig(:data, :params, 'context', 'url')
  end

  # Define llaves necesarias
  #
  # @param :options [Hash]
  # @return [Hash]
  def fix_options(options)
    options = { data: options } unless options.is_a? Hash
    options[:data] ||= {}
    options[:data][:params] ||= {}

    options
  end
end