# typed: strict
# frozen_string_literal: true

require_relative 'block_invocation_refinements'

using Workato::Extension::HashWithIndifferentAccess

module Workato
  module Connector
    module Sdk
      module SorbetTypes
        AcquireOutput = T.type_alias do
          T.any(
            # oauth2
            [
              HashWithIndifferentAccess, # tokens
              T.untyped, # resource_owner_id
              T.nilable(HashWithIndifferentAccess) # settings
            ],
            [
              HashWithIndifferentAccess, # tokens
              T.untyped # resource_owner_id
            ],
            [
              HashWithIndifferentAccess # tokens
            ],
            # custom_auth
            HashWithIndifferentAccess
          )
        end
      end

      class Connection
        extend T::Sig
        include MonitorMixin

        using BlockInvocationRefinements

        # @api private
        sig { returns(HashWithIndifferentAccess) }
        attr_reader :source

        class_attribute :on_settings_update, instance_predicate: false
        class_attribute :multi_auth_selected_fallback, instance_predicate: false

        sig do
          params(
            connection: SorbetTypes::SourceHash,
            methods: SorbetTypes::SourceHash,
            settings: SorbetTypes::SettingsHash
          ).void
        end
        def initialize(connection: {}, methods: {}, settings: {})
          super()
          @methods_source = T.let(HashWithIndifferentAccess.wrap(methods), HashWithIndifferentAccess)
          @source = T.let(HashWithIndifferentAccess.wrap(connection), HashWithIndifferentAccess)
          @settings = T.let(settings, SorbetTypes::SettingsHash)
        end

        # @api private
        sig { returns(SorbetTypes::SettingsHash) }
        def settings!
          @settings
        end

        # @api private
        sig { returns(HashWithIndifferentAccess) }
        def settings
          # we can't freeze or memoise because some developers modify it for storing something temporary in it.
          # always return a new copy
          synchronize do
            @settings.with_indifferent_access
          end
        end

        # @api private
        sig { params(settings: SorbetTypes::SettingsHash).returns(SorbetTypes::SettingsHash) }
        def merge_settings!(settings)
          @settings.merge!(settings)
        end

        # @api private
        sig { returns(T::Boolean) }
        def authorization?
          source[:authorization].present?
        end

        sig { returns(Authorization) }
        def authorization
          raise ::NotImplementedError, 'define authorization: before use' if source[:authorization].blank?

          @authorization = T.let(@authorization, T.nilable(Authorization))
          @authorization ||= Authorization.new(
            connection: self,
            authorization: source[:authorization],
            methods: methods_source
          )
        end

        sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
        def base_uri(settings = nil)
          return unless source[:base_uri]

          merge_settings!(settings) if settings
          global_dsl_context.execute(self.settings, &source['base_uri'])
        end

        # @api private
        sig do
          params(
            message: String,
            settings_before: SorbetTypes::SettingsHash,
            refresher: T.proc.returns(T.nilable(SorbetTypes::SettingsHash))
          ).returns(T::Boolean)
        end
        def update_settings!(message, settings_before, &refresher)
          updater = lambda do
            new_settings = refresher.call
            next unless new_settings

            settings.merge(new_settings)
          end

          synchronize do
            new_settings = if on_settings_update
                             T.must(on_settings_update).call(message, settings_before, updater)
                           else
                             updater.call
                           end
            return false unless new_settings

            merge_settings!(new_settings)

            true
          end
        end

        private

        sig { returns(HashWithIndifferentAccess) }
        attr_reader :methods_source

        sig { returns(Dsl::WithDsl) }
        def global_dsl_context
          Dsl::WithDsl.new(self)
        end

        class Authorization
          extend T::Sig

          sig do
            params(
              connection: Connection,
              authorization: HashWithIndifferentAccess,
              methods: HashWithIndifferentAccess
            ).void
          end
          def initialize(connection:, authorization:, methods:)
            @connection = T.let(connection, Connection)
            @connection_source = T.let(connection.source, HashWithIndifferentAccess)
            @source = T.let(authorization, HashWithIndifferentAccess)
            @methods_source = T.let(methods, HashWithIndifferentAccess)
          end

          sig { returns(String) }
          def type
            (source[:type].presence || 'none').to_s
          end

          sig { returns(T::Boolean) }
          def oauth2?
            !!(/oauth2/i =~ type)
          end

          sig { returns(T::Boolean) }
          def multi?
            @source[:type].to_s == 'multi'
          end

          sig { returns(T::Array[T.any(String, Symbol, Regexp, Integer)]) }
          def refresh_on
            Array.wrap(source[:refresh_on]).compact
          end

          sig { returns(T::Array[T.any(String, Symbol, Regexp, Integer)]) }
          def detect_on
            Array.wrap(source[:detect_on]).compact
          end

          sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
          def client_id(settings = nil)
            @connection.merge_settings!(settings) if settings
            client_id = source[:client_id]

            if client_id.is_a?(Proc)
              global_dsl_context.execute(@connection.settings, &client_id)
            else
              client_id
            end
          end

          sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
          def client_secret(settings = nil)
            @connection.merge_settings!(settings) if settings
            client_secret_source = source[:client_secret]

            if client_secret_source.is_a?(Proc)
              global_dsl_context.execute(@connection.settings, &client_secret_source)
            else
              client_secret_source
            end
          end

          sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
          def authorization_url(settings = nil)
            @connection.merge_settings!(settings) if settings
            return unless source[:authorization_url]

            global_dsl_context.execute(@connection.settings, &source[:authorization_url])
          end

          sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
          def token_url(settings = nil)
            @connection.merge_settings!(settings) if settings
            return unless source[:token_url]

            global_dsl_context.execute(@connection.settings, &source[:token_url])
          end

          sig do
            params(
              settings: T.nilable(SorbetTypes::SettingsHash),
              oauth2_code: T.nilable(String),
              redirect_url: T.nilable(String)
            ).returns(T.nilable(SorbetTypes::AcquireOutput))
          end
          def acquire(settings = nil, oauth2_code = nil, redirect_url = nil)
            @connection.merge_settings!(settings) if settings
            acquire_proc = source[:acquire]
            raise InvalidDefinitionError, "Expect 'acquire' block" unless acquire_proc

            Workato::Connector::Sdk::Operation.new(
              connection: Connection.new(
                connection: connection_source.merge(
                  authorization: source.merge(
                    apply: nil # only skip apply authorization for re-authorization request
                  )
                ),
                methods: methods_source,
                settings: @connection.settings!
              ),
              methods: methods_source
            ).execute(settings, { auth_code: oauth2_code, redirect_url: redirect_url }) do |connection, input|
              instance_exec(connection, input[:auth_code], input[:redirect_url], &acquire_proc)
            end
          end

          sig do
            params(
              http_code: T.nilable(Integer),
              http_body: T.nilable(String),
              exception: T.nilable(String)
            ).returns(T::Boolean)
          end
          def refresh?(http_code, http_body, exception)
            return false unless oauth2? || source[:acquire].present?

            refresh_on = self.refresh_on
            refresh_on.blank? || refresh_on.any? do |pattern|
              (pattern.is_a?(::Integer) && pattern == http_code) ||
                pattern === exception&.to_s ||
                pattern === http_body
            end
          end

          # @api private
          sig { params(settings: HashWithIndifferentAccess).returns(T.nilable(HashWithIndifferentAccess)) }
          def refresh!(settings)
            if oauth2?
              refresh_oauth2_token(settings)
            elsif source[:acquire].present?
              T.cast(acquire(settings), T.nilable(HashWithIndifferentAccess))
            end
          end

          sig do
            params(
              settings: T.nilable(SorbetTypes::SettingsHash),
              refresh_token: T.nilable(String)
            ).returns(
              T.any([HashWithIndifferentAccess, T.nilable(String)], HashWithIndifferentAccess)
            )
          end
          def refresh(settings = nil, refresh_token = nil)
            @connection.merge_settings!(settings) if settings
            refresh_proc = source[:refresh]
            raise InvalidDefinitionError, "Expect 'refresh' block" unless refresh_proc

            Workato::Connector::Sdk::Operation.new(
              connection: Connection.new(
                methods: methods_source,
                settings: @connection.settings!
              ),
              methods: methods_source
            ).execute(settings, { refresh_token: refresh_token }) do |connection, input|
              instance_exec(connection, input[:refresh_token], &refresh_proc)
            end
          end

          # @api private
          sig { returns(HashWithIndifferentAccess) }
          def source
            return @source unless multi?

            unless @source[:selected]
              raise InvalidMultiAuthDefinition, "Multi-auth connection must define 'selected' block"
            end

            if @source[:options].blank?
              raise InvalidMultiAuthDefinition, "Multi-auth connection must define 'options' list"
            end

            selected_auth_key = @source[:selected].call(@connection.settings)
            selected_auth_key ||= @connection.multi_auth_selected_fallback&.call(@source[:options])
            selected_auth_value = @source.dig(:options, selected_auth_key)

            raise UnresolvedMultiAuthOptionError, selected_auth_key unless selected_auth_value

            selected_auth_value
          end

          private

          sig { returns(HashWithIndifferentAccess) }
          attr_reader :connection_source

          sig { returns(HashWithIndifferentAccess) }
          attr_reader :methods_source

          sig { params(settings: HashWithIndifferentAccess).returns(HashWithIndifferentAccess) }
          def refresh_oauth2_token(settings)
            if source[:refresh].present?
              refresh_oauth2_token_using_refresh(settings)
            elsif source[:token_url].present?
              refresh_oauth2_token_using_token_url(settings)
            else
              raise InvalidDefinitionError, "'refresh' block or 'token_url' is required for refreshing the token"
            end
          end

          sig { params(settings: HashWithIndifferentAccess).returns(HashWithIndifferentAccess) }
          def refresh_oauth2_token_using_refresh(settings)
            new_tokens, new_settings = refresh(settings, settings[:refresh_token])
            new_tokens = HashWithIndifferentAccess.wrap(new_tokens)
            return new_tokens unless new_settings

            new_tokens.merge(new_settings)
          end

          sig { params(settings: HashWithIndifferentAccess).returns(HashWithIndifferentAccess) }
          def refresh_oauth2_token_using_token_url(settings)
            if settings[:refresh_token].blank?
              raise NotImplementedError, 'refresh_token is empty. ' \
                                         'Use workato oauth2 command to acquire access_token and refresh_token'
            end

            response = RestClient::Request.execute(
              url: token_url(settings),
              method: :post,
              payload: {
                client_id: client_id(settings),
                client_secret: client_secret(settings),
                grant_type: :refresh_token,
                refresh_token: settings[:refresh_token]
              },
              headers: {
                accept: :json
              }
            )
            tokens = JSON.parse(response.body)
            {
              access_token: tokens['access_token'],
              refresh_token: tokens['refresh_token'].presence || settings[:refresh_token]
            }.with_indifferent_access
          end

          sig { returns(Dsl::WithDsl) }
          def global_dsl_context
            Dsl::WithDsl.new(@connection)
          end
        end
      end
    end
  end
end