require 'socket'
require 'forwardable'

begin
  require 'securerandom'
rescue LoadError
  nil
end

require 'rollbar/item/backtrace'
require 'rollbar/util'
require 'rollbar/encoding'
require 'rollbar/truncation'
require 'rollbar/json'
require 'rollbar/scrubbers/params'

module Rollbar
  # This class represents the payload to be sent to the API.
  # It contains the logic to build the payload, trucante it
  # and dump the JSON.
  class Item
    extend Forwardable

    attr_writer :payload

    attr_reader :level
    attr_reader :message
    attr_reader :exception
    attr_reader :extra

    attr_reader :configuration
    attr_reader :scope
    attr_reader :logger
    attr_reader :notifier

    attr_reader :context

    def_delegators :payload, :[]

    class << self
      def build_with(payload, options = {})
        new(options).tap do |item|
          item.payload = payload
        end
      end
    end

    def initialize(options)
      @level = options[:level]
      @message = options[:message]
      @exception = options[:exception]
      @extra = options[:extra]
      @configuration = options[:configuration]
      @logger = options[:logger]
      @scope = options[:scope]
      @payload = nil
      @notifier = options[:notifier]
      @context = options[:context]
    end

    def payload
      @payload ||= build
    end

    def build
      data = build_data
      self.payload = {
        'data' => data
      }

      enforce_valid_utf8
      transform
      payload
    end

    def build_data
      data = {
        :timestamp => Time.now.to_i,
        :environment => build_environment,
        :level => level,
        :language => 'ruby',
        :framework => configuration.framework,
        :server => server_data,
        :notifier => {
          :name => 'rollbar-gem',
          :version => VERSION,
          :configured_options => configured_options
        },
        :body => build_body
      }
      data[:project_package_paths] = configuration.project_gem_paths if configuration.project_gem_paths.any?
      data[:code_version] = configuration.code_version if configuration.code_version
      data[:uuid] = SecureRandom.uuid if defined?(SecureRandom) && SecureRandom.respond_to?(:uuid)

      Util.deep_merge(data, configuration.payload_options)
      Util.deep_merge(data, scope)

      # Our API doesn't allow null context values, so just delete
      # the key if value is nil.
      data.delete(:context) unless data[:context]

      data
    end

    def configured_options
      if Gem.loaded_specs['activesupport'] && Gem.loaded_specs['activesupport'].version < Gem::Version.new('4.1')
        # There are too many types that crash ActiveSupport JSON serialization, and not worth
        # the risk just to send this diagnostic object. In versions < 4.1, ActiveSupport hooks
        # Ruby's JSON.generate so deeply there's no workaround.
        'not serialized in ActiveSupport < 4.1'
      elsif configuration.use_async && !configuration.async_json_payload
        # The setting allows serialization to be performed by each handler,
        # and this usually means it is actually performed by ActiveSupport,
        # which cannot safely serialize this key.
        'not serialized when async_json_payload is not set'
      else
        scrub(configuration.configured_options.configured)
      end
    end

    def dump
      # Ensure all keys are strings since we can receive the payload inline or
      # from an async handler job, which can be serialized.
      stringified_payload = Util::Hash.deep_stringify_keys(payload)
      attempts = []
      result = Truncation.truncate(stringified_payload, attempts)

      return result unless Truncation.truncate?(result)

      handle_too_large_payload(stringified_payload, result, attempts)

      nil
    end

    def handle_too_large_payload(stringified_payload, final_payload, attempts)
      uuid = stringified_payload['data']['uuid']
      host = stringified_payload['data'].fetch('server', {})['host']

      original_error = {
        :message => message,
        :exception => exception,
        :configuration => configuration,
        :uuid => uuid,
        :host => host
      }

      notifier.send_failsafe(
        too_large_payload_string(attempts),
        nil,
        original_error
      )
      logger.error("[Rollbar] Payload too large to be sent for UUID #{uuid}: #{Rollbar::JSON.dump(payload)}")
    end

    def too_large_payload_string(attempts)
      'Could not send payload due to it being too large after truncating attempts. ' \
        "Original size: #{attempts.first} Attempts: #{attempts.join(', ')} Final size: #{attempts.last}"
    end

    def ignored?
      data = payload['data']

      return unless data[:person]

      person_id = data[:person][configuration.person_id_method.to_sym]
      configuration.ignored_person_ids.include?(person_id)
    end

    private

    def build_environment
      env = configuration.environment
      env = 'unspecified' if env.nil? || env.empty?

      env
    end

    def build_body
      exception ? build_backtrace_body : build_message_body
    end

    def build_backtrace_body
      backtrace = Backtrace.new(exception,
                                :message => message,
                                :extra => build_extra,
                                :configuration => configuration)

      backtrace.to_h
    end

    def build_extra
      merged_extra = Util.deep_merge(scrub(extra), scrub(error_context))

      if custom_data_method? && !Rollbar::Util.method_in_stack(:custom_data, __FILE__)
        Util.deep_merge(scrub(custom_data), merged_extra)
      else
        merged_extra.empty? ? nil : merged_extra # avoid putting an empty {} in the payload.
      end
    end

    def error_context
      exception.respond_to?(:rollbar_context) && exception.rollbar_context
    end

    def scrub(data)
      return data unless data.is_a? Hash

      options = {
        :params => data,
        :config => Rollbar.configuration.scrub_fields,
        :whitelist => Rollbar.configuration.scrub_whitelist
      }
      Rollbar::Scrubbers::Params.call(options)
    end

    def custom_data_method?
      !!configuration.custom_data_method
    end

    def custom_data
      data = if configuration.custom_data_method.arity == 3
               configuration.custom_data_method.call(message, exception, context)
             else
               configuration.custom_data_method.call
             end

      Rollbar::Util.deep_copy(data)
    rescue StandardError => e
      return {} if configuration.safely?

      report_custom_data_error(e)
    end

    def report_custom_data_error(e)
      data = notifier.safely.error(e)

      return {} unless data.is_a?(Hash) && data[:uuid]

      uuid_url = Util.uuid_rollbar_url(data, configuration)

      { :_error_in_custom_data_method => uuid_url }
    end

    def build_message_body
      extra = build_extra
      result = { :body => message || 'Empty message' }
      result[:extra] = extra if extra

      { :message => result }
    end

    def server_data
      data = {
        :host => configuration.host || Socket.gethostname
      }
      data[:root] = configuration.root.to_s if configuration.root
      data[:branch] = configuration.branch if configuration.branch
      data[:pid] = Process.pid

      data
    end

    def enforce_valid_utf8
      Util.enforce_valid_utf8(payload)
    end

    def transform
      handlers = configuration.transform

      handlers.each do |handler|
        begin
          handler.call(transform_options)
        rescue StandardError => e
          logger.error("[Rollbar] Error calling the `transform` hook: #{e}")

          break
        end
      end
    end

    def transform_options
      {
        :level => level,
        :scope => scope,
        :exception => exception,
        :message => message,
        :extra => extra,
        :payload => payload
      }
    end
  end
end
