require 'ably/auth'

module Ably
  module Realtime
    # Auth is responsible for authentication with {https://www.ably.io Ably} using basic or token authentication
    # This {Ably::Realtime::Auth Realtime::Auth} class wraps the {Ably::Auth Synchronous Ably::Auth} class in an EventMachine friendly way using Deferrables for all IO.  See {Ably::Auth Ably::Auth} for more information
    #
    # Find out more about Ably authentication at: https://www.ably.io/documentation/general/authentication/
    #
    # @!attribute [r] client_id
    #   (see Ably::Auth#client_id)
    # @!attribute [r] current_token_details
    #   (see Ably::Auth#current_token_details)
    # @!attribute [r] token
    #   (see Ably::Auth#token)
    # @!attribute [r] key
    #   (see Ably::Auth#key)
    # @!attribute [r] key_name
    #   (see Ably::Auth#key_name)
    # @!attribute [r] key_secret
    #   (see Ably::Auth#key_secret)
    # @!attribute [r] options
    #   (see Ably::Auth#options)
    # @!attribute [r] token_params
    #   (see Ably::Auth#options)
    # @!attribute [r] using_basic_auth?
    #   (see Ably::Auth#using_basic_auth?)
    # @!attribute [r] using_token_auth?
    #   (see Ably::Auth#using_token_auth?)
    # @!attribute [r] token_renewable?
    #   (see Ably::Auth#token_renewable?)
    # @!attribute [r] authentication_security_requirements_met?
    #   (see Ably::Auth#authentication_security_requirements_met?)
    #
    class Auth
      extend Forwardable
      include Ably::Modules::AsyncWrapper

      def_delegators :auth_sync, :client_id
      def_delegators :auth_sync, :token_client_id_allowed?, :configure_client_id, :client_id_validated?
      def_delegators :auth_sync, :can_assume_client_id?, :has_client_id?
      def_delegators :auth_sync, :current_token_details, :token
      def_delegators :auth_sync, :key, :key_name, :key_secret, :options, :auth_options, :token_params
      def_delegators :auth_sync, :using_basic_auth?, :using_token_auth?
      def_delegators :auth_sync, :token_renewable?, :authentication_security_requirements_met?
      def_delegators :client, :logger
      def_delegators :client, :connection

      def initialize(client)
        @client = client
        @auth_sync = client.rest_client.auth
      end

      # For new connections, ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary.
      # If a connection is already established, the connection will be upgraded with a new token
      #
      # In the event that a new token request is made, the provided options are used
      #
      # @param (see Ably::Auth#authorize)
      # @option (see Ably::Auth#authorize)
      #
      # @return [Ably::Util::SafeDeferrable]
      # @yield [Ably::Models::TokenDetails]
      #
      # @example
      #    # will issue a simple token request using basic auth
      #    client = Ably::Rest::Client.new(key: 'key.id:secret')
      #    client.auth.authorize do |token_details|
      #      token_details #=> Ably::Models::TokenDetails
      #    end
      #
      def authorize(token_params = nil, auth_options = nil, &success_callback)
        Ably::Util::SafeDeferrable.new(logger).tap do |authorize_method_deferrable|
          # Wrap the sync authorize method and wait for the result from the deferrable
          async_wrap do
            authorize_sync(token_params, auth_options)
          end.tap do |auth_operation|
            # Authorize operation succeeded and we have a new token, now let's perform inline authentication
            auth_operation.callback do |token|
              case connection.state.to_sym
              when :initialized, :disconnected, :suspended, :closed, :closing, :failed
                connection.connect
              when :connected
                perform_inline_auth token
              when :connecting
                # Fail all current connection attempts and try again with the new token, see #RTC8b
                connection.manager.release_and_establish_new_transport
              else
                logger.fatal { "Auth#authorize: unsupported state #{connection.state}" }
                authorize_method_deferrable.fail Ably::Exceptions::InvalidState.new("Unsupported state #{connection.state} for Auth#authorize")
                next
              end

              # Indicate success or failure based on response from realtime, see #RTC8b1
              auth_deferrable_resolved = false

              connection.unsafe_once(:connected, :update) do
                auth_deferrable_resolved = true
                authorize_method_deferrable.succeed token
              end
              connection.unsafe_once(:suspended, :closed, :failed) do |state_change|
                auth_deferrable_resolved = true
                authorize_method_deferrable.fail state_change.reason
              end
            end

            # Authorize failed, likely due to auth_url or auth_callback failing
            auth_operation.errback do |error|
              client.connection.transition_state_machine :failed, reason: error if error.kind_of?(Ably::Exceptions::IncompatibleClientId)
              authorize_method_deferrable.fail error
            end
          end

          # Call the block provided to this method upon success of this deferrable
          authorize_method_deferrable.callback do |token|
            yield token if block_given?
          end
        end
      end

      # @deprecated Use {#authorize} instead
      def authorise(*args, &block)
        logger.warn { "Auth#authorise is deprecated and will be removed in 1.0. Please use Auth#authorize instead" }
        authorize(*args, &block)
      end

      # Synchronous version of {#authorize}. See {Ably::Auth#authorize} for method definition
      # Please note that authorize_sync will however not upgrade the current connection's token as this requires
      # an synchronous operation to send the new authentication details to Ably over a realtime connection
      #
      # @param (see Ably::Auth#authorize)
      # @option (see Ably::Auth#authorize)
      # @return [Ably::Models::TokenDetails]
      #
      def authorize_sync(token_params = nil, auth_options = nil)
        @authorization_in_flight = true
        auth_sync.authorize(token_params, auth_options)
      ensure
        @authorization_in_flight = false
      end

      # @api private
      def authorization_in_flight?
        @authorization_in_flight
      end

      # @deprecated Use {#authorize_sync} instead
      def authorise_sync(*args)
        logger.warn { "Auth#authorise_sync is deprecated and will be removed in 1.0. Please use Auth#authorize_sync instead" }
        authorize_sync(*args)
      end

      # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests
      #
      # @param (see Ably::Auth#request_token)
      # @option (see Ably::Auth#request_token)
      #
      # @return [Ably::Util::SafeDeferrable]
      # @yield [Ably::Models::TokenDetails]
      #
      # @example
      #    # simple token request using basic auth
      #    client = Ably::Rest::Client.new(key: 'key.id:secret')
      #    client.auth.request_token do |token_details|
      #      token_details #=> Ably::Models::TokenDetails
      #    end
      #
      def request_token(token_params = {}, auth_options = {}, &success_callback)
        async_wrap(success_callback) do
          request_token_sync(token_params, auth_options)
        end
      end

      # Synchronous version of {#request_token}. See {Ably::Auth#request_token} for method definition
      # @param (see Ably::Auth#authorize)
      # @option (see Ably::Auth#authorize)
      # @return [Ably::Models::TokenDetails]
      #
      def request_token_sync(token_params = {}, auth_options = {})
        auth_sync.request_token(token_params, auth_options)
      end

      # Creates and signs a token request that can then subsequently be used by any client to request a token
      #
      # @param (see Ably::Auth#create_token_request)
      # @option (see Ably::Auth#create_token_request)
      #
      # @return [Ably::Util::SafeDeferrable]
      # @yield [Models::TokenRequest]
      #
      # @example
      #   client.auth.create_token_request({ ttl: 3600 }, id: 'asd.asd') do |token_request|
      #     token_request #=> Ably::Models::TokenRequest
      #   end
      def create_token_request(token_params = {}, auth_options = {}, &success_callback)
        async_wrap(success_callback) do
          create_token_request_sync(token_params, auth_options)
        end
      end

      # Synchronous version of {#create_token_request}. See {Ably::Auth#create_token_request} for method definition
      # @param (see Ably::Auth#authorize)
      # @option (see Ably::Auth#authorize)
      # @return [Ably::Models::TokenRequest]
      #
      def create_token_request_sync(token_params = {}, auth_options = {})
        auth_sync.create_token_request(token_params, auth_options)
      end

      # Auth header string used in HTTP requests to Ably
      # Will reauthorize implicitly if required and capable
      #
      # @return [Ably::Util::SafeDeferrable]
      # @yield [String] HTTP authentication value used in HTTP_AUTHORIZATION header
      #
      def auth_header(&success_callback)
        async_wrap(success_callback) do
          auth_header_sync
        end
      end

      # Synchronous version of {#auth_header}. See {Ably::Auth#auth_header} for method definition
      # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header
      #
      def auth_header_sync
        auth_sync.auth_header
      end

      # Auth params used in URI endpoint for Realtime connections
      # Will reauthorize implicitly if required and capable
      #
      # @return [Ably::Util::SafeDeferrable]
      # @yield [Hash] Auth params for a new Realtime connection
      #
      def auth_params(&success_callback)
        fail_callback = Proc.new do |error, deferrable|
          logger.error { "Failed to authenticate: #{error}" }
          if error.kind_of?(Ably::Exceptions::BaseAblyException)
            # Use base exception if it exists carrying forward the status codes
            deferrable.fail Ably::Exceptions::AuthenticationFailed.new(error.message, nil, nil, error)
          else
            deferrable.fail Ably::Exceptions::AuthenticationFailed.new(error.message, 500, 80019)
          end
        end
        async_wrap(success_callback, fail_callback) do
          auth_params_sync
        end
      end

      # Synchronous version of {#auth_params}. See {Ably::Auth#auth_params} for method definition
      # @return [Hash] Auth params for a new Realtime connection
      #
      def auth_params_sync
        auth_sync.auth_params
      end

      private
      # The synchronous Auth class instanced by the Rest client
      # @return [Ably::Auth]
      def auth_sync
        @auth_sync
      end

      def client
        @client
      end

      # Sends an AUTH ProtocolMessage on the existing connection triggering
      # an inline AUTH process, see #RTC8a
      def perform_inline_auth(token)
        logger.debug { "Performing inline AUTH with Ably using token #{token}" }
        connection.send_protocol_message(
          action: Ably::Models::ProtocolMessage::ACTION.Auth.to_i,
          auth: { access_token: token.token }
        )
      end
    end
  end
end