#  Union Station - https://www.unionstationapp.com/
#  Copyright (c) 2010-2015 Phusion Holding B.V.
#
#  "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.

UnionStationHooks.require_lib 'utils'
UnionStationHooks.require_lib 'time_point'
UnionStationHooks.require_lib 'request_reporter'

module UnionStationHooks
  class << self
    # @note You do not have to call this! Passenger automatically calls
    #   this for you! Just obtain the RequestReporter object that has been made
    #   available for you.
    #
    # Indicates that a Rack request has begun. Given a Rack environment hash,
    # this method returns {RequestReporter} object, which you can use for
    # logging Union Station information about this request. This method should
    # be called as early as possible during a request, before any processing
    # has begun. Only after calling this method will it be possible to log
    # request-specific information to Union Station.
    #
    # The {RequestReporter} object that this method creates is also made
    # available through the `union_station_hooks` key in the Rack environment
    # hash, as well as the `:union_station_hooks` key in the current thread's
    # object:
    #
    #     env['union_station_hooks']
    #     # => RequestReporter object or nil
    #
    #     Thread.current[:union_station_hooks]
    #     # => RequestReporter object or nil
    #
    # If this method was already called on this Rack request, then this method
    # does nothing and merely returns the previously created {RequestReporter}.
    #
    # See {RequestReporter} to learn what kind of information you can log to
    # Union Station about Rack requests.
    #
    # @return [RequestReporter, nil] A {RequestReporter} object, or nil or
    #   because of certain error conditions. See the RequestReporter class
    #   description for when this may be nil.
    def begin_rack_request(rack_env)
      reporter = rack_env['union_station_hooks']
      return reporter if reporter

      # PASSENGER_TXN_ID may be nil here even if Union Station support is
      # enabled. For example, if the user tried to access the application
      # process directly through its private HTTP socket.
      txn_id = rack_env['PASSENGER_TXN_ID']
      return nil if !txn_id

      # Workaround for Ruby < 2.1 support where there is no function for querying
      # the monotonic time.
      delta_monotonic_env = rack_env['PASSENGER_DELTA_MONOTONIC']
      set_delta_monotonic(delta_monotonic_env.to_i) if delta_monotonic_env

      reporter = RequestReporter.new(context, txn_id, app_group_name, key)
      return if reporter.null?

      rack_env['union_station_hooks'] = reporter
      Thread.current[:union_station_hooks] = reporter
      reporter.log_request_begin
      reporter.log_gc_stats_on_request_begin
      reporter
    end

    # @note You do not have to call this! Passenger automatically calls
    #   this for you!
    #
    # Indicates that a Rack request, on which {begin_rack_request} was called,
    # has ended. You should call this method as late as possible during a
    # request, after all processing have ended. Preferably after the Rack
    # response body has closed.
    #
    # The {RequestReporter} object associated with this Rack request and with
    # the current, will be closed (by calling {RequestReporter#close}), which
    # finalizes the Union Station logs for this request.
    #
    # This method MUST be called in the same thread that called
    # {begin_rack_request}.
    #
    # It is undefined what will happen if you call this method a Rack request
    # on which {begin_rack_request} was not called, so don't do that.
    #
    # This method does nothing if it was already called on this Rack request.
    def end_rack_request(rack_env,
        uncaught_exception_raised_during_request = false)
      reporter = rack_env.delete('union_station_hooks')
      Thread.current[:union_station_hooks] = nil
      if reporter
        begin
          reporter.log_gc_stats_on_request_end
          reporter.log_request_end(uncaught_exception_raised_during_request)
        ensure
          reporter.close
        end
      end
    end

    # Logs an exception that did NOT occur during a request.
    #
    # This method should be used for logging exceptions outside the
    # request-response cycle, e.g. exceptions in threads. If you want to
    # log an exception that occurred during a request, use
    # {RequestReporter#log_exception} instead. That method will also log
    # any related request-specific information, while this method does not.
    #
    # @param [Exception] exception
    # @since 2.1.0
    def log_exception(exception)
      transaction = context.new_transaction(app_group_name, :exceptions, key)
      begin
        return do_nothing_on_null(:log_exception) if transaction.null?

        base64_message = exception.message
        base64_message = exception.to_s if base64_message.empty?
        base64_message = Utils.base64(base64_message)
        base64_backtrace = Utils.base64(exception.backtrace.join("\n"))

        transaction.message("Message: #{base64_message}")
        transaction.message("Class: #{exception.class.name}")
        transaction.message("Backtrace: #{base64_backtrace}")
      ensure
        transaction.close
      end
    end

    # Returns an opaque object (a {TimePoint}) that represents a collection
    # of metrics about the current time.
    #
    # This TimePoint samples monotonic time (with a fallback to wall clock
    # time) as well as CPU time, time spent in userspace and kernel space,
    # time spent context switching, etc. The exact information contained
    # in the object is operating system specific, hence the object is opaque.
    #
    # You should use it for the various API methods that require timing
    # information. Those methods also accept standard Ruby `Time` objects,
    # but we strongly recommended against doing this, because wall clock
    # time can jump forwards and backwards, which may create issues.
    #
    # See {RequestReporter#log_controller_action} for an example of
    # an API method which expects timing information.
    # `RequestReporter#log_controller_action` expects you to
    # provide timing information about a controller action. That timing
    # information is supposed to be obtained by calling
    # `UnionStationHooks.now`.
    #
    # @return [TimePoint]
    def now
      monotime_usec = Utils.monotime_usec_now
      pt = Utils.process_times
      TimePoint.new(monotime_usec, pt.utime, pt.stime)
    end

    # Returns a best-estimate delta (usec) between the wallclock and
    # the monotonic clock (updated every request), such that:
    # time_monotic_usec = time_wallclock_usec - delta
    def get_delta_monotonic
      @mono_mutex.synchronize { @delta_monotonic }
    end

  private

    # Although the de-facto state seems to be that reading/writing instance variables
    # is MT-safe, it's not guaranteed so better safe than sorry here.
    def set_delta_monotonic(val)
      @mono_mutex.synchronize { @delta_monotonic = val }
    end

    def create_context
      require_lib('context')
      @@context = Context.new(config[:ust_router_address],
        config[:ust_router_username] || 'logging',
        config[:ust_router_password],
        config[:node_name])
    end

    def install_postfork_hook
      if defined?(PhusionPassenger)
        PhusionPassenger.on_event(:starting_worker_process) do |forked|
          if forked
            UnionStationHooks.context.clear_connection
          end
        end
      end
    end

    def install_event_pre_hook
      preprocessor = @@config[:event_preprocessor]
      if preprocessor
        define_singleton_method(:call_event_pre_hook, &preprocessor)
      else
        def call_event_pre_hook(_event)
          # Do nothing
        end
      end
    end

    def initialize_other_union_station_hooks_gems
      @@initializers.each do |initializer|
        initializer.initialize!
      end
    end

    def finalize_install
      @@config.freeze
      @@initializers.freeze
      @@app_group_name = @@config[:app_group_name]
      @@key = @@config[:union_station_key]
      if @@config[:debug]
        UnionStationHooks::Log.debugging = true
      end
      require_simple_json
      @mono_mutex = Mutex.new
      @delta_monotonic = 0
      @@initialized = true
    end
  end
end
