import json
import random
import re
import string
from datetime import date, datetime, timedelta, MINYEAR
from typing import Union

from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser, Permission
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.timezone import is_aware, make_aware
from django.utils.translation import ugettext_lazy as _

from users.licenses import MYSKILLBOX_LICENSES
from users.managers import LicenseManager, RoleManager, UserManager, UserRoleManager

DEFAULT_SCHOOL_ID = 1
NO_DATE = date(MINYEAR, 1, 1)  # date to tag licenses without date
NO_DATETIME = datetime.combine(NO_DATE, datetime.min.time())
AWARE_NO_DATETIME = make_aware(NO_DATETIME)


class User(AbstractUser):
    LICENSE_NONE = 'no-license'
    LICENSE_VALID = 'valid-license'
    LICENSE_EXPIRED = 'expired-license'

    last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True)
    recent_modules = models.ManyToManyField('books.Module', related_name='+', through='books.RecentModule')
    last_topic = models.ForeignKey('books.Topic', related_name='+', on_delete=models.SET_NULL, null=True)
    avatar_url = models.CharField(max_length=254, blank=True, default='')
    email = models.EmailField(_('email address'), unique=True)
    hep_id = models.PositiveIntegerField(null=True, blank=False)
    hep_group_id = models.PositiveIntegerField(null=True, blank=False)
    license_expiry_date = models.DateField(blank=False, null=True, default=None)
    onboarding_visited = models.BooleanField(default=False)
    team = models.ForeignKey('users.Team', on_delete=models.SET_NULL, blank=True, null=True, related_name='members')

    # for wagtail autocomplete
    autocomplete_search_field = 'username'

    def autocomplete_label(self):
        return self.username

    objects = UserManager()

    def get_role_permissions(self):
        perms = set()
        for role in Role.objects.get_roles_for_user(self):
            perms = perms.union(
                ('{}.{}'.format(r.content_type.app_label, r.codename) for r in role.role_permission.all())
            )
        return perms

    def get_all_permissions(self, obj=None):
        """
        works as long as we have only a single school
        :param obj:
        :return: django permissions and school permissions for single default school
        """
        django_permissions = super().get_all_permissions(obj)
        return django_permissions.union(self.get_role_permissions())

    def has_perm(self, perm, obj=None):
        return super(User, self).has_perm(perm, obj) or perm in self.get_all_permissions(obj)

    def users_in_same_school_class(self):
        return User.objects.filter(school_classes__users=self.pk)

    def get_teacher(self):
        if self.is_teacher():
            return self
        elif self.school_classes.count() > 0:
            return self.school_classes.first().get_teacher()
        else:
            return None

    def is_teacher(self):
        return self.user_roles.filter(role__key='teacher').exists()

    @cached_property
    def selected_class(self) -> Union['SchoolClass', None]:
        return self._get_selected_class()

    def _get_selected_class(self) -> Union['SchoolClass', None]:
        try:
            settings = UserSetting.objects.get(user=self)
            return settings.selected_class
        except ObjectDoesNotExist:
            if self.school_classes.count() > 0:
                default_selected_class = self.school_classes.first()
                UserSetting.objects.create(selected_class=default_selected_class, user=self)
                return default_selected_class
            else:
                return None

    def sync_with_hep_data(self, hep_data):

        data_has_changed = False

        if self.email != hep_data['email']:
            self.email = hep_data['email']
            self.username = hep_data['email']
            data_has_changed = True

        if self.first_name != hep_data['first_name']:
            self.first_name = hep_data['first_name']
            data_has_changed = True

        if self.last_name != hep_data['last_name']:
            self.last_name = hep_data['last_name']
            data_has_changed = True

        if data_has_changed:
            self.save()

    def set_selected_class(self, school_class):
        user_settings, created = UserSetting.objects.get_or_create(user=self)
        user_settings.selected_class = school_class
        user_settings.save()

    @property
    def can_manage_school_class_content(self):
        return self.has_perm('users.can_manage_school_class_content')

    @property
    def full_name(self):
        return self.get_full_name()

    @property
    def read_only(self):
        return self.get_license_status() == User.LICENSE_EXPIRED

    def get_license_status(self):
        if self.license_expiry_date is None:
            return User.LICENSE_NONE
        if self.license_expiry_date >= date.today():
            return User.LICENSE_VALID
        return User.LICENSE_EXPIRED

    class Meta:
        ordering = ['pk', ]


class GroupWithCode(models.Model):
    class Meta:
        abstract = True

    name = models.CharField(max_length=100, blank=False, null=False, unique=True)
    is_deleted = models.BooleanField(blank=False, null=False, default=False)
    code = models.CharField('Code zum Beitreten', blank=True, null=True, max_length=10, unique=True, default=None)

    def generate_code(self):
        letters = string.ascii_lowercase
        digits = string.digits
        code = ''.join(random.choice(letters) for i in range(4)) + ''.join(random.choice(digits) for i in range(2))
        try:
            self.__class__.objects.get(code=code)
            self.generate_code()
        except self.__class__.DoesNotExist:
            self.code = code.upper()
            self.save()


class Team(GroupWithCode):
    creator = models.ForeignKey(get_user_model(), null=True, on_delete=models.SET_NULL, blank=True, related_name='+')

    class Meta:
        verbose_name = 'Team'
        verbose_name_plural = 'Teams'

    def __str__(self):
        return self.name


class SchoolClass(GroupWithCode):
    users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True,
                                   through='users.SchoolClassMember')

    class Meta:
        verbose_name = 'Schulklasse'
        verbose_name_plural = 'Schulklassen'

    def __str__(self):
        return '{}'.format(self.name)

    @classmethod
    def generate_default_group_name(cls, user=None):
        prefix = 'Meine Klasse'
        if user is not None:
            return f'{prefix} {user.pk}'
        prefix_regex = r'Meine Klasse (\d+)'
        initial_default_group = '{} 1'.format(prefix)
        my_group_filter = cls.objects.filter(name__startswith=prefix).order_by('-pk')

        if len(my_group_filter) == 0:
            return initial_default_group

        match = re.search(prefix_regex, my_group_filter[0].name)

        if not match:
            return initial_default_group

        index = int(match.group(1))

        return '{} {}'.format(prefix, index + 1)

    @classmethod
    def create_default_group_for_teacher(cls, user):
        default_class_name = cls.generate_default_group_name(user)
        default_class = cls.objects.create(name=default_class_name)
        SchoolClassMember.objects.create(
            user=user,
            school_class=default_class
        )

    def is_user_in_schoolclass(self, user):
        return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0

    def get_teacher(self):
        return self.users.filter(user_roles__role__key='teacher').first()

    def save(self, *args, **kwargs):
        if self.code == '':  # '' can't be unique, so we null it
            self.code = None
        super().save(*args, **kwargs)


class Role(models.Model):
    key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True)
    name = models.CharField(_('Name'), max_length=100, blank=False, null=False)
    role_permission = models.ManyToManyField(Permission, verbose_name=_('Role permission'),
                                             blank=True, related_name="role_set", related_query_name="role")

    objects = RoleManager()

    def __str__(self):
        return self.name

    def permissions_as_code(self):
        return self.role_permission.values_list('codename', flat=True)

    def has_permission(self, permission_name):
        try:
            self.role_permission.get(codename=permission_name)
            return True
        except Permission.DoesNotExist:
            return False

    @staticmethod
    def create_key(name):
        return name.lower()

    class Meta:
        permissions = (
            # ("can_edit_events", "Can edit events"),
            # ("can_edit_own_comments", "Can edit own comments"),
            # ("can_delete_comments", "Can delete comments"),
            ('can_manage_school_class_content', 'Can manage contents for assigned school clases'),
            # ("can_admin_school", "Can admin school"),
        )


class UserRole(models.Model):
    user = models.ForeignKey(User, blank=False, null=False, on_delete=models.CASCADE, related_name='user_roles')
    role = models.ForeignKey(Role, blank=False, null=False, on_delete=models.CASCADE, related_name='user_roles')

    objects = UserRoleManager()

    @property
    def groups(self):
        return SchoolClass.objects.filter(users=self.user.id)

    @property
    def group_ids(self):
        return map(lambda g: g.id, self.groups)

    @classmethod
    def get_roles_for_user(cls, user):
        roles = UserRole.objects.filter(user=user)
        return roles

    @classmethod
    def get_role_for_user(cls, user):
        return UserRole.get_roles_for_user(user).first()

    def __str__(self):
        return '%s: %s' % (self.role, self.user)


class UserSetting(models.Model):
    user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_setting')
    selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE)


class UserData(models.Model):
    user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_data')
    accepted_terms = models.BooleanField(default=False)


class License(models.Model):
    for_role = models.ForeignKey(Role, blank=False, null=True, on_delete=models.CASCADE)
    licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE)
    expire_date = models.DateField(blank=False, null=True, )
    order_id = models.IntegerField(blank=False, null=False, default=-1)
    raw = models.TextField(default='')
    isbn = models.CharField(max_length=50, blank=False, null=False,
                            default=list(MYSKILLBOX_LICENSES.keys())[0])  # student license
    created_at = models.DateTimeField(auto_now_add=True)
    hep_created_at = models.DateTimeField(default=AWARE_NO_DATETIME)
    new_api_raw = models.TextField(default='')

    objects = LicenseManager()

    def is_teacher_license(self):
        return self.for_role.key == RoleManager.TEACHER_KEY

    def is_valid(self):
        date = make_aware(datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day))
        return License.is_product_active(date, self.isbn)

    def _read_as_json(self, raw_string: str) -> dict:
        return json.loads(raw_string.replace("'", '"').replace('True', 'true').replace('False', 'false')
                          .replace('None', 'null'))

    def get_hep_start_date(self) -> date:
        hep_data = self._read_as_json(self.raw)
        if 'updated_at' in hep_data:  # old key from Magento. Format: 2020-06-22 18:45:13
            date_string = hep_data['created_at']
            return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S').date()

        return NO_DATE

    def get_hep_isbn(self) -> str:
        hep_data = self._read_as_json(self.raw)
        if self._is_old_api(hep_data):
            return hep_data['sku']
        elif self._is_new_api(hep_data):
            return hep_data['isbn']
        else:
            return ''

    def is_new_api(self) -> bool:
        hep_data = self._read_as_json(self.raw)
        return self._is_new_api(hep_data)

    def is_old_api(self) -> bool:
        hep_data = self._read_as_json(self.raw)
        return self._is_old_api(hep_data)

    def _is_new_api(self, hep_data: dict) -> bool:
        return 'isbn' in hep_data

    def _is_old_api(self, hep_data: dict) -> bool:
        return 'sku' in hep_data

    @staticmethod
    def is_product_active(expiry_date, isbn):
        now = timezone.now()

        if not is_aware(expiry_date):
            expiry_date = make_aware(expiry_date)

        return expiry_date >= now >= expiry_date - timedelta(days=MYSKILLBOX_LICENSES[isbn]['duration'])

    def __str__(self):
        return f'License for role: {self.for_role}'


class SchoolClassMember(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    school_class = models.ForeignKey(SchoolClass, on_delete=models.CASCADE)
    active = models.BooleanField(default=True)
