# frozen_string_literal: true

module Doorkeeper
  module OpenidConnect
    module Helpers
      module Controller
        private

        # FIXME: remove after Doorkeeper will merge it
        def current_resource_owner
          return @current_resource_owner if defined?(@current_resource_owner)

          super
        end

        def authenticate_resource_owner!
          super.tap do |owner|
            next unless oidc_authorization_request?

            handle_oidc_max_age_param!(owner)
            handle_oidc_prompt_param!(owner)
          end
        rescue Errors::OpenidConnectError => e
          handle_oidc_error!(e)
        end

        def oidc_authorization_request?
          controller_path == Doorkeeper::Rails::Routes.mapping[:authorizations][:controllers] &&
            action_name == 'new' &&
            pre_auth.valid? &&
            pre_auth.scopes.include?('openid')
        end

        def handle_oidc_error!(exception)
          # clear the previous response body to avoid a DoubleRenderError
          self.response_body = nil

          # FIXME: workaround for Rails 5, see https://github.com/rails/rails/issues/25106
          @_response_body = nil

          error_response = if exception.type == :invalid_request
                             ::Doorkeeper::OAuth::InvalidRequestResponse.new(
                               name: exception.type,
                               state: params[:state],
                               redirect_uri: params[:redirect_uri],
                               response_on_fragment: pre_auth.response_on_fragment?,
                             )
                           else
                             ::Doorkeeper::OAuth::ErrorResponse.new(
                               name: exception.type,
                               state: params[:state],
                               redirect_uri: params[:redirect_uri],
                               response_on_fragment: pre_auth.response_on_fragment?,
                             )
                           end

          response.headers.merge!(error_response.headers)

          # NOTE: Assign error_response to @authorize_response then use redirect_or_render method that are defined at
          #   doorkeeper's authorizations_controller.
          # - https://github.com/doorkeeper-gem/doorkeeper/blob/v5.5.0/app/controllers/doorkeeper/authorizations_controller.rb#L110
          # - https://github.com/doorkeeper-gem/doorkeeper/blob/v5.5.0/app/controllers/doorkeeper/authorizations_controller.rb#L52
          @authorize_response = error_response
          redirect_or_render(@authorize_response)
        end

        def handle_oidc_prompt_param!(owner)
          prompt_values ||= params[:prompt].to_s.split(/ +/).uniq

          priority = ['none', 'consent', 'login', 'select_account']
          prompt_values.sort_by! do |prompt|
            priority.find_index(prompt).to_i
          end

          prompt_values.each do |prompt|
            case prompt
            when 'none'
              raise Errors::InvalidRequest if (prompt_values - ['none']).any?
              raise Errors::LoginRequired unless owner
              raise Errors::ConsentRequired if oidc_consent_required?
            when 'login'
              reauthenticate_oidc_resource_owner(owner) if owner
            when 'consent'
              if owner
                clear_oidc_response
                render :new
              end
            when 'select_account'
              select_account_for_oidc_resource_owner(owner)
            when 'create'
              # NOTE: not supported, but not raise error.
            else
              raise Errors::InvalidRequest
            end
          end
        end

        def handle_oidc_max_age_param!(owner)
          max_age = params[:max_age].to_i
          return unless (params[:max_age].to_s == '0' || max_age > 0) && owner

          auth_time = instance_exec(
            owner,
            &Doorkeeper::OpenidConnect.configuration.auth_time_from_resource_owner
          )

          # NOTE: clock skew
          max_age = [1, max_age].max

          if !auth_time || (Time.zone.now - auth_time) > max_age
            reauthenticate_oidc_resource_owner(owner)
          end
        end

        def return_without_oidc_prompt_param(prompt_value)
          return_to = URI.parse(request.path)
          return_to.query = request.query_parameters.tap do |params|
            params['prompt'] = params['prompt'].to_s.sub(/\b#{prompt_value}\s*\b/, '').strip
            params.delete('prompt') if params['prompt'].blank?
          end.to_query
          return_to.to_s
        end

        def clear_oidc_response
          self.response_body = nil
          @_response_body = nil
        end

        def reauthenticate_oidc_resource_owner(owner)
          clear_oidc_response
          return_to = return_without_oidc_prompt_param('login')

          instance_exec(
            owner,
            return_to,
            &Doorkeeper::OpenidConnect.configuration.reauthenticate_resource_owner
          )

          raise Errors::LoginRequired unless performed?
        end

        def oidc_consent_required?
          !skip_authorization? && !matching_token?
        end

        def select_account_for_oidc_resource_owner(owner)
          clear_oidc_response
          return_to = return_without_oidc_prompt_param('select_account')

          instance_exec(
            owner,
            return_to,
            &Doorkeeper::OpenidConnect.configuration.select_account_for_resource_owner
          )
        end
      end
    end
  end

  Helpers::Controller.prepend OpenidConnect::Helpers::Controller
end
