# TODO:
# a mechanism to iterate through errors in reverse
# async logging should queue, if dupe stack traces are found in batch error should be merged into prev one

class ErrorLog

  @lock = Mutex.new

  def self.filename
    "#{Rails.root}/log/#{Rails.env}_errors.log"
  end

  def self.clear!(guid)
    raise NotImplementedError
  end

  def self.clear_all!()
    File.delete(ErrorLog.filename) if File.exists?(ErrorLog.filename)
  end

  def self.report_async!(exception, controller, request, user)
    Thread.new do
      report!(exception, controller, request, user)
    end
  end

  def self.report!(exception, controller, request, user)
    add_row!(
      date: DateTime.now,
      guid: SecureRandom.uuid,
      user_id: user && user.id,
      request: filter_sensitive_post_data_parameters(controller, request.parameters).inspect,
      action: controller.action_name,
      controller: controller.controller_name,
      backtrace: sanitize_backtrace(exception.backtrace).join("\n"),
      message: exception.message,
      url: "#{request.protocol}#{request.env["HTTP_X_FORWARDED_HOST"] || request.env["HTTP_HOST"]}#{request.fullpath}",
      exception_class: exception.class.to_s
    )
  end

  def self.add_row!(hash)
    data = hash.to_xml(skip_instruct: true)
    # use background thread to write the log cause it may block if it gets backed up
    @lock.synchronize do
      File.open(filename, "a") do |f|
        f.flock(File::LOCK_EX)
        f.write(data)
        f.close
      end
    end
  end


  def self.each(&blk)
    skip(0, &blk)
  end

  def self.skip(skip=0)
    pos = 0
    return [] unless File.exists?(filename)

    loop do
      lines = ""
      File.open(self.filename, "r") do |f|
        f.flock(File::LOCK_SH)
        f.pos = pos
        while !f.eof?
          line = f.readline
          lines << line
          break if line.starts_with? "</hash>"
        end
        pos = f.pos
      end
      if lines != "" && skip == 0
        h = {}
        e = Nokogiri.parse(lines).children[0]
        e.children.each do |inner|
          h[inner.name] = inner.text
        end
        yield h
      end
      skip-=1 if skip > 0
      break if lines == ""
    end
  end

  private

  def self.sanitize_backtrace(trace)
    re = Regexp.new(/^#{Regexp.escape(Rails.root.to_s)}/)
    trace.map { |line| Pathname.new(line.gsub(re, "[RAILS_ROOT]")).cleanpath.to_s }
  end

  def self.exclude_raw_post_parameters?(controller)
    controller && controller.respond_to?(:filter_parameters)
  end

  def self.filter_sensitive_post_data_parameters(controller, parameters)
    exclude_raw_post_parameters?(controller) ? controller.__send__(:filter_parameters, parameters) : parameters
  end

  def self.filter_sensitive_post_data_from_env(env_key, env_value, controller)
    return env_value unless exclude_raw_post_parameters?
    return PARAM_FILTER_REPLACEMENT if (env_key =~ /RAW_POST_DATA/i)
    return controller.__send__(:filter_parameters, {env_key => env_value}).values[0]
  end
end
