# frozen_string_literal: true

module ReleaseTools
  module ReleaseManagers
    class Client
      include ::SemanticLogger::Loggable

      SyncError = Class.new(StandardError)
      UserNotFoundError = Class.new(SyncError)
      UnauthorizedError = Class.new(SyncError)

      GITLAB_API_ENDPOINT = 'https://gitlab.com/api/v4'
      DEV_API_ENDPOINT = 'https://dev.gitlab.org/api/v4'
      OPS_API_ENDPOINT = 'https://ops.gitlab.net/api/v4'

      MASTER_ACCESS = 40

      attr_reader :sync_errors, :target

      # Initialize a GitLab API client specific to Release Manager tasks
      #
      # target - Target :production, :dev or :ops environment (default: :production)
      def initialize(target = :production)
        @sync_errors = []
        @target = target

        case target
        when :dev
          @group = 'gitlab/release/managers'
          @client = Gitlab.client(
            endpoint: DEV_API_ENDPOINT,
            private_token: ENV.fetch('RELEASE_BOT_DEV_TOKEN', nil)
          )
        when :ops
          @group = 'gitlab-org/release/managers'
          @client = Gitlab.client(
            endpoint: OPS_API_ENDPOINT,
            private_token: ENV.fetch('RELEASE_BOT_OPS_TOKEN', nil)
          )
        else
          @target = :production
          @group = 'gitlab-org/release/managers'
          @client = Gitlab.client(
            endpoint: GITLAB_API_ENDPOINT,
            private_token: ENV.fetch('RELEASE_BOT_PRODUCTION_TOKEN', nil)
          )
        end
      end

      def members
        client.group_members(group)
      end

      def sync_membership(usernames)
        logger.info('Syncing membership', target: target)

        usernames.map!(&:downcase)
        existing = member_usernames

        to_add = usernames - existing
        to_remove = existing - usernames

        to_add.each do |username|
          track_sync_errors { add_member(username) }
        end
        to_remove.each do |username|
          track_sync_errors { remove_member(username) }
        end
      rescue Gitlab::Error::Unauthorized
        sync_errors << UnauthorizedError.new("Unauthorized")
      rescue Gitlab::Error::Forbidden
        sync_errors << UnauthorizedError.new('Insufficient permissions')
      end

      def join(username)
        track_sync_errors do
          if member_usernames.include?(username)
            logger.warn('User is already part of the group', username: username)
            return
          end

          add_member(username)
        end
      end

      def leave(username)
        track_sync_errors do
          unless member_usernames.include?(username)
            logger.warn('User is not part of the group', username: username)
            return
          end

          remove_member(username)
        end
      end

      def get_user(username)
        user = client
          .users(username: username)
          .first

        user || raise(UserNotFoundError, "#{username} not found")
      end

      private

      attr_reader :client, :group

      def member_usernames
        members.collect { |member| member.username.downcase }
      end

      def add_member(username)
        user = get_user(username)

        logger.info('Adding user to group', user: user.username, group: group)
        client.add_group_member(group, user.id, MASTER_ACCESS)
      rescue Gitlab::Error::Conflict => ex
        raise SyncError.new(ex) unless ex.message.include?('Member already exists')
      rescue Gitlab::Error::BadRequest => ex
        # Ignore when a new member has greater permissions via group inheritance
        raise SyncError.new(ex) unless ex.message.include?('should be greater than or equal to')
      end

      def remove_member(username)
        user = get_user(username)

        logger.info('Removing user from group', user: username, group: group)
        client.remove_group_member(group, user.id)
      end

      def track_sync_errors
        yield
      rescue SyncError => ex
        sync_errors << ex
      end
    end
  end
end
