import logging
from typing import ClassVar
from django.db import models, transaction
from django.contrib.auth.models import Group, User
from django.core.exceptions import ObjectDoesNotExist
from django.db import models, transaction

from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveAllianceInfo, EveCorporationInfo

logger = logging.getLogger(__name__)


def get_users_for_state(state: State):
    return User.objects.select_related('profile').prefetch_related('profile__main_character')\
            .filter(profile__state_id=state.pk)


class AutogroupsConfigManager(models.Manager):
    def update_groups_for_state(self, state: State):
        """
        Update all the Group memberships for the users
        who have State
        :param state: State to update for
        :return:
        """
        users = get_users_for_state(state)
        for config in self.filter(states=state):
            logger.debug("in state loop")
            for user in users:
                logger.debug(f"in user loop for {user}")
                config.update_group_membership_for_user(user)

    def update_groups_for_user(self, user: User, state: State = None):
        """
        Update the Group memberships for the given users state
        :param user: User to update for
        :param state: State to update user for
        :return:
        """
        if state is None:
            state = user.profile.state
        for config in self.exclude(states=state):
            # ensure user does not have groups from previous state
            config.remove_user_from_alliance_groups(user)
            config.remove_user_from_corp_groups(user)
        for config in self.filter(states=state):
            # grant user new groups for their state
            config.update_group_membership_for_user(user)


class AutogroupsConfig(models.Model):
    OPT_TICKER = 'ticker'
    OPT_NAME = 'name'
    NAME_OPTIONS = (
        (OPT_TICKER, 'Ticker'),
        (OPT_NAME, 'Full name'),
    )

    states = models.ManyToManyField(State, related_name='autogroups')

    corp_groups = models.BooleanField(default=False, help_text="Setting this to false will delete all the created groups.")
    corp_group_prefix = models.CharField(max_length=50, default='Corp ', blank=True)
    corp_name_source = models.CharField(max_length=20, choices=NAME_OPTIONS, default=OPT_NAME)

    alliance_groups = models.BooleanField(default=False, help_text="Setting this to false will delete all the created groups.")
    alliance_group_prefix = models.CharField(max_length=50, default='Alliance ', blank=True)
    alliance_name_source = models.CharField(max_length=20, choices=NAME_OPTIONS, default=OPT_NAME)

    corp_managed_groups = models.ManyToManyField(
        Group, through='ManagedCorpGroup', related_name='corp_managed_config',
        help_text='A list of corporation groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you\'re doing.')

    alliance_managed_groups = models.ManyToManyField(
        Group, through='ManagedAllianceGroup', related_name='alliance_managed_config',
        help_text='A list of alliance groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you\'re doing.')

    replace_spaces = models.BooleanField(default=False)
    replace_spaces_with = models.CharField(
        max_length=10, default='', blank=True,
        help_text='Any spaces in the group name will be replaced with this.')

    objects: ClassVar[AutogroupsConfigManager] = AutogroupsConfigManager()

    def __str__(self) -> str:
        return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None))

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

    def __repr__(self) -> str:
        return self.__class__.__name__

    def update_all_states_group_membership(self) -> None:
        list(map(self.update_group_membership_for_state, self.states.all()))

    def update_group_membership_for_state(self, state: State):
        list(map(self.update_group_membership_for_user, get_users_for_state(state)))

    @transaction.atomic
    def update_group_membership_for_user(self, user: User) -> None:
        self.update_alliance_group_membership(user)
        self.update_corp_group_membership(user)

    def user_entitled_to_groups(self, user: User) -> bool:
        try:
            return user.profile.state in self.states.all()
        except ObjectDoesNotExist:
            return False

    @transaction.atomic
    def update_alliance_group_membership(self, user: User):
        group = None
        try:
            if not self.alliance_groups or not self.user_entitled_to_groups(user):
                logger.debug(f'User {user} does not have required state for alliance group membership')
                return
            else:
                alliance = user.profile.main_character.alliance
                if alliance is None:
                    logger.debug(f'User {user} alliance is None, cannot update group membership')
                    return
                group = self.get_alliance_group(alliance)
        except EveAllianceInfo.DoesNotExist:
            logger.debug(f'User {user} main characters alliance does not exist in the database. Creating.')
            alliance = EveAllianceInfo.objects.create_alliance(user.profile.main_character.alliance_id)
            group = self.get_alliance_group(alliance)
        except AttributeError:
            logger.warning(f'User {user} does not have a main character. Group membership not updated')
        finally:
            self.remove_user_from_alliance_groups(user, except_group=group)
            if group is not None:
                logger.debug(f'Adding user {user} to alliance group {group}')
                user.groups.add(group)

    @transaction.atomic
    def update_corp_group_membership(self, user: User):
        group = None
        try:
            if not self.corp_groups or not self.user_entitled_to_groups(user):
                logger.debug(f'User {user} does not have required state for corp group membership')
            else:
                corp = user.profile.main_character.corporation
                group = self.get_corp_group(corp)
        except EveCorporationInfo.DoesNotExist:
            logger.debug(f'User {user} main characters corporation does not exist in the database. Creating.')
            corp = EveCorporationInfo.objects.create_corporation(user.profile.main_character.corporation_id)
            group = self.get_corp_group(corp)
        except AttributeError:
            logger.warning(f'User {user} does not have a main character. Group membership not updated')
        finally:
            self.remove_user_from_corp_groups(user, except_group=group)
            if group is not None:
                logger.debug(f'Adding user {user} to corp group {group}')
                user.groups.add(group)

    @transaction.atomic
    def remove_user_from_alliance_groups(self, user: User, except_group: Group = None):
        remove_groups = user.groups.filter(pk__in=self.alliance_managed_groups.all().values_list('pk', flat=True))
        if except_group is not None:
            remove_groups = remove_groups.exclude(pk=except_group.pk)
        list(map(user.groups.remove, remove_groups))

    @transaction.atomic
    def remove_user_from_corp_groups(self, user: User, except_group: Group = None):
        remove_groups = user.groups.filter(pk__in=self.corp_managed_groups.all().values_list('pk', flat=True))
        if except_group is not None:
            remove_groups = remove_groups.exclude(pk=except_group.pk)
        list(map(user.groups.remove, remove_groups))

    def get_alliance_group(self, alliance: EveAllianceInfo) -> Group:
        return self.create_alliance_group(alliance)

    def get_corp_group(self, corp: EveCorporationInfo) -> Group:
        return self.create_corp_group(corp)

    @transaction.atomic
    def create_alliance_group(self, alliance: EveAllianceInfo) -> Group:
        group, created = Group.objects.get_or_create(name=self.get_alliance_group_name(alliance))
        ManagedAllianceGroup.objects.get_or_create(group=group, config=self, alliance=alliance)
        return group

    @transaction.atomic
    def create_corp_group(self, corp: EveCorporationInfo) -> Group:
        group, created = Group.objects.get_or_create(name=self.get_corp_group_name(corp))
        ManagedCorpGroup.objects.get_or_create(group=group, config=self, corp=corp)
        return group

    def delete_alliance_managed_groups(self):
        """
        Deletes ALL managed alliance groups
        """
        for g in self.alliance_managed_groups.all():
            g.delete()

    def delete_corp_managed_groups(self):
        """
        Deletes ALL managed corp groups
        """
        for g in self.corp_managed_groups.all():
            g.delete()

    def get_alliance_group_name(self, alliance: EveAllianceInfo) -> str:
        if self.alliance_name_source == self.OPT_TICKER:
            name = alliance.alliance_ticker
        elif self.alliance_name_source == self.OPT_NAME:
            name = alliance.alliance_name
        else:
            raise NameSourceException('Not a valid name source')
        return self._replace_spaces(self.alliance_group_prefix + name)

    def get_corp_group_name(self, corp: EveCorporationInfo) -> str:
        if self.corp_name_source == self.OPT_TICKER:
            name = corp.corporation_ticker
        elif self.corp_name_source == self.OPT_NAME:
            name = corp.corporation_name
        else:
            raise NameSourceException('Not a valid name source')
        return self._replace_spaces(self.corp_group_prefix + name)

    def _replace_spaces(self, name: str) -> str:
        """
        Replace the spaces in the given name based on the config
        :param name: name to replace spaces in
        :return: name with spaces replaced with the configured character(s) or unchanged if configured
        """
        if self.replace_spaces:
            return name.strip().replace(' ', str(self.replace_spaces_with))
        return name


class ManagedGroup(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    config = models.ForeignKey(AutogroupsConfig, on_delete=models.CASCADE)

    class Meta:
        abstract = True

    def __str__(self) -> str:
        return f"Managed Group: {self.group.name}"


class ManagedCorpGroup(ManagedGroup):
    corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE)


class ManagedAllianceGroup(ManagedGroup):
    alliance = models.ForeignKey(EveAllianceInfo, on_delete=models.CASCADE)


class NameSourceException(Exception):
    pass
