require_dependency 'authentication/strategy'

module Authentication
  module Strategy
    class Oauth2 < Authentication::Strategy::Internal

      class MissingAuthenticationProvider < StandardError; end

      attr_reader :user, :user_data

      class Procedure
        attr_reader :strategy, :users, :params, :user_data

        delegate :authentication_provider, to: :strategy

        def initialize(strategy, users, params, user_data)
          @strategy  = strategy
          @users     = users
          @params    = params
          @user_data = user_data
        end

        private

        def uid
          user_data[:uid]
        end
      end

      class FindOrCreateAccount < Procedure

        def call
          find_user || create_account(session)
        end

        private

        def find_user
          Users::FindOauth2UserService.run(user_data, authentication_provider, users).tap do |user|
            strategy.migrate_sso_authorization(user, uid) if user
          end
        end

        def create_account(session)
          signup_user    = nil
          signup_buyer   = nil
          user_params    = user_data.except(:org_name)
          account_params = user_data.slice(:org_name)

          Account.transaction do
            signup_service = SignupService.new(
              provider:       site_account,
              plans:          [],
              session:        session,
              account_params: account_params,
              user_params:    user_params)

            signup_service.create do |buyer, user|
              signup_user  = user
              signup_buyer = buyer

              strategy.migrate_sso_authorization(signup_user, uid)
            end

            if signup_user.valid? && signup_buyer.valid? && user_data[:email_confirmed]
              signup_user.activate
            end
          end

          if signup_user.try(:persisted?)
            strategy.instance_variable_set(:@new_user_created, true)

            signup_user
          end
        end

        def session
          @session ||= params.fetch(:request).session
        end

        def site_account
          @site_account ||= users.proxy_association.owner
        end
      end

      class CreateInvitedUser < Procedure

        def call
          user = invitation.make_user(user_data)
          strategy.migrate_sso_authorization(user, uid)

          if user.save
            user.activate

            user
          end
        end

        private

        def invitation
          params[:invitation]
        end
      end

      def authenticate(params, procedure: FindOrCreateAccount)
        find_authentication_provider(params[:system_name])

        return super(params) unless authentication_provider_exist?

        client = ThreeScale::OAuth2::Client.build(authentication_provider)

        @user_data     = client.authenticate!(params[:code], params[:request])
        @error_message = @user_data[:error].presence

        @user ||= procedure.new(self, users, params, @user_data).call or return false

        if @user.can_login?
          @user
        else
          @error_message = inactive_user_message
          false
        end
      rescue Faraday::SSLError
        @error_message = I18n.t('errors.messages.oauth_invalid_certificate')
        false
      end

      def signup_path(params)
        super(params.except(:code, :system_name))
      end

      def redirects_to_signup?
        if authentication_provider_exist?
          @error_message.blank? && @user.nil?
        else
          super
        end
      end

      def on_signup(session)
        if @user_data
          session[:authentication_kind] = @user_data[:kind]
          session[:authentication_id] = @user_data[:uid]
          session[:authentication_username] = @user_data[:username]
          session[:authentication_provider] = authentication_provider.system_name

          if @user_data[:email] && @user_data[:email_confirmed]
            session[:authentication_email] = @user_data[:email]
          end
        end
      end

      def on_signup_complete(session)
        if @user_for_signup && @user_for_signup.email == session[:authentication_email]
          @user_for_signup.activate!
        end
        session[:authentication_id] = nil
        session[:authentication_email] = nil
        session[:authentication_username] = nil
        session[:authentication_kind] = nil
        session[:authentication_provider] = nil
      end

      def on_new_user(user, session)
        uid = session[:authentication_id]
        system_name = session[:authentication_provider]
        return if uid.blank? || system_name.blank?

        find_authentication_provider(system_name)
        migrate_sso_authorization(user, uid)
        assign_sso_attributes(user, session)
        @user_for_signup = user
      end

      def track_signup_options(options = {})
        if @user_for_signup && @user_for_signup.signup.oauth2?
          { kind: options[:session][:authentication_kind], strategy: 'oauth2' }
        else
          super(options)
        end
      end

      def migrate_sso_authorization(user, uid)
        sso_auth_params = { uid: uid, authentication_provider: authentication_provider }
        user.sso_authorizations.find_or_initialize_by(sso_auth_params)

        user.save if user.persisted?
      end

      def authentication_provider
        @authentication_provider or raise MissingAuthenticationProvider
      end

      private

      def authentication_provider_exist?
        @authentication_provider.present?
      end

      def assign_sso_attributes(user, session)
        user.username ||= session[:authentication_username].presence
        user.email ||= session[:authentication_email].presence
      end

      def find_authentication_provider(system_name)
        @authentication_provider ||= site_account.authentication_providers.find_by(system_name: system_name)
      end
    end
  end
end
