text: import asyncio
  import discord
  import logging
  import pydest
  import pytz
  
  from datetime import datetime
  from discord.ext import commands
  from discord.ext.commands.errors import BadArgument
  from tortoise.query_utils import Q
  
  from seraphsix import constants
  from seraphsix.cogs.utils.checks import (
      is_clan_admin,
      is_valid_game_mode,
      is_registered,
      clan_is_linked,
  )
  from seraphsix.cogs.utils.helpers import date_as_string, get_requestor
  from seraphsix.cogs.utils.message_manager import MessageManager
  from seraphsix.cogs.utils.paginator import FieldPages, EmbedPages
  from seraphsix.errors import InvalidAdminError, InvalidCommandError
  from seraphsix.models import deserializer, serializer
  from seraphsix.models.database import Clan, Guild, ClanMemberApplication, Role
  from seraphsix.models.destiny import (
      DestinyMembershipResponse,
      DestinyMemberGroupResponse,
      DestinyGroupResponse,
      DestinyGroupPendingMembersResponse,
      DestinySearchPlayerResponse,
  )
  from seraphsix.tasks.activity import get_game_counts, get_last_active
  from seraphsix.tasks.core import (
      execute_pydest,
      get_primary_membership,
      execute_pydest_auth,
      get_memberships,
  )
  from seraphsix.tasks.clan import info_sync, member_sync
  
  log = logging.getLogger(__name__)
  
  
  class ClanCog(commands.Cog, name="Clan"):
      def __init__(self, bot):
          self.bot = bot
  
      async def get_admin_group(self, ctx):
          clan_db = await Clan.get(
              guild__guild_id=ctx.message.guild.id,
              members__member_type__gte=constants.CLAN_MEMBER_ADMIN,
              members__member__discord_id=ctx.author.id,
          )
          if not clan_db:
              raise InvalidAdminError
          return clan_db
  
      async def get_user_details(self, args):
          username, platform_id = (None,) * 2
  
          if not args:
              raise InvalidCommandError("Username is required")
  
          if args[-1] == "-platform":
              raise InvalidCommandError(
                  f"Platform must be specified like `-platform {list(constants.PLATFORM_EMOJI_MAP.keys())}`"
              )
  
          if "-platform" in args:
              platform_name = args[-1].lower()
              username = " ".join(args[0:-2])
              platform_id = constants.PLATFORM_MAP.get(platform_name)
              if not platform_id:
                  raise InvalidCommandError(
                      f"Invalid platform `{platform_name}` was specified"
                  )
          else:
              username = " ".join(args)
  
          if not username:
              raise InvalidCommandError("Username is required")
  
          return username, platform_id
  
      async def get_member_db(self, ctx, username, include_clan=False):
          try:
              member_discord = await commands.MemberConverter().convert(
                  ctx, str(username)
              )
          except BadArgument:
              member_query = self.bot.database.get_member_by_naive_username(
                  username, include_clan=include_clan
              )
          else:
              member_query = self.bot.database.get_member_by_discord_id(
                  member_discord.id, include_clan=include_clan
              )
          return await asyncio.create_task(member_query)
  
      async def get_bungie_details(self, username, bungie_id=None, platform_id=None):
          membership_id = None
          username_lower = username.lower()
  
          if bungie_id:
              try:
                  player = await execute_pydest(
                      self.bot.destiny.api.get_membership_data_by_id,
                      bungie_id,
                      return_type=DestinyMembershipResponse,
                  )
              except pydest.PydestException as e:
                  log_message = f"Could not find Destiny player for {username}"
                  log.error(f"{log_message}\n\n{e}\n\n{username}")
                  raise InvalidCommandError(log_message)
  
              for membership in player.response.destiny_memberships:
                  if membership.membership_type != constants.PLATFORM_BUNGIE:
                      membership_id = membership.membership_id
                      platform_id = membership.membership_type
                      break
          else:
              player = await execute_pydest(
                  self.bot.destiny.api.search_destiny_player,
                  platform_id,
                  username,
                  return_type=DestinySearchPlayerResponse,
              )
              if not player.response:
                  log_message = f"Could not find Destiny player for {username}"
                  log.error(f"{log_message}\n\n{player}")
                  raise InvalidCommandError(log_message)
  
              if len(player.response) == 1:
                  membership = player.response[0]
                  if (
                      membership.display_name.lower() == username_lower
                      and membership.membership_type == platform_id
                  ):
                      membership_id = membership.membership_id
                      platform_id = membership.membership_type
                  else:
                      membership_orig = membership
                      profile = await execute_pydest(
                          self.bot.destiny.api.get_membership_data_by_id,
                          membership.membership_id,
                          return_type=DestinyMembershipResponse,
                      )
                      for membership in profile.response.destiny_memberships:
                          if membership.display_name.lower() == username_lower:
                              user_matches = True
                              break
                      if user_matches:
                          membership_id = membership_orig.membership_id
                          platform_id = membership_orig.membership_type
              else:
                  for membership in player.response:
                      display_name = membership.display_name.lower()
                      membership_type = membership.membership_type
                      if (
                          membership_type == platform_id
                          and display_name == username_lower
                      ):
                          membership_id = membership.membership_id
                          platform_id = membership.membership_type
                          break
          return membership_id, platform_id
  
      async def create_application_embed(self, ctx, requestor_db, guild_db):
          redis_cache = self.bot.ext_conns["redis_cache"]
  
          if requestor_db.bungie_username:
              membership_name = requestor_db.bungie_username
  
          platform_id, membership_id, _ = get_primary_membership(requestor_db)
  
          group_id = None
          group_name = None
          groups_info = await execute_pydest(
              self.bot.destiny.api.get_groups_for_member,
              platform_id,
              membership_id,
              return_type=DestinyMemberGroupResponse,
          )
          if len(groups_info.response.results) > 0:
              for group in groups_info.response.results:
                  if group.member.destiny_user_info.membership_id == membership_id:
                      group_id = group.group.group_id
                      group_name = group.group.name
  
          if group_id and group_name:
              group_url = f"https://www.bungie.net/en/ClanV2/Index?groupId={group_id}"
              group_link = f"[{group_name}]({group_url})"
          else:
              group_link = "None"
  
          last_active = await get_last_active(
              self.bot.ext_conns, platform_id=platform_id, member_id=membership_id
          )
  
          embed = discord.Embed(
              colour=constants.BLUE,
              title=f"Clan Application for {ctx.author.display_name}",
          )
  
          bungie_url = f"https://www.bungie.net/en/Profile/{platform_id}/{membership_id}"
          bungie_link = f"[{membership_name}]({bungie_url})"
  
          if requestor_db.discord_id:
              member_discord = await commands.MemberConverter().convert(
                  ctx, str(requestor_db.discord_id)
              )
              discord_username = str(member_discord)
  
          embed.add_field(name="Last Active Date", value=date_as_string(last_active))
          embed.add_field(name="Bungie Username", value=bungie_link)
          embed.add_field(name="Current Clan", value=group_link)
          embed.add_field(name="Xbox Gamertag", value=requestor_db.xbox_username)
          embed.add_field(name="PSN Username", value=requestor_db.psn_username)
          embed.add_field(name="Steam Username", value=requestor_db.steam_username)
          embed.add_field(name="Stadia Username", value=requestor_db.stadia_username)
          embed.add_field(name="Discord Username", value=discord_username)
          embed.set_footer(text="All times shown in UTC")
          embed.set_thumbnail(url=str(ctx.author.avatar_url))
  
          await redis_cache.set(
              f"{ctx.guild.id}-clan-application-{requestor_db.id}", serializer(embed)
          )
          return embed
  
      async def get_inactive_members(self, ctx, clan_db):
          inactive_members_filtered = []
  
          query = Role.filter(guild__guild_id=ctx.guild.id, is_protected_clanmember=True)
          protected_roles = [role.role_id for role in await query]
  
          inactive_members = await self.bot.database.get_clan_members_inactive(clan_db)
          if len(inactive_members) > 0:
              for clanmember in inactive_members:
                  time_delta = (
                      datetime.now(pytz.utc) - clanmember.last_active
                  ).total_seconds()
                  platform_id, membership_id, username = get_primary_membership(
                      clanmember.member
                  )
                  member_discord = ctx.guild.get_member(clanmember.member.discord_id)
                  if member_discord:
                      member_roles = [role.id for role in member_discord.roles]
                      if any(role in protected_roles for role in member_roles):
                          continue
                  inactive_members_filtered.append(
                      (time_delta, platform_id, membership_id, username)
                  )
  
          inactive_members_filtered.sort(reverse=True)
          return inactive_members_filtered
  
      @commands.group(invoke_without_command=True)
      async def clan(self, ctx):
          """Clan Specific Commands"""
          if ctx.invoked_subcommand is None:
              raise commands.CommandNotFound()
  
      @clan.group(invoke_without_command=True)
      @is_clan_admin()
      @commands.guild_only()
      @commands.has_permissions(administrator=True)
      async def admin(self, ctx):
          """Clan Admin Specific Commands"""
          if ctx.invoked_subcommand is None:
              raise commands.CommandNotFound()
  
      @clan.group(invoke_without_command=True)
      async def the100(self, ctx):
          """The100 Specific Commands"""
          if ctx.invoked_subcommand is None:
              raise commands.CommandNotFound()
  
      @the100.command()
      @clan_is_linked()
      @commands.guild_only()
      @commands.has_permissions(administrator=True)
      async def link(self, ctx, group_id):
          """Link clan to the100 group (Admin only)"""
          manager = MessageManager(ctx)
  
          if not group_id:
              return await manager.send_and_clean(
                  "Command must include the the100 group ID"
              )
  
          res = await self.bot.the100.get_group(group_id)
          if res.get("error"):
              return await manager.send_and_clean(
                  f"Could not locate the100 group {group_id}"
              )
  
          group_name = res["name"]
          callsign = res["clan_tag"]
  
          clan_db = await self.get_admin_group(ctx)
          if clan_db.the100_group_id:
              return await manager.send_and_clean(
                  f"**{clan_db.name} [{clan_db.callsign}]** is already linked to another the100 group."
              )
  
          clan_db.the100_group_id = res["id"]
          await clan_db.save()
  
          message = (
              f"**{clan_db.name} [{clan_db.callsign}]** "
              f"linked to **{group_name} [{callsign}]**"
          )
          return await manager.send_message(message, mention=False, clean=False)
  
      @the100.command()
      @clan_is_linked()
      @commands.guild_only()
      @commands.has_permissions(administrator=True)
      async def unlink(self, ctx, group_id):
          """Unlink clan from the100 group (Admin only)"""
          manager = MessageManager(ctx)
  
          clan_db = await self.get_admin_group(ctx)
          if not clan_db.the100_group_id:
              return await manager.send_and_clean(
                  f"**{clan_db.name} [{clan_db.callsign}]** is not linked to a the100 group."
              )
  
          res = await self.bot.the100.get_group(clan_db.the100_group_id)
          if res.get("error"):
              return await manager.send_and_clean(
                  f"Could not locate the100 group {clan_db.the100_group_id}"
              )
  
          group_name = res["name"]
          callsign = res["clan_tag"]
  
          clan_db.the100_group_id = None
          await clan_db.save()
  
          message = (
              f"**{clan_db.name} [{clan_db.callsign}]** "
              f"unlinked from **{group_name} [{callsign}]**"
          )
          return await manager.send_message(message, mention=False, clean=False)
  
      @clan.command()
      @clan_is_linked()
      @commands.guild_only()
      async def info(self, ctx, *args):
          """Show information for all connected clans"""
          redis_cache = self.bot.ext_conns["redis_cache"]
          manager = MessageManager(ctx)
  
          clan_dbs = await self.bot.database.get_clans_by_guild(ctx.guild.id)
  
          if not clan_dbs:
              return await manager.send_and_clean(
                  "No connected clans found", mention=False
              )
  
          embeds = []
          clan_redis_key = f"{ctx.guild.id}-clan-info"
          clan_info_redis = await redis_cache.get(clan_redis_key)
          if clan_info_redis and "-nocache" not in args:
              log.debug(f"{clan_redis_key} {clan_info_redis}")
              await redis_cache.expire(clan_redis_key, constants.TIME_HOUR_SECONDS)
              embeds = [
                  discord.Embed.from_dict(embed)
                  for embed in deserializer(clan_info_redis)
              ]
          else:
              for clan_db in clan_dbs:
                  group = await execute_pydest(
                      self.bot.destiny.api.get_group,
                      clan_db.clan_id,
                      return_type=DestinyGroupResponse,
                  )
                  if not group.response:
                      log.error(
                          f"Could not get details for clan {clan_db.name} ({clan_db.clan_id}) - "
                          f"{group.error_status} {group.error_description}"
                      )
                      return await manager.send_and_clean(
                          f"Clan {clan_db.name} not found", mention=False
                      )
                  else:
                      group = group.response
  
                  embed = discord.Embed(
                      colour=constants.BLUE,
                      title=group.detail.motto,
                      description=group.detail.about,
                  )
                  embed.set_author(
                      name=f"{group.detail.name} [{group.detail.clan_info.clan_callsign}]",
                      url=f"https://www.bungie.net/en/ClanV2?groupid={clan_db.clan_id}",
                  )
                  embed.add_field(
                      name="Members", value=group.detail.member_count, inline=True
                  )
                  embed.add_field(
                      name="Founder",
                      value=group.founder.bungie_net_user_info.display_name,
                      inline=True,
                  )
                  embed.add_field(
                      name="Founded",
                      value=date_as_string(group.detail.creation_date),
                      inline=True,
                  )
                  embeds.append(embed)
              await redis_cache.set(
                  clan_redis_key,
                  serializer([embed.to_dict() for embed in embeds]),
                  expire=constants.TIME_HOUR_SECONDS,
              )
  
          if len(embeds) > 1:
              paginator = EmbedPages(ctx, embeds)
              await paginator.paginate()
          else:
              await manager.send_embed(embeds[0])
  
      @clan.command()
      @clan_is_linked()
      @commands.guild_only()
      async def roster(self, ctx, *args):
          """Show roster for all connected clans"""
          manager = MessageManager(ctx)
          clan_dbs = await self.bot.database.get_clans_by_guild(ctx.guild.id)
  
          if not clan_dbs:
              return await manager.send_and_clean("No connected clans found")
  
          # members = []
          # members_redis = await self.bot.ext_conns['redis_cache'].lrange(f"{ctx.guild.id}-clan-roster", 0, -1)
          # if members_redis and '-nocache' not in args:
          #     await self.bot.ext_conns['redis_cache'].expire(f"{ctx.guild.id}-clan-roster", constants.TIME_HOUR_SECONDS)
          #     for member in members_redis:
          #         members.append(deserializer(member))
          # else:
          #     members_db = await self.bot.database.get_clan_members([clan_db.clan_id for clan_db in clan_dbs])
          #     for member in members_db:
          #         await self.bot.ext_conns['redis_cache'].rpush(f"{ctx.guild.id}-clan-roster", serializer(member))
          #     await self.bot.ext_conns['redis_cache'].expire(f"{ctx.guild.id}-clan-roster", constants.TIME_HOUR_SECONDS)
          #     members = members_db
          members_db = await self.bot.database.get_clan_members(
              [clan_db.clan_id for clan_db in clan_dbs]
          )
  
          platform_names = list(constants.PLATFORM_MAP.keys())
          platform_ids = list(constants.PLATFORM_MAP.values())
  
          entries = []
  
          clans = {}
          for clanmember in members_db:
              if clanmember.clan.id not in clans.keys():
                  clans[clanmember.clan.id] = {}
  
              member = clanmember.member
              timezone = "Not Set"
              if member.timezone:
                  tz = datetime.now(pytz.timezone(member.timezone))
                  timezone = f"{tz.strftime('UTC%z')} ({tz.tzname()})"
  
              if member.bungie_username:
                  username = member.bungie_username
              else:
                  username = f"{member.xbox_username} X"
              memberships = get_memberships(member)
              if constants.PLATFORM_BUNGIE in memberships.keys():
                  username = memberships[constants.PLATFORM_BUNGIE][1]
              else:
                  username = list(memberships.values())[0][1]
  
              platform_emojis = [
                  constants.PLATFORM_EMOJI_MAP.get(
                      platform_names[platform_ids.index(platform)]
                  )
                  for platform in memberships.keys()
              ]
  
              member_info = (
                  f"{username} {' '.join([str(self.bot.get_emoji(emoji)) for emoji in platform_emojis if emoji])}",
                  f"Clan: {clanmember.clan.name} [{clanmember.clan.callsign}]\n"
                  f"Join Date: {date_as_string(clanmember.join_date)}\n"
                  f"Timezone: {timezone}",
              )
              clans[clanmember.clan.id][username] = member_info
  
          entries = []
          for i in sorted(clans.keys()):
              for v in sorted(clans[i].keys()):
                  entries.append(clans[i][v])
          p = FieldPages(
              ctx,
              entries=entries,
              per_page=5,
              title="Roster for All Connected Clans",
              color=constants.BLUE,
          )
          await p.paginate()
  
      @admin.command()
      async def pending(self, ctx):
          """Show a list of pending members (Admin only, requires registration)"""
          manager = MessageManager(ctx)
  
          admin_db = await self.bot.database.get_member_by_discord_id(
              ctx.author.id, include_clan=False
          )
          clan_db = await self.get_admin_group(ctx)
  
          members = await execute_pydest_auth(
              self.bot.ext_conns,
              self.bot.destiny.api.get_group_pending_members,
              admin_db,
              manager,
              group_id=clan_db.clan_id,
              access_token=admin_db.bungie_access_token,
              return_type=DestinyGroupPendingMembersResponse,
          )
  
          embed = discord.Embed(
              colour=constants.BLUE, title=f"Pending Clan Members in {clan_db.name}"
          )
  
          if len(members.response.results) == 0:
              embed.description = "None"
          else:
              for member in members.response.results:
                  bungie_name = member.destiny_user_info.display_name
                  bungie_member_id = member.destiny_user_info.membership_id
                  bungie_member_type = member.destiny_user_info.membership_type
                  date_applied = date_as_string(member.creation_date, with_tz=True)
                  bungie_url = f"https://www.bungie.net/en/Profile/{bungie_member_type}/{bungie_member_id}"
                  member_info = f"Date Applied: {date_applied}\nProfile: {bungie_url}"
                  embed.add_field(name=bungie_name, value=member_info)
  
          await manager.send_embed(embed)
  
      @admin.command(
          help="""
  Approve a pending clan member (Admin only, requires registration)
  
  To approve based on Discord info, the user must be registered. Once that is done,
  the command takes any Discord user info (username, nickname, id, etc.)
  
  For in-game username if the server default platform is not set, the `-platform`
  argument is required.
  
  Examples:
  ?clan approve username
  ?clan approve username -platform xbox
  """
      )
      async def approve(self, ctx, *args):
          """Approve a pending member (Admin only, requires registration)"""
          manager = MessageManager(ctx)
          username, platform_id = await self.get_user_details(args)
  
          member_db = await self.get_member_db(ctx, username)
          admin_db = await self.bot.database.get_member_by_discord_id(
              ctx.author.id, include_clan=False
          )
          clan_db = await self.get_admin_group(ctx)
  
          if clan_db.platform:
              platform_id = clan_db.platform
          elif not platform_id:
              raise InvalidCommandError(
                  "Platform was not specified and clan default platform is not set"
              )
  
          bungie_id = None
          if member_db:
              bungie_id = member_db.bungie_id
  
          membership_id, platform_id = await self.get_bungie_details(
              username, bungie_id, platform_id
          )
  
          res = await execute_pydest_auth(
              self.bot.ext_conns,
              self.bot.destiny.api.group_approve_pending_member,
              admin_db,
              manager,
              group_id=clan_db.clan_id,
              membership_type=platform_id,
              membership_id=membership_id,
              message=f"Welcome to {clan_db.name}!",
              access_token=admin_db.bungie_access_token,
          )
  
          if res.error_status != "Success":
              message = f"Could not approve **{username}**"
              log.info(f"Could not approve '{username}': {res}")
          else:
              message = f"Approved **{username}** as a member of clan **{clan_db.name}**"
  
          return await manager.send_message(message, mention=False, clean=False)
  
      @admin.command()
      async def invited(self, ctx):
          """Show a list of invited members (Admin only, requires registration)"""
          manager = MessageManager(ctx)
  
          admin_db = await self.bot.database.get_member_by_discord_id(
              ctx.author.id, include_clan=False
          )
          clan_db = await self.get_admin_group(ctx)
  
          members = await execute_pydest_auth(
              self.bot.ext_conns,
              self.bot.destiny.api.get_group_invited_members,
              admin_db,
              manager,
              group_id=clan_db.clan_id,
              access_token=admin_db.bungie_access_token,
              return_type=DestinyGroupPendingMembersResponse,
          )
  
          embed = discord.Embed(
              colour=constants.BLUE, title=f"Invited Clan Members in {clan_db.name}"
          )
  
          if len(members.response.results) == 0:
              embed.description = "None"
          else:
              for member in members.response.results:
                  bungie_name = member.destiny_user_info.display_name
                  bungie_member_id = member.destiny_user_info.membership_id
                  bungie_member_type = member.destiny_user_info.membership_type
                  date_invited = date_as_string(member.creation_date, with_tz=True)
                  bungie_url = f"https://www.bungie.net/en/Profile/{bungie_member_type}/{bungie_member_id}"
                  member_info = f"Date Invited: {date_invited}\nProfile: {bungie_url}"
                  embed.add_field(name=bungie_name, value=member_info)
  
          await manager.send_embed(embed)
  
      @admin.command(
          help="""
  Invite a user to clan (Admin only, requires registration)
  
  To invite based on Discord info, the user must be registered. Once that is done,
  the command takes any Discord user info (username, nickname, id, etc.)
  
  For in-game username if the server default platform is not set, the `-platform`
  argument is required.
  
  Examples:
  ?clan invite username
  ?clan invite username -platform xbox
  """
      )
      async def invite(self, ctx, *args):
          """Invite a member by username (Admin only, requires registration)"""
          manager = MessageManager(ctx)
          username, platform_id = await self.get_user_details(args)
  
          member_db = await self.get_member_db(ctx, username)
          admin_db = await self.bot.database.get_member_by_discord_id(
              ctx.author.id, include_clan=False
          )
          clan_db = await self.get_admin_group(ctx)
  
          if not platform_id and clan_db.platform:
              platform_id = clan_db.platform
          else:
              raise InvalidCommandError(
                  "Platform was not specified and clan default platform is not set"
              )
  
          bungie_id = None
          if member_db:
              bungie_id = member_db.bungie_id
  
          membership_id, platform_id = await self.get_bungie_details(
              username, bungie_id, platform_id
          )
  
          res = await execute_pydest_auth(
              self.bot.ext_conns,
              self.bot.destiny.api.group_invite_member,
              admin_db,
              manager,
              group_id=clan_db.clan_id,
              membership_type=platform_id,
              membership_id=membership_id,
              message=f"Join my clan {clan_db.name}!",
              access_token=admin_db.bungie_access_token,
          )
  
          if res.error_status == "ClanTargetDisallowsInvites":
              message = f"User **{username}** has disabled clan invites"
          elif res.error_status == "ClanMaximumMembershipReached":
              message = f"Could not invite **{username}**, clan is full"
          elif res.error_status != "Success":
              message = f"Could not invite **{username}**"
              log.info(f"Could not invite '{username}': {res}")
          else:
              message = f"Invited **{username}** to clan **{clan_db.name}**"
  
          return await manager.send_message(message, mention=False, clean=False)
  
      @admin.command()
      @clan_is_linked()
      async def sync(self, ctx):
          """Sync member list with Destiny (Admin only)"""
          manager = MessageManager(ctx)
  
          member_changes = await member_sync(
              self.bot.ext_conns, ctx.guild.id, str(ctx.guild)
          )
          clan_info_changes = await info_sync(self.bot.ext_conns, ctx.guild.id)
  
          clan_dbs = await self.bot.database.get_clans_by_guild(ctx.guild.id)
          embeds = []
          for clan_db in clan_dbs:
              embed = discord.Embed(
                  colour=constants.BLUE,
                  title=f"Clan Changes for {clan_db.name}",
              )
  
              added, removed, changed = [
                  member_changes[clan_db.clan_id][k]
                  for k in ["added", "removed", "changed"]
              ]
  
              if (
                  not added
                  and not removed
                  and not changed
                  and not clan_info_changes.get(clan_db.clan_id)
              ):
                  embed.add_field(name="No Changes", value="-")
  
              if added:
                  if len(added) > 10:
                      members_value = f"Too many to list: {len(added)} total"
                  else:
                      members_value = ", ".join(added)
                  embed.add_field(name="Members Added", value=members_value, inline=False)
  
              if removed:
                  if len(removed) >= 10:
                      members_value = f"Too many to list: {len(removed)} total"
                  else:
                      members_value = ", ".join(removed)
                  embed.add_field(
                      name="Members Removed", value=members_value, inline=False
                  )
  
              if changed:
                  embed.add_field(
                      name="Members Changed", value=", ".join(changed), inline=False
                  )
  
              if clan_info_changes.get(clan_db.clan_id):
                  changes = clan_info_changes[clan_db.clan_id]
                  if changes.get("name"):
                      embed.add_field(
                          name="Name Changed",
                          value=f"From **{changes['name']['from']}** to **{changes['name']['to']}**",
                          inline=False,
                      )
                  if changes.get("callsign"):
                      embed.add_field(
                          name="Callsign Changed",
                          value=f"From **{changes['callsign']['from']}** to **{changes['callsign']['to']}**",
                          inline=False,
                      )
              embeds.append(embed)
  
          if len(embeds) > 1:
              paginator = EmbedPages(ctx, embeds)
              await paginator.paginate()
          else:
              return await manager.send_embed(embeds[0])
  
      @clan.command()
      @is_registered()
      @clan_is_linked()
      @commands.guild_only()
      async def apply(self, ctx):
          """Apply to be a member of the linked clan"""
          manager = MessageManager(ctx)
          requestor_db = await get_requestor(ctx)
          guild_db = await Guild.get_or_none(guild_id=ctx.guild.id)
  
          clan_app_db = await ClanMemberApplication.get_or_none(
              guild=guild_db, member=requestor_db
          )
          if not clan_app_db:
              embed = await self.create_application_embed(ctx, requestor_db, guild_db)
              application_embed = await manager.send_embed(
                  embed, channel_id=guild_db.admin_channel
              )
  
              await ClanMemberApplication.create(
                  guild=guild_db,
                  member=requestor_db,
                  message_id=application_embed.id,
                  approved=False,
              )
  
              await manager.send_and_clean(
                  "Your application has been submitted for admin approval."
              )
          else:
              if not clan_app_db.approved:
                  message = "Your application is still pending for admin approval."
              else:
                  message = "Your application has been approved!"
              return await manager.send_and_clean(message)
  
      @admin.command()
      async def applied(self, ctx):
          redis_cache = self.bot.ext_conns["redis_cache"]
          manager = MessageManager(ctx)
  
          admin_channel = self.bot.get_channel(
              self.bot.guild_map[ctx.guild.id].admin_channel
          )
  
          cursor = b"0"
          while cursor:
              cursor, keys = await redis_cache.scan(
                  cursor, match=f"{ctx.guild.id}-clan-application-*"
              )
  
          if len(keys) > 0:
              for key in keys:
                  embed_packed = await redis_cache.get(key)
                  member_db_id = key.decode("utf-8").split("-")[-1]
                  application_db = await ClanMemberApplication.filter(
                      member_id=member_db_id
                  )
                  previous_message_id = application_db.message_id
  
                  previous_message = await admin_channel.fetch_message(
                      previous_message_id
                  )
                  await previous_message.delete()
  
                  new_message = await manager.send_embed(deserializer(embed_packed))
                  application_db.message_id = new_message.id
                  await application_db.save()
          else:
              await manager.send_and_clean("No applications found.")
  
      @clan.command(usage=f"<{', '.join(constants.SUPPORTED_GAME_MODES.keys())}>")
      @clan_is_linked()
      @is_valid_game_mode()
      @commands.guild_only()
      async def games(self, ctx, game_mode: str):
          """Show totals of all eligible clan games for all members"""
          manager = MessageManager(ctx)
  
          log.info(f"Finding all {game_mode} games for all members")
  
          game_counts = await get_game_counts(self.bot.database, game_mode)
  
          embed = discord.Embed(
              colour=constants.BLUE,
              title=f"Eligible {game_mode.title().replace('Pvp', 'PvP')} Games for All Members",
          )
  
          total_count = 0
          if len(game_counts) == 1:
              (total_count,) = game_counts.values()
          else:
              for game, count in game_counts.items():
                  embed.add_field(name=game.title(), value=str(count))
                  total_count += count
  
          embed.description = str(total_count)
          await manager.send_embed(embed)
  
      @admin.command()
      async def activitytracking(self, ctx):
          """Enable activity tracking on all connected clans (Admin only)"""
          manager = MessageManager(ctx)
  
          clan_dbs = await self.bot.database.get_clans_by_guild(ctx.guild.id)
          for clan_db in clan_dbs:
              if clan_db.activity_tracking:
                  clan_db.activity_tracking = False
              else:
                  clan_db.activity_tracking = True
              await clan_db.save()
              message = (
                  f"Clan activity tracking has been "
                  f"{'enabled' if clan_db.activity_tracking else 'disabled'} "
                  f"for **{clan_db.name}**."
              )
              await manager.send_message(message, mention=False, clean=False)
  
          return await manager.clean_messages()
  
      @admin.command()
      async def update_games(self, ctx, *args):
          manager = MessageManager(ctx)
  
          discord_guild = await self.bot.fetch_guild(ctx.guild.id)
          guild_id = ctx.guild.id
          guild_name = str(discord_guild)
  
          message = f"Queueing task to find recent games for all members of {guild_name} ({guild_id})"
          log.info(message)
          await self.bot.ext_conns["redis_jobs"].enqueue_job(
              "store_all_games",
              guild_id,
              guild_name,
              30,
              False,
              _job_id=f"store_all_games-{guild_id}",
          )
  
          await manager.send_message(message, mention=False, clean=False)
  
      @admin.command()
      async def inactive(self, ctx, *args):
          manager = MessageManager(ctx)
  
          embeds = []
          clan_dbs = await Clan.filter(guild__guild_id=ctx.guild.id).prefetch_related(
              "guild"
          )
          for clan_db in clan_dbs:
              embed = discord.Embed(
                  colour=constants.BLUE,
                  title=f"Clan Members in {clan_db.name} Inactive for over 30 Days",
              )
  
              inactive_members = await self.get_inactive_members(ctx, clan_db)
              if len(inactive_members) == 0:
                  embed.add_field(name="None", value="-")
              else:
                  for inactive_member in inactive_members:
                      time_delta, _, _, username = inactive_member
                      months, remainder = divmod(time_delta, 2628000)
                      days, _ = divmod(remainder, 86400)
                      embed.add_field(
                          name=username, value=f"{int(months)} months {int(days)} days"
                      )
  
              embeds.append(embed)
  
          for embed in embeds:
              await manager.send_embed(embed)
  
      @admin.command()
      async def kick(self, ctx, *args):
          manager = MessageManager(ctx)
          username = " ".join(args)
  
          admin_db = await self.bot.database.get_member_by_discord_id(
              ctx.author.id, include_clan=True
          )
  
          member_db = await self.get_member_db(ctx, username, include_clan=True)
          if not member_db:
              return await manager.send_and_clean(f"Could not find username `{username}`")
  
          platform_id, membership_id, username = get_primary_membership(member_db)
  
          confirm = {constants.EMOJI_CHECKMARK: True, constants.EMOJI_CROSSMARK: False}
  
          confirm_res = await manager.send_message_react(
              f"Kick **{username}**?", reactions=confirm.keys(), clean=False
          )
          if confirm_res == constants.EMOJI_CROSSMARK:
              return await manager.send_and_clean("Canceling command")
  
          await execute_pydest_auth(
              self.bot.ext_conns,
              self.bot.destiny.api.group_kick_member,
              admin_db.member,
              manager,
              group_id=admin_db.clan.clan_id,
              membership_type=platform_id,
              membership_id=membership_id,
              access_token=admin_db.member.bungie_access_token,
          )
  
          await member_db.delete()  # TODO
  
          return await manager.send_message(
              f"Member **{username}** has been kicked from {admin_db.clan.name}"
          )
  
      @admin.command()
      async def purge(self, ctx, *args):
          manager = MessageManager(ctx)
  
          admin_db = await self.bot.database.get_member_by_discord_id(ctx.author.id)
  
          inactive_members = await self.get_inactive_members(ctx, admin_db.clan)
          if not inactive_members:
              return await manager.send_message(
                  f"Clan {admin_db.clan.name} has no inactive members in the past 30 days"
              )
  
          roles_db = await Role.filter(
              Q(
                  Q(guild__guild_id=ctx.guild.id),
                  Q(
                      Q(is_clanmember=True),
                      Q(is_new_clanmember=True),
                      Q(is_non_clanmember=True),
                      join_type="OR",
                  ),
              )
          )
          member_roles = [
              ctx.guild.get_role(role_db.role_id)
              for role_db in roles_db
              if role_db.is_clanmember or role_db.is_new_clanmember
          ]
  
          non_member_roles = [
              ctx.guild.get_role(role_db.role_id)
              for role_db in roles_db
              if role_db.is_non_clanmember
          ]
  
          announcements = []
          for member in inactive_members:
              time_delta, platform_id, membership_id, username = member
              months, remainder = divmod(time_delta, 2628000)
              days, _ = divmod(remainder, 86400)
  
              member_db = await self.bot.database.get_clan_member_by_platform(
                  membership_id, platform_id, [admin_db.clan.id]
              )
  
              kick_message = f"Kick **{username}** "
  
              member_discord = None
              if member_db.member.discord_id:
                  member_discord = ctx.guild.get_member(member_db.member.discord_id)
                  if member_discord:
                      kick_message += f"(Discord: {member_discord.display_name}), "
  
              if not member_discord:
                  kick_message += "(not in this Discord server or not registered), "
  
              kick_message += f"inactive for {int(months)} months {int(days)} days?"
  
              confirm = {
                  constants.EMOJI_CHECKMARK: True,
                  constants.EMOJI_CROSSMARK: False,
              }
  
              confirm_res = await manager.send_message_react(
                  kick_message, reactions=confirm.keys(), clean=False, with_cancel=True
              )
              if confirm_res == constants.EMOJI_CROSSMARK:
                  continue
              elif not confirm_res:
                  return await manager.send_and_clean("Canceling command")
  
              await execute_pydest_auth(
                  self.bot.ext_conns,
                  self.bot.destiny.api.group_kick_member,
                  admin_db.member,
                  manager,
                  group_id=admin_db.clan.clan_id,
                  membership_type=platform_id,
                  membership_id=membership_id,
                  access_token=admin_db.bungie_access_token,
              )
  
              await member_db.delete()  # TODO
  
              announcement_base = (
                  f"has been kicked from {admin_db.clan.name} after being inactive "
                  f"for {int(months)} months {int(days)} days"
              )
              await manager.send_message(
                  f"Member **{username}** {announcement_base}", mention=False, clean=False
              )
  
              if member_discord:
                  await member_discord.remove_roles(*member_roles)
                  await member_discord.add_roles(*non_member_roles)
                  announcement = f"{member_discord.mention} {announcement_base}"
              else:
                  announcement = f"Member **{username}** {announcement_base}"
  
              announcements.append(announcement)
  
          if announcements:
              # await set_cached_members(self.bot.ext_conns, ctx.guild.id, ctx.guild.name)
  
              announcement_channel = ctx.guild.get_channel(
                  self.bot.guild_map[ctx.guild.id].announcement_channel
              )
              if announcement_channel:
                  await announcement_channel.send("\n".join(announcements))
  
  
  def setup(bot):
      bot.add_cog(ClanCog(bot))
  