# frozen_string_literal: true

module Members
  # This class serves as more of an app-wide way we add/create members
  # All roads to add members should take this path.
  class CreatorService
    class << self
      def parsed_access_level(access_level)
        access_levels.fetch(access_level) { access_level.to_i }
      end

      def access_levels
        Gitlab::Access.sym_options_with_owner
      end
    end

    def initialize(source, user, access_level, **args)
      @source = source
      @user = user
      @access_level = self.class.parsed_access_level(access_level)
      @args = args
    end

    def execute
      find_or_build_member
      commit_member
      create_member_task

      member
    end

    private

    delegate :new_record?, to: :member
    attr_reader :source, :user, :access_level, :member, :args

    def assign_member_attributes
      member.attributes = member_attributes
    end

    def commit_member
      if can_commit_member?
        assign_member_attributes
        commit_changes
      else
        add_commit_error
      end
    end

    def can_commit_member?
      # There is no current user for bulk actions, in which case anything is allowed
      return true if skip_authorization?

      if new_record?
        can_create_new_member?
      else
        can_update_existing_member?
      end
    end

    def can_create_new_member?
      raise NotImplementedError
    end

    def can_update_existing_member?
      raise NotImplementedError
    end

    # Populates the attributes of a member.
    #
    # This logic resides in a separate method so that EE can extend this logic,
    # without having to patch the `add_user` method directly.
    def member_attributes
      {
        created_by: member.created_by || current_user,
        access_level: access_level,
        expires_at: args[:expires_at]
      }
    end

    def commit_changes
      if member.request?
        approve_request
      else
        # Calling #save triggers callbacks even if there is no change on object.
        # This previously caused an incident due to the hard to predict
        # behaviour caused by the large number of callbacks.
        # See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6351
        # and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80920#note_911569038
        # for details.
        member.save if member.changed?
      end
    end

    def create_member_task
      return unless member.persisted?
      return if member_task_attributes.value?(nil)
      return if member.member_task.present?

      member.create_member_task(member_task_attributes)
    end

    def member_task_attributes
      {
        tasks_to_be_done: args[:tasks_to_be_done],
        project_id: args[:tasks_project_id]
      }
    end

    def approve_request
      ::Members::ApproveAccessRequestService.new(current_user,
                                                 access_level: access_level)
                                            .execute(
                                              member,
                                              skip_authorization: ldap,
                                              skip_log_audit_event: ldap
                                            )
    end

    def current_user
      args[:current_user]
    end

    def skip_authorization?
      !current_user
    end

    def add_commit_error
      msg = if new_record?
              _('not authorized to create member')
            else
              _('not authorized to update member')
            end

      member.errors.add(:base, msg)
    end

    def find_or_build_member
      @user = parse_user_param

      @member = if user.is_a?(User)
                  find_or_initialize_member_by_user
                else
                  source.members.build(invite_email: user)
                end

      @member.blocking_refresh = args[:blocking_refresh]
    end

    # This method is used to find users that have been entered into the "Add members" field.
    # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
    def parse_user_param
      case user
      when User
        user
      when Integer
        # might not return anything - this needs enhancement
        User.find_by(id: user) # rubocop:todo CodeReuse/ActiveRecord
      else
        # must be an email or at least we'll consider it one
        source.users_by_emails([user])[user] || user
      end
    end

    def find_or_initialize_member_by_user
      # have to use members and requesters here since project/group limits on requested_at being nil for members and
      # wouldn't be found in `source.members` if it already existed
      # this of course will not treat active invites the same since we aren't searching on email
      source.members_and_requesters.find_or_initialize_by(user_id: user.id) # rubocop:disable CodeReuse/ActiveRecord
    end

    def ldap
      args[:ldap] || false
    end
  end
end

Members::CreatorService.prepend_mod_with('Members::CreatorService')
