import copy
import json
import os
import re
import time
from enum import Enum
from typing import Union

import disnake
from disnake import InteractionMessage
from disnake.ext import commands
from disnake.ui import Button

import utils
from cogs.help import help, handle_error, help_category

"""
  Environment Variablen:
  DISCORD_LEARNINGGROUPS_OPEN - ID der Kategorie für offene Lerngruppen
  DISCORD_LEARNINGGROUPS_CLOSE - ID der Kategorie für private Lerngruppen
  DISCORD_LEARNINGGROUPS_ARCHIVE - ID der Kategorie für archivierte Lerngruppen
  DISCORD_LEARNINGGROUPS_REQUEST - ID des Channels in welchem Requests vom Bot eingestellt werden
  DISCORD_LEARNINGGROUPS_INFO - ID des Channels in welchem die Lerngruppen-Informationen gepostet/aktualisert werden
  DISCORD_LEARNINGGROUPS_FILE - Name der Datei mit Verwaltungsdaten der Lerngruppen (minimaler Inhalt: {"requested": {},"groups": {}})
  DISCORD_LEARNINGGROUPS_COURSE_FILE - Name der Datei welche die Kursnamen für die Lerngruppen-Informationen enthält (minimalter Inhalt: {})
  DISCORD_MOD_ROLE - ID der Moderator Rolle von der erweiterte Lerngruppen-Actionen ausgeführt werden dürfen
"""

LG_OPEN_SYMBOL = f'🌲'
LG_CLOSE_SYMBOL = f'🛑'
LG_PRIVATE_SYMBOL = f'🚪'
LG_LISTED_SYMBOL = f'📖'


class GroupState(Enum):
    OPEN = "OPEN"
    CLOSED = "CLOSED"
    PRIVATE = "PRIVATE"
    ARCHIVED = "ARCHIVED"
    REMOVED = "REMOVED"


@help_category("learninggroups", "Lerngruppen",
               "Mit dem Lerngruppen-Feature kannst du Lerngruppen-Kanäle beantragen und verwalten.",
               "Hier kannst du Lerngruppen-Kanäle anlegen, beantragen und verwalten.")
class LearningGroups(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        # ratelimit 2 in 10 minutes (305 * 2 = 610 = 10 minutes and 10 seconds)
        self.rename_ratelimit = 305
        self.msg_max_len = 2000

        self.categories = {
            GroupState.OPEN: os.getenv('DISCORD_LEARNINGGROUPS_OPEN'),
            GroupState.CLOSED: os.getenv('DISCORD_LEARNINGGROUPS_CLOSE'),
            GroupState.PRIVATE: os.getenv('DISCORD_LEARNINGGROUPS_PRIVATE'),
            GroupState.ARCHIVED: os.getenv('DISCORD_LEARNINGGROUPS_ARCHIVE')
        }
        self.symbols = {
            GroupState.OPEN: LG_OPEN_SYMBOL,
            GroupState.CLOSED: LG_CLOSE_SYMBOL,
            GroupState.PRIVATE: LG_PRIVATE_SYMBOL
        }
        self.channel_request = os.getenv('DISCORD_LEARNINGGROUPS_REQUEST')
        self.channel_info = os.getenv('DISCORD_LEARNINGGROUPS_INFO')
        self.group_file = os.getenv('DISCORD_LEARNINGGROUPS_FILE')
        self.header_file = os.getenv('DISCORD_LEARNINGGROUPS_COURSE_FILE')
        self.support_channel = os.getenv('DISCORD_SUPPORT_CHANNEL')
        self.mod_role = os.getenv("DISCORD_MOD_ROLE")
        self.guild_id = os.getenv("DISCORD_GUILD")
        self.groups = {}  # owner and learninggroup-member ids
        self.channels = {}  # complete channel configs
        self.header = {}  # headlines for statusmessage
        self.load_groups()
        self.load_header()

    @commands.Cog.listener()
    async def on_button_click(self, interaction: InteractionMessage):
        button: Button = interaction.component

        if button.custom_id == "learninggroups:group_yes":
            await self.on_group_request(True, button, interaction)
        elif button.custom_id == "learninggroups:group_no":
            await self.on_group_request(False, button, interaction)
        elif button.custom_id == "learninggroups:join_yes":
            await self.on_join_request(True, button, interaction)
        elif button.custom_id == "learninggroups:join_no":
            await self.on_join_request(False, button, interaction)


    @commands.Cog.listener(name="on_ready")
    async def on_ready(self):
        await self.update_channels()

    def load_header(self):
        file = open(self.header_file, mode='r')
        self.header = json.load(file)

    def save_header(self):
        file = open(self.header_file, mode='w')
        json.dump(self.header, file)

    def load_groups(self):
        group_file = open(self.group_file, mode='r')
        self.groups = json.load(group_file)
        if not self.groups.get("groups"):
            self.groups['groups'] = {}
        if not self.groups.get("requested"):
            self.groups['requested'] = {}
        if not self.groups.get("messageids"):
            self.groups['messageids'] = []

        for _, group in self.groups['requested'].items():
            group["state"] = GroupState[group["state"]]

    async def save_groups(self):
        await self.update_channels()
        group_file = open(self.group_file, mode='w')

        groups = copy.deepcopy(self.groups)

        for _, group in groups['requested'].items():
            group["state"] = group["state"].name
        json.dump(groups, group_file)

    def arg_state_to_group_state(self, state: str):
        if state in ["offen", "open", "o"]:
            return GroupState.OPEN
        if state in ["geschlossen", "closed", "close"]:
            return GroupState.CLOSED
        if state in ["private", "privat"]:
            return GroupState.PRIVATE
        return None

    def is_request_owner(self, request, member):
        return request["owner_id"] == member.id

    def is_group_owner(self, channel, member):
        channel_config = self.groups["groups"].get(str(channel.id))
        if channel_config:
            return channel_config["owner_id"] == member.id
        return False

    def is_mod(self, member):
        roles = member.roles
        for role in roles:
            if role.id == int(self.mod_role):
                return True

        return False

    def is_group_request_message(self, message):
        return len(message.embeds) > 0 and message.embeds[0].title == "Lerngruppenanfrage!"

    async def is_channel_config_valid(self, ctx, channel_config, command=None):
        if channel_config['state'] is None:
            if command:
                await ctx.channel.send(
                    f"Fehler! Bitte gib an ob die Gruppe **offen** (**open**) **geschlossen** (**closed**) oder **privat** (**private**) ist. Gib `!help {command}` für Details ein.")
            return False
        if not re.match(r"^[0-9]+$", channel_config['course']):
            if command:
                await ctx.channel.send(
                    f"Fehler! Die Kursnummer muss numerisch sein. Gib `!help {command}` für Details ein.")
            return False
        if not re.match(r"^(sose|wise)[0-9]{2}$", channel_config['semester']):
            if command:
                await ctx.channel.send(
                    f"Fehler! Das Semester muss mit **sose** oder **wise** angegeben werden gefolgt von der **zweistelligen Jahreszahl**. Gib `!help {command}` für Details ein.")
            return False
        return True

    async def check_rename_rate_limit(self, channel_config):
        if channel_config.get("last_rename") is None:
            return False
        now = int(time.time())
        seconds = channel_config["last_rename"] + self.rename_ratelimit - now
        if seconds > 0:
            channel = await self.bot.fetch_channel(int(channel_config["channel_id"]))
            await channel.send(f"Discord limitiert die Aufrufe für manche Funktionen, daher kannst du diese Aktion erst wieder in {seconds} Sekunden ausführen.")
        return seconds > 0

    async def category_of_channel(self, state: GroupState):
        category_to_fetch = self.categories[state]
        category = await self.bot.fetch_channel(category_to_fetch)
        return category

    def full_channel_name(self, channel_config):
        return (f"{self.symbols[channel_config['state']]}"
                f"{channel_config['course']}-{channel_config['name']}-{channel_config['semester']}"
                f"{LG_LISTED_SYMBOL if channel_config['is_listed'] else ''}")

    async def update_statusmessage(self):
        info_message_ids = self.groups.get("messageids")
        channel = await self.bot.fetch_channel(int(self.channel_info))

        for info_message_id in info_message_ids:
            message = await channel.fetch_message(info_message_id)
            await message.delete()

        info_message_ids = []

        msg = f"**Lerngruppen**\n\n"
        course_msg = ""
        sorted_channels = sorted(self.channels.values(
        ), key=lambda channel: f"{channel['course']}-{channel['name']}")
        open_channels = [channel for channel in sorted_channels if channel['state'] in [GroupState.OPEN]
                         or channel['is_listed']]
        courseheader = None
        no_headers = []
        for lg_channel in open_channels:

            if lg_channel['course'] != courseheader:
                if len(msg) + len(course_msg) > self.msg_max_len:
                    message = await channel.send(msg)
                    info_message_ids.append(message.id)
                    msg = course_msg
                    course_msg = ""
                else:
                    msg += course_msg
                    course_msg = ""
                header = self.header.get(lg_channel['course'])
                if header:
                    course_msg += f"**{header}**\n"
                else:
                    course_msg += f"**{lg_channel['course']} - -------------------------------------**\n"
                    no_headers.append(lg_channel['course'])
                courseheader = lg_channel['course']

            groupchannel = await self.bot.fetch_channel(int(lg_channel['channel_id']))
            course_msg += f"    {groupchannel.mention}"

            if lg_channel['is_listed'] and lg_channel['state'] == GroupState.PRIVATE:
                group_config = self.groups["groups"].get(lg_channel['channel_id'])
                if group_config:
                    user = await self.bot.fetch_user(group_config['owner_id'])
                    if user:
                        course_msg += f" **@{user.name}#{user.discriminator}**"
                course_msg +=  f"\n       **↳** `!lg join {groupchannel.id}`"
            course_msg += "\n"

        msg += course_msg
        message = await channel.send(msg)
        if len(no_headers) > 0:
            support_channel = await self.bot.fetch_channel(int(self.support_channel))
            if support_channel:
                await support_channel.send(f"Es fehlen noch Überschriften für folgende Kurse in der Lerngruppenübersicht: **{', '.join(no_headers)}**")
        info_message_ids.append(message.id)
        self.groups["messageids"] = info_message_ids
        await self.save_groups()

    async def archive(self, channel):
        group_config = self.groups["groups"].get(str(channel.id))
        if not group_config:
            await channel.send("Das ist kein Lerngruppenkanal.")
            return
        category = await self.bot.fetch_channel(self.categories[GroupState.ARCHIVED])
        await self.move_channel(channel, category)
        await channel.edit(name=f"archiv-${channel.name[1:]}")
        await self.remove_group(channel)
        await self.update_permissions(channel)

    async def set_channel_state(self, channel, state: GroupState = None):
        channel_config = self.channels[str(channel.id)]
        if await self.check_rename_rate_limit(channel_config):
            return False  # prevent api requests when ratelimited

        if state is not None:
            old_state = channel_config["state"]
            if old_state == state:
                return False  # prevent api requests when nothing changed
            channel_config["state"] = state
            await self.alter_channel(channel, channel_config)
            return True

    async def set_channel_listing(self, channel, is_listed):
        channel_config = self.channels[str(channel.id)]
        if await self.check_rename_rate_limit(channel_config):
            return False  # prevent api requests when ratelimited
        if channel_config["state"] in [GroupState.CLOSED, GroupState.PRIVATE]:
            was_listed = channel_config["is_listed"]
            if was_listed == is_listed:
                return False  # prevent api requests when nothing changed
            channel_config["is_listed"] = is_listed
            await self.alter_channel(channel, channel_config)
            return True

    async def alter_channel(self, channel, channel_config):
        self.groups["groups"][str(channel.id)]["last_rename"] = int(time.time())
        await channel.edit(name=self.full_channel_name(channel_config))
        category = await self.category_of_channel(channel_config["state"])
        await self.move_channel(channel, category,
                                sync=True if channel_config["state"] in [GroupState.OPEN, GroupState.CLOSED] else False)
        await self.save_groups()
        await self.update_statusmessage()
        return True

    async def set_channel_name(self, channel, name):
        channel_config = self.channels[str(channel.id)]

        if await self.check_rename_rate_limit(channel_config):
            return  # prevent api requests when ratelimited

        self.groups["groups"][str(channel.id)]["last_rename"] = int(time.time())
        channel_config["name"] = name

        await channel.edit(name=self.full_channel_name(channel_config))
        await self.save_groups()
        await self.update_statusmessage()

    async def move_channel(self, channel, category, sync=True):
        for sortchannel in category.text_channels:
            if sortchannel.name[1:] > channel.name[1:]:
                await channel.move(category=category, before=sortchannel, sync_permissions=sync)
                return
        await channel.move(category=category, sync_permissions=sync, end=True)

    async def add_requested_group_channel(self, message, direct=False):
        requested_channel_config = self.groups["requested"].get(str(message.id))

        category = await self.category_of_channel(requested_channel_config["state"])
        full_channel_name = self.full_channel_name(requested_channel_config)
        channel = await category.create_text_channel(full_channel_name)
        await self.move_channel(channel, category, False)
        user = await self.bot.fetch_user(requested_channel_config["owner_id"])

        await channel.send(f":wave: <@!{user.id}>, hier ist deine neue Lerngruppe.\n"
                           "Es gibt offene und private Lerngruppen. Eine offene Lerngruppe ist für jeden sichtbar "
                           "und jeder kann darin schreiben. Eine private Lerngruppe ist unsichtbar und auf eine "
                           "Gruppe an Kommilitoninnen beschränkt."
                           "```"
                           "Besitzerinfunktionen:\n"
                           "!lg addmember <@newmember>: Fügt ein Mitglied zur Lerngruppe hinzu.\n"                           
                           "!lg owner <@newowner>: Ändert die Besitzerin der Lerngruppe auf @newowner.\n"
                           "!lg open: Öffnet eine Lerngruppe.\n"
                           "!lg close: Schließt eine Lerngruppe.\n"
                           "!lg private: Stellt die Lerngruppe auf privat.\n"
                           "!lg show: Zeigt eine private oder geschlossene Lerngruppe in der Lerngruppenliste an.\n"
                           "!lg hide: Entfernt eine private oder geschlossene Lerngruppe aus der Lerngruppenliste.\n"
                           "!lg kick <@user>: Schließt eine Benutzerin von der Lerngruppe aus.\n"   
                           "\nKommandos für alle:\n"
                           "!lg id: Zeigt die ID der Lerngruppe an mit der andere Kommilitoninnen beitreten können.\n"
                           "!lg members: Zeigt die Mitglieder der Lerngruppe an.\n"
                           "!lg owner: Zeigt die Besitzerin der Lerngruppe.\n"
                           "!lg leave: Du verlässt die Lerngruppe.\n"
                           "!lg join: Anfrage stellen in die Lerngruppe aufgenommen zu werden.\n"
                           "\nMit dem nachfolgenden Kommando kann eine Kommilitonin darum "
                           "bitten in die Lerngruppe aufgenommen zu werden wenn diese bereits privat ist.\n"
                           f"!lg join {channel.id}"
                            "\n(manche Kommandos sind von Discord limitiert und können nur einmal alle 5 Minuten ausgeführt werden)"
                           "```"
                           )
        self.groups["groups"][str(channel.id)] = {
            "owner_id": requested_channel_config["owner_id"],
            "last_rename": int(time.time())
        }

        await self.remove_group_request(message)
        if not direct:
            await message.delete()

        await self.save_groups()
        await self.update_statusmessage()
        if requested_channel_config["state"] is GroupState.PRIVATE:
            await self.update_permissions(channel)

    async def remove_group_request(self, message):
        del self.groups["requested"][str(message.id)]
        await self.save_groups()

    async def remove_group(self, channel):
        del self.groups["groups"][str(channel.id)]
        await self.save_groups()

    def channel_to_channel_config(self, channel):
        cid = str(channel.id)
        is_listed = channel.name[-1] == LG_LISTED_SYMBOL
        result = re.match(r"([0-9]+)-(.*)-([a-z0-9]+)$", channel.name[1:] if not is_listed else channel.name[1:-1])

        state = None
        if channel.name[0] == LG_OPEN_SYMBOL:
            state = GroupState.OPEN
        elif channel.name[0] == LG_CLOSE_SYMBOL:
            state = GroupState.CLOSED
        elif channel.name[0] == LG_PRIVATE_SYMBOL:
            state = GroupState.PRIVATE

        course, name, semester = result.group(1, 2, 3)

        channel_config = {"course": course, "name": name, "category": channel.category_id, "semester": semester,
                          "state": state, "is_listed": is_listed, "channel_id": cid}
        if self.groups["groups"].get(cid):
            channel_config.update(self.groups["groups"].get(cid))
        return channel_config

    async def update_channels(self):
        self.channels = {}
        for state in [GroupState.OPEN, GroupState.CLOSED, GroupState.PRIVATE]:
            category = await self.category_of_channel(state)

            for channel in category.text_channels:
                channel_config = self.channel_to_channel_config(channel)

                self.channels[str(channel.id)] = channel_config

    async def add_member_to_group(self, channel: disnake.TextChannel, arg_member: disnake.Member, send_message=True):
        group_config = self.groups["groups"].get(str(channel.id))
        if not group_config:
            await channel.send("Das ist kein Lerngruppenkanal.")
            return

        users = group_config.get("users")
        if not users:
            users = {}
        mid = str(arg_member.id)
        if not users.get(mid):
            users[mid] = True
            user = await self.bot.fetch_user(mid)
            if user and send_message:
                await utils.send_dm(user, f"Du wurdest in die Lerngruppe <#{channel.id}> aufgenommen. " 
                                          "Viel Spass beim gemeinsamen Lernen!\n"
                                          "Dieser Link führt dich direkt zum Lerngruppen-Channel. " 
                                          "Diese Nachricht kannst du bei Bedarf in unserer Unterhaltung " 
                                          "über Rechtsklick anpinnen.")

        group_config["users"] = users

        await self.save_groups()

    async def remove_member_from_group(self, channel: disnake.TextChannel, arg_member: disnake.Member, send_message=True):
        group_config = self.groups["groups"].get(str(channel.id))
        if not group_config:
            await channel.send("Das ist kein Lerngruppenkanal.")
            return

        users = group_config.get("users")
        if not users:
            return
        mid = str(arg_member.id)
        if users.pop(mid, None):
            user = await self.bot.fetch_user(mid)
            if user and send_message:
                await utils.send_dm(user, f"Du wurdest aus der Lerngruppe {channel.name} entfernt")

        await self.save_groups()

    async def update_permissions(self, channel):
        channel_config = self.channels[str(channel.id)]
        if channel_config.get("state") == GroupState.PRIVATE:
            overwrites = await self.overwrites(channel)
            await channel.edit(overwrites=overwrites)
        else:
            await channel.edit(sync_permissions=True)

    async def overwrites(self, channel):
        channel = await self.bot.fetch_channel(str(channel.id))
        group_config = self.groups["groups"].get(str(channel.id))
        guild = await self.bot.fetch_guild(int(self.guild_id))
        mods = guild.get_role(int(self.mod_role))

        overwrites = {
            mods: disnake.PermissionOverwrite(read_messages=True),
            guild.default_role: disnake.PermissionOverwrite(read_messages=False)
        }

        if not group_config:
            return overwrites

        owner = self.bot.get_user(group_config["owner_id"])
        if not owner:
            return overwrites

        overwrites[owner] = disnake.PermissionOverwrite(read_messages=True)
        users = group_config.get("users")
        if not users:
            return overwrites

        for userid in users.keys():
            user = await self.bot.fetch_user(userid)
            overwrites[user] = disnake.PermissionOverwrite(read_messages=True)

        return overwrites

    @help(
        category="learninggroups",
        syntax="!lg <command>",
        brief="Lerngruppenverwaltung"
    )
    @commands.group(name="lg", aliases=["learninggroup", "lerngruppe"], pass_context=True)
    async def cmd_lg(self, ctx):
        if not ctx.invoked_subcommand:
            await ctx.channel.send("Gib `!help lg` ein um eine Übersicht über die Lerngruppen-Kommandos zu erhalten.")

    @help(
        command_group="lg",
        category="learninggroups",
        brief="Updated die Lerngruppenliste",
        mod=True
    )
    @cmd_lg.command(name="update")
    @commands.check(utils.is_mod)
    async def cmd_update(self, ctx):
        await self.update_channels()
        await self.update_statusmessage()

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg header <coursenumber> <name...>",
        brief="Fügt einen Kurs als neue Überschrift in Botys Lerngruppen-Liste (Kanal #lerngruppen) hinzu. "
              "Darf Leerzeichen enthalten, Anführungszeichen sind nicht erforderlich.",
        example="!lg header 1141 Mathematische Grundlagen",
        parameters={
            "coursenumber": "Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).",
            "name...": "Ein frei wählbarer Text (darf Leerzeichen enthalten).",
        },
        description="Kann auch zum Bearbeiten einer Überschrift genutzt werden. Bei bereits existierender "
                    "Kursnummer wird die Überschrift abgeändert",
        mod=True
    )
    @cmd_lg.command(name="header")
    @commands.check(utils.is_mod)
    async def cmd_add_header(self, ctx, arg_course, *arg_name):
        if not re.match(r"[0-9]+", arg_course):
            await ctx.channel.send(
                f"Fehler! Die Kursnummer muss numerisch sein. Gib `!help add-course` für Details ein.")
            return

        self.header[arg_course] = f"{arg_course} - {' '.join(arg_name)}"
        self.save_header()
        await self.update_statusmessage()

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg add <coursenumber> <name> <semester> <status> <@usermention>",
        example="!lg add 1142 mathegenies sose22 closed @someuser",
        brief="Fügt einen Lerngruppen-Kanal hinzu. Der Name darf keine Leerzeichen enthalten.",
        parameters={
            "coursenumber": "Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).",
            "name": "Ein frei wählbarer Text ohne Leerzeichen. Bindestriche sind zulässig.",
            "semester": ("Das Semester, für welches diese Lerngruppe erstellt werden soll."
                         "sose oder wise gefolgt von der zweistelligen Jahreszahl (z. B. sose22)."),
            "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed).",
            "@usermention": "Die so erwähnte Benutzerin wird als Besitzerin für die Lerngruppe gesetzt."
        },
        mod=True
    )
    @cmd_lg.command(name="add")
    @commands.check(utils.is_mod)
    async def cmd_add_group(self, ctx, arg_course, arg_name, arg_semester, arg_state, arg_owner: disnake.Member):
        state = self.arg_state_to_group_state(arg_state)
        channel_config = {"owner_id": arg_owner.id, "course": arg_course, "name": arg_name, "semester": arg_semester,
                          "state": state, "is_listed": False}

        if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name):
            return

        self.groups["requested"][str(ctx.message.id)] = channel_config
        await self.save_groups()
        await self.add_requested_group_channel(ctx.message, direct=True)

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg request <coursenumber> <name> <semester> <status>",
        brief="Stellt eine Anfrage für einen neuen Lerngruppen-Kanal.",
        example="!lg request 1142 mathegenies sose22 closed",
        description=("Moderatorinnen können diese Anfrage bestätigen, dann wird die Gruppe eingerichtet. "
                     "Die Besitzerin der Gruppe ist die Benutzerin die die Anfrage eingestellt hat."),
        parameters={
            "coursenumber": "Nummer des Kurses, wie von der FernUni angegeben (ohne führende Nullen z. B. 1142).",
            "name": "Ein frei wählbarer Text ohne Leerzeichen.",
            "semester": "Das Semester, für welches diese Lerngruppe erstellt werden soll. sose oder wise gefolgt "
            "von der zweistelligen Jahreszahl (z. B. sose22).",
            "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed)."
        }
    )
    @cmd_lg.command(name="request", aliases=["r", "req"])
    async def cmd_request_group(self, ctx, arg_course, arg_name, arg_semester, arg_state):
        state = self.arg_state_to_group_state(arg_state)
        arg_name = re.sub(
            r"[^A-Za-zäöüß0-9-]",
            "",
            arg_name.lower().replace(" ", "-")
        )
        arg_semester = arg_semester.lower()
        if len(arg_semester) == 8:
            arg_semester = f"{arg_semester[0:4]}{arg_semester[-2:]}"
        channel_config = {"owner_id": ctx.author.id, "course": arg_course, "name": arg_name, "semester": arg_semester,
                          "state": state, "is_listed": False}

        if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name):
            return

        channel = await self.bot.fetch_channel(int(self.channel_request))
        channel_name = self.full_channel_name(channel_config)

        message = await utils.confirm(
            channel=channel,
            title="Lerngruppenanfrage",
            description=f"<@!{ctx.author.id}> möchte gerne die Lerngruppe **#{channel_name}** eröffnen.",
            custom_prefix="learninggroups:group"
        )
        self.groups["requested"][str(message.id)] = channel_config
        await self.save_groups()

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg show",
        brief="Zeigt einen privaten Lerngruppenkanal trotzdem in der Liste an.",
        description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. "
                     "Die Lerngruppe wird in der Übersicht der Lerngruppen gelistet, so können Kommilitoninnen noch "
                     "Anfragen stellen, um in die Lerngruppe aufgenommen zu werden."
                     "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ")
    )
    @cmd_lg.command(name="show")
    async def cmd_show(self, ctx):
        if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx):
            channel_config = self.channels[str(ctx.channel.id)]
            if channel_config:
                if channel_config.get("state") == GroupState.PRIVATE:
                    if await self.set_channel_listing(ctx.channel, True):
                        await ctx.channel.send("Die Lerngruppe wird nun in der Lerngruppenliste angezeigt.")
                elif channel_config.get("state") == GroupState.OPEN:
                    await ctx.channel.send("Nichts zu tun. Offene Lerngruppen werden sowieso in der Liste angezeigt.")
                elif channel_config.get("state") == GroupState.CLOSED:
                    await ctx.channel.send("Möchtest du die Gruppen öffnen? Versuch‘s mit `!lg open`")


    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg hide",
        brief="Versteckt einen privaten Lerngruppenkanal. ",
        description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. "
                     "Die Lerngruppe wird nicht mehr in der Liste der Lerngruppen aufgeführt. "
                     "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ")
    )
    @cmd_lg.command(name="hide")
    async def cmd_hide(self, ctx):
        if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx):
            channel_config = self.channels[str(ctx.channel.id)]
            if channel_config:
                if channel_config.get("state") == GroupState.PRIVATE:
                    if await self.set_channel_listing(ctx.channel, False):
                        await ctx.channel.send("Die Lerngruppe wird nun nicht mehr in der Lerngruppenliste angezeigt.")
                    return

                elif channel_config.get("state") == GroupState.OPEN:
                    await ctx.channel.send("Offene Lerngruppen können nicht aus der Lerngruppenliste entfernt werden. " 
                                           "Führe `!lg close` aus um die Lerngruppe zu schließen, "
                                           "oder `!lg private` um diese auf "
                                           "privat zu schalten.")
                elif channel_config.get("state") == GroupState.CLOSED:
                    await ctx.channel.send("Wenn diese Gruppe privat werden soll, ist das Kommando das du brauchst: `!lg private`")

    @cmd_lg.command(name="debug")
    @commands.check(utils.is_mod)
    async def cmd_debug(self, ctx):
        channel_config = self.channels[str(ctx.channel.id)]
        if not channel_config:
            await ctx.channel.send("None")
            return
        await ctx.channel.send(str(channel_config))


    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg open",
        brief="Öffnet den Lerngruppen-Kanal wenn du die Besitzerin bist. ",
        description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. "
                     "Verschiebt den Lerngruppen-Kanal in die Kategorie für offene Kanäle und ändert das Icon. "
                     "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ")
    )
    @cmd_lg.command(name="open", aliases=["opened", "offen"])
    async def cmd_open(self, ctx):
        if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx):
            await self.set_channel_state(ctx.channel, state=GroupState.OPEN)

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg close",
        brief="Schließt den Lerngruppen-Kanal wenn du die Besitzerin bist. ",
        description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. "
                     "Stellt die Lerngruppe auf geschlossen. Dies ist rein symbolisch und zeigt an, "
                     "dass keine neuen Mitglieder mehr aufgenommen werden. "
                     "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ")
    )
    @cmd_lg.command(name="close", aliases=["closed", "geschlossen"])
    async def cmd_close(self, ctx):
        if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx):
            await self.set_channel_state(ctx.channel, state=GroupState.CLOSED)

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg private",
        brief="Macht aus deiner Lerngruppe eine private Lerngruppe wenn du die Besitzerin bist. ",
        description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. "
                     "Stellt die Lerngruppe auf privat. Es haben nur noch Mitglieder "
                     "der Lerngruppe zugriff auf den Kanal. (siehe `!lg members`)"
                     "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ")
    )
    @cmd_lg.command(name="private", aliases=["privat"])
    async def cmd_private(self, ctx):
        if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx):
            if await self.set_channel_state(ctx.channel, state=GroupState.PRIVATE):
                await self.update_permissions(ctx.channel)



    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg rename <name>",
        brief="Ändert den Namen des Lerngruppen-Kanals, in dem das Komando ausgeführt wird.",
        example="!lg rename matheluschen",
        description="Aus #1142-matheprofis-sose22 wird nach dem Aufruf des Beispiels #1142-matheluschen-sose22.",
        parameters={
            "name": "Der neue Name der Lerngruppe ohne Leerzeichen."
        },
        mod=True
    )
    @cmd_lg.command(name="rename")
    @commands.check(utils.is_mod)
    async def cmd_rename(self, ctx, arg_name):
        await self.set_channel_name(ctx.channel, arg_name)

    @help(
        command_group="lg",
        syntax="!lg archive",
        category="learninggroups",
        brief="Archiviert den Lerngruppen-Kanal",
        description="Verschiebt den Lerngruppen-Kanal, in welchem dieses Kommando ausgeführt wird, ins Archiv.",
        mod=True
    )
    @cmd_lg.command(name="archive", aliases=["archiv"])
    @commands.check(utils.is_mod)
    async def cmd_archive(self, ctx):
        await self.archive(ctx.channel)

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg owner <@usermention>",
        example="!owner @someuser",
        brief="Setzt die Besitzerin eines Lerngruppen-Kanals",
        description="Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. ",
        parameters={
            "@usermention": "Die neue Besitzerin der Lerngruppe."
        }
    )
    @cmd_lg.command(name="owner")
    async def cmd_owner(self, ctx, new_owner: disnake.Member = None):
        group_config = self.groups["groups"].get(str(ctx.channel.id))

        if not group_config:
            self.groups["groups"][str(ctx.channel.id)] = {}
            group_config = self.groups["groups"][str(ctx.channel.id)]

        owner_id = group_config.get("owner_id")

        if not owner_id:
            return

        if not new_owner:
                user = await self.bot.fetch_user(owner_id)
                await ctx.channel.send(f"Besitzerin: @{user.name}#{user.discriminator}")

        elif isinstance(group_config, dict):
            owner = await self.bot.fetch_user(owner_id)
            if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx):
                group_config["owner_id"] = new_owner.id
                await self.remove_member_from_group(ctx.channel, new_owner, False)
                if new_owner != owner:
                    await self.add_member_to_group(ctx.channel, owner, False)
                await self.save_groups()
                await self.update_permissions(ctx.channel)
                await ctx.channel.send(
                    f"Glückwunsch {new_owner.mention}! Du bist jetzt die Besitzerin dieser Lerngruppe.")

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg addmember <@usermention> <#channel>",
        example="!lg addmember @someuser #1141-mathegl-lerngruppe-sose21",
        brief="Fügt eine Benutzerin zu einer Lerngruppe hinzu.",
        parameters={
            "@usermention": "Die so erwähnte Benutzerin wird zur Lerngruppe hinzugefügt.",
            "#channel": "(optional) Der Kanal dem die Benutzerin hinzugefügt werden soll."
        }
    )
    @cmd_lg.command(name="addmember", aliases=["addm", "am"])
    async def cmd_add_member(self, ctx, arg_member: disnake.Member, arg_channel: disnake.TextChannel = None):
        if not arg_channel:
            if not self.channels.get(str(ctx.channel.id)):
                await ctx.channel.send("Wenn das Kommando außerhalb eines Lerngruppenkanals aufgerufen wird, muss der" 
                                       "Lerngruppenkanal angehängt werden. `!lg addmember <@usermention> <#channel>`")
                return
            arg_channel = ctx.channel
        if self.is_group_owner(arg_channel, ctx.author) or utils.is_mod(ctx):
            await self.add_member_to_group(arg_channel, arg_member)
            await self.update_permissions(arg_channel)

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg removemember <@usermention> <#channel>",
        example="!lg removemember @someuser #1141-mathegl-lerngruppe-sose21",
        brief="Entfernt eine Benutzerin aus einer Lerngruppe.",
        parameters={
            "#channel": "Der Kanal aus dem die Benutzerin gelöscht werden soll.",
            "@usermention": "Die so erwähnte Benutzerin wird aus der Lerngruppe entfernt."
        },
        mod=True
    )
    @cmd_lg.command(name="removemember", aliases=["remm", "rm"])
    @commands.check(utils.is_mod)
    async def cmd_remove_member(self, ctx, arg_member: disnake.Member, arg_channel: disnake.TextChannel):
        await self.remove_member_from_group(arg_channel, arg_member)
        await self.update_permissions(arg_channel)

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg members",
        brief="Listet die Mitglieder der Lerngruppe auf.",
    )
    @cmd_lg.command(name="members")
    async def cmd_members(self, ctx):
        group_config = self.groups["groups"].get(str(ctx.channel.id))
        if not group_config:
            await ctx.channel.send("Das ist kein Lerngruppenkanal.")
            return
        owner_id = group_config.get("owner_id")

        if not owner_id:
            return

        owner = await self.bot.fetch_user(owner_id)
        users = group_config.get("users", {})
        if not users and not owner:
            await ctx.channel.send("Keine Lerngruppenmitglieder vorhanden.")
            return

        names = []

        for user_id in users:
            user = await self.bot.fetch_user(user_id)
            names.append("@" + user.name + "#" + user.discriminator)

        await ctx.channel.send(f"Besitzerin: **@{owner.name}#{owner.discriminator}**\nMitglieder: " +
                               (f"{', '.join(names)}" if len(names) > 0 else "Keine"))

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg id",
        brief="Zeigt die ID für deine Lerngruppe an.",
    )
    @cmd_lg.command(name="id")
    async def cmd_id(self, ctx):
        if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx):
            group_config = self.groups["groups"].get(str(ctx.channel.id))
            if not group_config:
                await ctx.channel.send("Das ist kein Lerngruppenkanal.")
                return
        await ctx.channel.send(f"Die ID dieser Lerngruppe lautet: `{str(ctx.channel.id)}`.\n"
                               f"Beitrittsanfrage mit: `!lg join {str(ctx.channel.id)}`")

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg join <lg-id>",
        brief="Fragt bei der Besitzerin einer Lerngruppe um Aufnahme.",
        parameters={
            "id": "Die ID zur Lerngruppe."
        }
    )
    @cmd_lg.command(name="join")
    async def cmd_join(self, ctx, arg_id_or_channel: Union[int, disnake.TextChannel] = None):

        if arg_id_or_channel is None:
            arg_id_or_channel = ctx.channel

        cid = arg_id_or_channel.id if type(arg_id_or_channel) is disnake.TextChannel else arg_id_or_channel

        group_config = self.groups["groups"].get(str(cid))
        if not group_config:
            await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.")
            return

        channel = await self.bot.fetch_channel(int(cid))

        await utils.confirm(
            channel=channel,
            title="Jemand möchte deiner Lerngruppe beitreten!",
            description=f"<@!{ctx.author.id}> möchte gerne der Lerngruppe **#{channel.name}** beitreten.",
            message=f"Anfrage von <@!{ctx.author.id}>",
            custom_prefix="learninggroups:join"
        )
        await utils.send_dm(ctx.author, f"Deine Anfrage wurde an **#{channel.name}** gesendet. "
                                        "Sobald die Besitzerin der Lerngruppe darüber "
                                        "entschieden hat bekommst du Bescheid.")

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg kick <@usermention>",
        brief="Wirft @usermention aus der Gruppe."
    )
    @cmd_lg.command(name="kick")
    async def cmd_kick(self, ctx, arg_member: disnake.Member):
        if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx):
            group_config = self.groups["groups"].get(str(ctx.channel.id))
            if not group_config:
                await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.")
                return

            await self.remove_member_from_group(ctx.channel, arg_member)
            await self.update_permissions(ctx.channel)

    @help(
        command_group="lg",
        category="learninggroups",
        syntax="!lg leave",
        brief="Du verlässt die Lerngruppe."
    )
    @cmd_lg.command(name="leave")
    async def cmd_leave(self, ctx):
        group_config = self.groups["groups"].get(str(ctx.channel.id))
        if not group_config:
            await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.")
            return

        if group_config["owner_id"] == ctx.author.id:
            await ctx.channel.send("Du kannst nicht aus deiner eigenen Lerngruppe flüchten. Übertrage erst den Besitz.")
            return

        await self.remove_member_from_group(ctx.channel, ctx.author)
        await self.update_permissions(ctx.channel)

    async def on_group_request(self, confirmed, button, interaction: InteractionMessage):
        channel = interaction.channel
        member = interaction.author
        message = interaction.message

        if str(channel.id) == str(self.channel_request):
            request = self.groups["requested"].get(str(message.id))
            if confirmed and self.is_mod(member):
                await self.add_requested_group_channel(message, direct=False)

            elif not confirmed and (self.is_request_owner(request, member) or self.is_mod(member)):
                if self.is_mod(member):
                    user = await self.bot.fetch_user(request["owner_id"] )
                    if user:
                        await utils.send_dm(user, f"Deine Lerngruppenanfrage für #{self.full_channel_name(request)} wurde abgelehnt.")
                await self.remove_group_request(message)

                await message.delete()

    async def on_join_request(self, confirmed, button, interaction: InteractionMessage):
        channel = interaction.channel
        member = interaction.author
        message = interaction.message
        group_config = self.groups["groups"].get(str(channel.id))

        if not group_config:
            return

        if self.is_group_owner(channel, member) or self.is_mod(member):
            if confirmed:
                if message.mentions and len(message.mentions) == 1:
                    await self.add_member_to_group(channel, message.mentions[0])
                    await self.update_permissions(channel)

                else:
                    await channel.send(f"Leider ist ein Fehler aufgetreten.")
            else:
                if message.mentions and len(message.mentions) == 1:
                    await utils.send_dm(message.mentions[0], f"Deine Anfrage für die Lerngruppe **#{channel.name}**" 
                                                             "wurde abgelehnt.")
            await message.delete()

    async def cog_command_error(self, ctx, error):
        try:
            await handle_error(ctx, error)
        except:
            pass