require 'ruby-saml'

class Devise::SamlSessionsController < Devise::SessionsController
  include DeviseSamlAuthenticatable::SamlConfig

  skip_before_action :verify_authenticity_token, raise: false
  prepend_before_action :store_info_for_sp_initiated_logout, :verify_signed_out_user, only: :destroy

  def new
    idp_entity_id = get_idp_entity_id(params)
    auth_request = OneLogin::RubySaml::Authrequest.new
    auth_params = { RelayState: relay_state } if relay_state
    action = auth_request.create(saml_config(idp_entity_id, request), auth_params || {})
    session[:saml_transaction_id] = auth_request.request_id if auth_request.respond_to?(:request_id)
    redirect_to action, allow_other_host: true
  end

  def metadata
    idp_entity_id = params[:idp_entity_id]
    meta = OneLogin::RubySaml::Metadata.new
    render xml: meta.generate(saml_config(idp_entity_id, request))
  end

  def idp_sign_out
    if params[:SAMLRequest] && Devise.saml_session_index_key
      Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
      session[Devise.saml_session_index_key] = nil

      saml_config = saml_config(get_idp_entity_id(params), request)
      logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], settings: saml_config)
      redirect_to generate_idp_logout_response(saml_config, logout_request.id), allow_other_host: true
    elsif params[:SAMLResponse]
      # Currently Devise handles the session invalidation when the request is made.
      # To support a true SP initiated logout response, the request ID would have to be tracked and session invalidated
      # based on that.
      if Devise.saml_sign_out_success_url
        redirect_to Devise.saml_sign_out_success_url
      else
        redirect_to action: :new
      end
    else
      head 500
    end
  end

  protected

  def relay_state
    @relay_state ||= (Devise.saml_relay_state.call(request) if Devise.saml_relay_state.present?)
  end

  # For non transient name ID, save info to identify user for logout purpose
  # before that user's session got destroyed. These info are used in the
  # `after_sign_out_path_for` method below.
  def store_info_for_sp_initiated_logout
    return if Devise.saml_config.name_identifier_format == 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'

    @name_identifier_value_for_sp_initiated_logout = Devise.saml_name_identifier_retriever.call(current_user)
    if Devise.saml_session_index_key
      @sessionindex_for_sp_initiated_logout = session[Devise.saml_session_index_key]
    end
  end

  # Override devise to send user to IdP logout for SLO
  def after_sign_out_path_for(_)
    idp_entity_id = get_idp_entity_id(params)
    logout_request = OneLogin::RubySaml::Logoutrequest.new
    saml_settings = saml_config(idp_entity_id, request).dup

    # Add attributes to saml_settings which will later be used to create the SP
    # initiated logout request
    unless Devise.saml_config.name_identifier_format == 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
      saml_settings.name_identifier_value = @name_identifier_value_for_sp_initiated_logout
      saml_settings.sessionindex = @sessionindex_for_sp_initiated_logout
    end

    logout_request.create(saml_settings)
  end

  # Overried devise: if user is signed out, not create the SP initiated logout request,
  # redirect to saml_sign_out_success_url,
  # or devise's after_sign_out_path_for
  def verify_signed_out_user
    if all_signed_out?
      set_flash_message! :notice, :already_signed_out

      redirect_to (Devise.saml_sign_out_success_url.presence ||
                  Devise::SessionsController.new.after_sign_out_path_for(resource_name)), allow_other_host: true
    end
  end

  def generate_idp_logout_response(saml_config, logout_request_id)
    params = {}
    params[:RelayState] = relay_state if relay_state

    OneLogin::RubySaml::SloLogoutresponse.new.create(saml_config, logout_request_id, nil, params)
  end
end
