# frozen_string_literal: true

# Add single sign on functionalities to the user model
module SingleSignOnConcern
  extend ActiveSupport::Concern
  require 'sso_logger'

  included do
    # enable OmniAuth authentication only if needed
    devise :omniauthable, omniauth_providers: [AuthProvider.active.strategy_name.to_sym] unless
      AuthProvider.active.providable_type == DatabaseProvider.name

    ## Retrieve the requested data in the User and user's Profile tables
    ## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
    def get_data_from_sso_mapping(sso_mapping)
      service = UserGetterService.new(self)
      service.read_attribute(sso_mapping)
    end

    ## Set some data on the current user, according to the sso_key given
    ## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
    ## @param data {*} the data to put in the given key. Eg. 'user@example.com'
    def set_data_from_sso_mapping(sso_mapping, data)
      return if data.nil? || data.blank?

      service = UserSetterService.new(self)
      service.assign_attibute(sso_mapping, data)

      return if mapped_from_sso&.include?(sso_mapping)

      self.mapped_from_sso = [mapped_from_sso, sso_mapping].compact.join(',')
    end

    ## used to allow the migration of existing users between authentication providers
    def generate_auth_migration_token
      update(auth_token: Devise.friendly_token)
    end

    ## link the current user to the given provider (omniauth attributes hash)
    ## and remove the auth_token to mark his account as "migrated"
    def link_with_omniauth_provider(auth)
      active_provider = AuthProvider.active
      raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider

      if User.where(provider: auth.provider, uid: auth.uid).size.positive?
        raise DuplicateIndexError, "This #{active_provider.name} account is already linked to an existing user"
      end

      update(provider: auth.provider, uid: auth.uid, auth_token: nil)
    end

    ## Merge the provided User's SSO details into the current user and drop the provided user to ensure the unity
    ## @param sso_user {User} the provided user will be DELETED after the merge was successful
    def merge_from_sso(sso_user)
      logger = SsoLogger.new
      logger.debug "[User::merge_from_sso] initiated with parameter #{sso_user}"
      # update the attributes to link the account to the sso account
      self.provider = sso_user.provider
      self.uid = sso_user.uid

      # remove the token
      self.auth_token = nil
      self.merged_at = DateTime.current

      # check that the email duplication was resolved
      if sso_user.email.end_with? '-duplicate'
        email_addr = sso_user.email.match(/^<([^>]+)>.{20}-duplicate$/)[1]
        logger.error 'duplicate email was not resolved'
        raise(DuplicateIndexError, email_addr) unless email_addr == email
      end

      # update the user's profile to set the data managed by the SSO
      auth_provider = AuthProvider.from_strategy_name(sso_user.provider)
      logger.debug "found auth_provider=#{auth_provider&.name}"
      auth_provider&.sso_fields&.each do |field|
        value = sso_user.get_data_from_sso_mapping(field)
        logger.debug "mapping sso field #{field} with value=#{value}"
        # We do not merge the email field if its end with the special value '-duplicate' as this means
        # that the user is currently merging with the account that have the same email than the sso.
        # Moreover, if the user is an administrator, we must keep him in his group
        unless (field == 'user.email' && value.end_with?('-duplicate')) || (field == 'user.group_id' && admin?)
          set_data_from_sso_mapping(field, value)
        end
      end

      # run the account transfer in an SQL transaction to ensure data integrity
      begin
        User.transaction do
          # remove the temporary account
          logger.debug 'removing the temporary user'
          sso_user.destroy
          # finally, save the new details
          logger.debug 'saving the updated user'
          save!
        end
      rescue ActiveRecord::RecordInvalid => e
        logger.error "error while merging user #{sso_user.id} into #{id}: #{e.message}"
        raise e
      end
    end
  end

  class_methods do
    def from_omniauth(auth)
      logger = SsoLogger.new
      logger.debug "[User::from_omniauth] initiated with parameter #{auth}"
      active_provider = AuthProvider.active
      raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider

      where(provider: auth.provider, uid: auth.uid).first_or_create.tap do |user|
        # execute this regardless of whether record exists or not (-> User#tap)
        # this will init or update the user thanks to the information retrieved from the SSO
        logger.debug user.id.nil? ? 'no user found, creating a new one' : "found user id=#{user.id}"
        user.profile ||= Profile.new
        auth.info.mapping.each do |key, value|
          logger.debug "mapping info #{key} with value=#{value}"
          user.set_data_from_sso_mapping(key, value)
        end
        logger.debug 'generating a new password'
        user.password = SecurePassword.generate
      end
    end
  end
end