# frozen_string_literal: true

module DeviseTokenAuth::Concerns::SetUserByToken
  extend ActiveSupport::Concern
  include DeviseTokenAuth::Concerns::ResourceFinder

  included do
    before_action :set_request_start
    after_action :update_auth_header
  end

  protected

  # keep track of request duration
  def set_request_start
    @request_started_at = Time.zone.now
    @used_auth_by_token = true

    # initialize instance variables
    @client_id ||= nil
    @resource ||= nil
    @token ||= nil
    @is_batch_request ||= nil
  end

  def ensure_pristine_resource
    if @resource.changed?
      # Stash pending changes in the resource before reloading.
      changes = @resource.changes
      @resource.reload
    end
    yield
  ensure
    # Reapply pending changes
    @resource.assign_attributes(changes) if changes
  end

  # user auth
  def set_user_by_token(mapping = nil)
    # determine target authentication class
    rc = resource_class(mapping)

    # no default user defined
    return unless rc

    # gets the headers names, which was set in the initialize file
    uid_name = DeviseTokenAuth.headers_names[:'uid']
    access_token_name = DeviseTokenAuth.headers_names[:'access-token']
    client_name = DeviseTokenAuth.headers_names[:'client']

    # parse header for values necessary for authentication
    uid        = request.headers[uid_name] || params[uid_name]
    @token     ||= request.headers[access_token_name] || params[access_token_name]
    @client_id ||= request.headers[client_name] || params[client_name]

    # client_id isn't required, set to 'default' if absent
    @client_id ||= 'default'

    # check for an existing user, authenticated via warden/devise, if enabled
    if DeviseTokenAuth.enable_standard_devise_support
      devise_warden_user = warden.user(rc.to_s.underscore.to_sym)
      if devise_warden_user && devise_warden_user.tokens[@client_id].nil?
        @used_auth_by_token = false
        @resource = devise_warden_user
        # REVIEW: The following line _should_ be safe to remove;
        #  the generated token does not get used anywhere.
        # @resource.create_new_auth_token
      end
    end

    # user has already been found and authenticated
    return @resource if @resource && @resource.is_a?(rc)

    # ensure we clear the client_id
    unless @token
      @client_id = nil
      return
    end

    return false unless @token

    # mitigate timing attacks by finding by uid instead of auth token
    user = uid && rc.find_by(uid: uid)

    if user && user.valid_token?(@token, @client_id)
      # sign_in with bypass: true will be deprecated in the next version of Devise
      if respond_to?(:bypass_sign_in) && DeviseTokenAuth.bypass_sign_in
        bypass_sign_in(user, scope: :user)
      else
        sign_in(:user, user, store: false, event: :fetch, bypass: DeviseTokenAuth.bypass_sign_in)
      end
      return @resource = user
    else
      # zero all values previously set values
      @client_id = nil
      return @resource = nil
    end
  end

  def update_auth_header
    # cannot save object if model has invalid params

    return unless @resource && @client_id

    # Generate new client_id with existing authentication
    @client_id = nil unless @used_auth_by_token

    if @used_auth_by_token && !DeviseTokenAuth.change_headers_on_each_request
      # should not append auth header if @resource related token was
      # cleared by sign out in the meantime
      return if @resource.reload.tokens[@client_id].nil?

      auth_header = @resource.build_auth_header(@token, @client_id)

      # update the response header
      response.headers.merge!(auth_header)

    else
      unless @resource.reload.valid?
        @resource = resource_class.find(@resource.to_param) # errors remain after reload
        # if we left the model in a bad state, something is wrong in our app
        unless @resource.valid?
          raise DeviseTokenAuth::Errors::InvalidModel, "Cannot set auth token in invalid model. Errors: #{@resource.errors.full_messages}"
        end
      end
      refresh_headers
    end
  end

  private

  def refresh_headers
    ensure_pristine_resource do
      # Lock the user record during any auth_header updates to ensure
      # we don't have write contention from multiple threads
      @resource.with_lock do
        # should not append auth header if @resource related token was
        # cleared by sign out in the meantime
        return if @used_auth_by_token && @resource.tokens[@client_id].nil?

        # update the response header
        response.headers.merge!(auth_header_from_batch_request)
      end # end lock
    end # end ensure_pristine_resource
  end

  def is_batch_request?(user, client_id)
    !params[:unbatch] &&
      user.tokens[client_id] &&
      user.tokens[client_id]['updated_at'] &&
      Time.parse(user.tokens[client_id]['updated_at']) > @request_started_at - DeviseTokenAuth.batch_request_buffer_throttle
  end

  def auth_header_from_batch_request
    # determine batch request status after request processing, in case
    # another processes has updated it during that processing
    @is_batch_request = is_batch_request?(@resource, @client_id)

    auth_header = {}
    # extend expiration of batch buffer to account for the duration of
    # this request
    if @is_batch_request
      auth_header = @resource.extend_batch_buffer(@token, @client_id)

      # Do not return token for batch requests to avoid invalidated
      # tokens returned to the client in case of race conditions.
      # Use a blank string for the header to still be present and
      # being passed in a XHR response in case of
      # 304 Not Modified responses.
      auth_header[DeviseTokenAuth.headers_names[:"access-token"]] = ' '
      auth_header[DeviseTokenAuth.headers_names[:"expiry"]] = ' '
    else
      # update Authorization response header with new token
      auth_header = @resource.create_new_auth_token(@client_id)
    end
    auth_header
  end
end