module ApiAuthentication
  module ByAccessToken
    extend ActiveSupport::Concern

    def current_user
      @current_user ||= authenticated_token.try!(:owner) or defined?(super) && super
    end

    included do
      include ApiAuthentication::HttpAuthentication

      class_attribute :access_token_scope, instance_writer: false, instance_reader: false
      self.access_token_scope = []

      before_filter :verify_access_token_scope
      around_filter :enforce_access_token_permission
      rescue_from ApiAuthentication::ByAccessToken::Error,
                  with: :show_access_key_permission_error
    end

    module ConnectionExtension
      extend ActiveSupport::Concern

      included do
        prepend TransactionMethods
      end

      module TransactionMethods
        def begin_db_transaction
          transaction = ::ApiAuthentication::ByAccessToken::PermissionEnforcer.start_transaction
          transaction ? execute(transaction) : super
        end
      end
    end

    protected

    module ClassMethods
      def access_token_scopes(*scopes)
        validate_scopes!(scopes)

        self.access_token_scope = access_token_scope + scopes.flatten
      end

      def validate_scopes!(scopes)
        available_scopes = AccessToken::SCOPES.values
        invalid_scopes   = scopes.map(&:to_s) - available_scopes

        raise(ScopeError, "scopes #{invalid_scopes} do not exist") if invalid_scopes.any?
      end
    end

    def access_token_scope
      self.class.access_token_scope.map(&:to_s)
    end

    def show_access_key_permission_error
      self.response_body = nil # prevent double render errors
      render_error "Your access token does not have the correct permissions", status: 403
    end

    def authenticated_token
      return @authenticated_token if instance_variable_defined?(:@authenticated_token)
      @authenticated_token = domain_account.access_tokens.by_value(access_token) if access_token
    end

    def enforce_access_token_permission
      PermissionEnforcer.enforce(authenticated_token, &Proc.new)
    end

    def verify_access_token_scope
      return true unless params[:access_token]

      if !authenticated_token || (access_token_scope & allowed_scopes).blank?
        raise PermissionError
      end

      if (access_token_scope & authenticated_token.scopes).blank?
        raise ScopeError
      end

      true
    end

    Error = Class.new(StandardError)

    ScopeError = Class.new(Error)
    PermissionError = Class.new(Error)


    module PermissionEnforcer

      extend self

      READ_ONLY = 'ro'.freeze
      READ_WRITE = 'rw'.freeze

      def start_transaction
        case level
        when READ_ONLY then 'START TRANSACTION READ ONLY'
        when READ_WRITE then 'START TRANSACTION READ WRITE'
        end
      end

      class EnforceError < StandardError
      end

      def enforce(access_token)
        self.level = access_token.try!(:permission)

        return yield unless requires_transaction?

        if connection.transaction_open?
          raise "Can't use read-only Access Token with transactional fixtures" if Rails.env.test?

          error = EnforceError.new("couldn't open new transaction to enforce read-only access token")
          System::ErrorReporting.report_error(error)
        end

        connection.transaction(requires_new: true, &Proc.new)
      rescue ActiveRecord::StatementInvalid => error
        case error.message
        when /Cannot execute statement in a READ ONLY transaction/
            fail PermissionError, error.message, caller
        else raise
        end
      ensure
        self.level = nil
      end

      private

      def requires_transaction?
        case level
        when READ_ONLY then true
        when READ_WRITE then false
        end
      end

      THREAD_VARIABLE = :__permission_enforcer_level

      def level=(level)
        Rails.logger.info "PriorityEnforcer: level = #{level}"
        Thread.current[THREAD_VARIABLE] = level
      end

      def level
        Thread.current[THREAD_VARIABLE]
      end

      def connection
        ActiveRecord::Base.connection
      end
    end

    private

    def access_token
      @access_token ||= params.fetch(:access_token, &method(:http_authentication))
    end

    def allowed_scopes
      current_user.allowed_access_token_scopes.values
    end
  end
end
