import datetime
import functools
import operator

from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from reversion import revisions as reversion

from . import hooks, signals


class SignupCode(models.Model):
    class AlreadyExists(Exception):
        pass

    class InvalidCode(Exception):
        pass

    code = models.CharField(max_length=64, unique=True)
    expiry = models.DateTimeField(null=True, blank=True)
    inviter = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE)
    email = models.EmailField(max_length=254, blank=True)
    notes = models.TextField(blank=True)
    sent = models.DateTimeField(null=True, blank=True)
    created = models.DateTimeField(default=timezone.now, editable=False)
    use_count = models.PositiveIntegerField(editable=False, default=0)

    class Meta:
        verbose_name = _("signup code")
        verbose_name_plural = _("signup codes")

    def __str__(self):
        if self.email:
            return "{0} [{1}]".format(self.email, self.code)
        else:
            return self.code

    @classmethod
    def exists(cls, code=None, email=None):
        checks = []
        if code:
            checks.append(models.Q(code=code))
        if email:
            checks.append(models.Q(email=code))
        if not checks:
            return False
        return cls._default_manager.filter(functools.reduce(operator.or_, checks)).exists()

    @classmethod
    def create(cls, **kwargs):
        email, code = kwargs.get("email"), kwargs.get("code")
        if kwargs.get("check_exists", True) and cls.exists(code=code, email=email):
            raise cls.AlreadyExists()
        expiry = timezone.now() + datetime.timedelta(hours=kwargs.get("expiry", 24))
        if not code:
            code = hooks.generate_signup_code_token(email)
        params = {
            "code": code,
            "expiry": expiry,
            "inviter": kwargs.get("inviter"),
            "notes": kwargs.get("notes", ""),
        }
        if email:
            params["email"] = email
        return cls(**params)

    @classmethod
    def check_code(cls, code):
        try:
            signup_code = cls._default_manager.get(code=code)
        except cls.DoesNotExist:
            raise cls.InvalidCode()
        else:
            if signup_code.use_count > 0:
                raise cls.InvalidCode()
            else:
                if signup_code.expiry and timezone.now() > signup_code.expiry:
                    raise cls.InvalidCode()
                else:
                    return signup_code

    def calculate_use_count(self):
        self.use_count = self.signupcoderesult_set.count()
        self.save()

    def use(self, user):
        """
        Add a SignupCode result attached to the given user.
        """
        result = SignupCodeResult()
        result.signup_code = self
        result.user = user
        result.save()
        signals.signup_code_used.send(sender=result.__class__, signup_code_result=result)

    def send(self, **kwargs):
        protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http")
        current_site = kwargs["site"] if "site" in kwargs else Site.objects.get_current()
        team_signup_url = reverse('create_user_from_code', kwargs={'code': self.code})
        signup_url = f'{protocol}://{current_site}{team_signup_url}'
        hooks.send_invitation_email(self.email, signup_url)
        self.sent = timezone.now()
        self.save()
        signals.signup_code_sent.send(sender=SignupCode, signup_code=self)


class SignupCodeResult(models.Model):
    signup_code = models.ForeignKey(SignupCode, on_delete=models.CASCADE)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    timestamp = models.DateTimeField(default=timezone.now)

    def save(self, **kwargs):
        super(SignupCodeResult, self).save(**kwargs)
        self.signup_code.calculate_use_count()


class JoinInvitation(models.Model):
    class Status(models.IntegerChoices):
        SENT = 1
        ACCEPTED = 2
        JOINED_INDEPENDENTLY = 3

    from_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="invites_sent")
    to_user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, related_name="invites_received"
    )
    message = models.TextField(null=True)
    sent = models.DateTimeField(default=timezone.now)
    status = models.IntegerField(choices=Status.choices)
    signup_code = models.OneToOneField(SignupCode, on_delete=models.CASCADE)

    def to_user_email(self):
        return self.signup_code.email

    def accept(self, user):
        self.to_user = user
        self.status = JoinInvitation.Status.ACCEPTED
        self.save()
        self.from_user.invitation_statistics.increment_accepted()
        signals.invite_accepted.send(sender=JoinInvitation, invitation=self)

    @classmethod
    def process_independent_joins(cls, user, email):
        invites = cls.objects.filter(to_user__isnull=True, signup_code__email=email)
        for invite in invites:
            invite.to_user = user
            invite.status = cls.Status.JOINED_INDEPENDENTLY
            invite.save()
            signals.joined_independently.send(sender=cls, invitation=invite)

    @classmethod
    def invite(cls, from_user, to_email, message=None, send=True):
        signup_code = SignupCode.create(
            email=to_email,
            inviter=from_user,
            check_exists=False,  # before we are called caller must check for existence
        )
        signup_code.save()
        join = cls.objects.create(
            from_user=from_user, message=message, status=JoinInvitation.Status.SENT, signup_code=signup_code
        )

        def send_invite(*args, **kwargs):
            signup_code.send(*args, **kwargs)
            stat, _ = InvitationStatistics.objects.get_or_create(user=from_user)
            stat.increment_sent()
            signals.invite_sent.send(sender=cls, invitation=join)

        if send:
            send_invite()
        else:
            join.send_invite = send_invite
        return join


class InvitationStatistics(models.Model):
    """User invitation statistics"""

    user = models.OneToOneField(
        settings.AUTH_USER_MODEL, related_name="invitation_statistics", on_delete=models.CASCADE
    )
    invites_sent = models.IntegerField(default=0)
    invites_accepted = models.IntegerField(default=0)

    def increment_accepted(self):
        self.invites_accepted += 1
        self.save()

    def increment_sent(self):
        self.invites_sent += 1
        self.save()


class Team(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    creator = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="teams_created", on_delete=models.CASCADE)
    created = models.DateTimeField(default=timezone.now, editable=False)

    class Type(models.IntegerChoices):
        COMPANY = 0
        BUILDING = 1
        INSPECTION_BODY = 2
        ASSOCIATE = 3
        SUPER_OWNER = 4  # System owners and administrators

    team_type = models.IntegerField(choices=Type.choices)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse("team_detail", args=[self.pk])

    def can_join(self, user):
        state = self.state_for(user)
        if state is None:
            return True
        elif state == Membership.State.INVITED:
            return True
        else:
            return False

    def can_leave(self, user):
        # Admins can't leave at the moment
        role = self.role_for(user)
        return role == Membership.Role.ADMIN

    def can_apply(self, user):
        state = self.state_for(user)
        return state is None

    @property
    def applicants(self):
        return self.memberships.filter(state=Membership.State.APPLIED)

    @property
    def invitees(self):
        return self.memberships.filter(state=Membership.State.INVITED)

    @property
    def declines(self):
        return self.memberships.filter(state=Membership.State.DECLINED)

    @property
    def rejections(self):
        return self.memberships.filter(state=Membership.State.REJECTED)

    @property
    def waitlisted(self):
        return self.memberships.filter(state=Membership.State.WAITLISTED)

    @property
    def acceptances(self):
        return self.memberships.filter(state__in=[Membership.State.ACCEPTED, Membership.State.AUTO_JOINED])

    @property
    def viewers(self):
        return self.acceptances.filter(role=Membership.Role.VIEWER)

    @property
    def admins(self):
        return self.acceptances.filter(role=Membership.Role.ADMIN)

    @property
    def technical_signatories(self):
        return self.acceptances.filter(role=Membership.Role.TECHNICAL_SIGNATORY)

    @property
    def inspectors(self):
        return self.acceptances.filter(role=Membership.Role.INSPECTOR)

    @property
    def associates(self):
        return self.acceptances.filter(role=Membership.Role.ASSOCIATE)

    @property
    def promoters(self):
        return self.acceptances.filter(role=Membership.Role.PROMOTER)

    def is_viewer(self, user):
        return self.acceptances.filter(role__in=[Membership.Role.VIEWER], user=user).exists()

    def is_admin(self, user):
        return self.acceptances.filter(role__in=[Membership.Role.ADMIN], user=user).exists()

    def is_technical_signatory(self, user):
        return self.acceptances.filter(role__in=[Membership.Role.TECHNICAL_SIGNATORY], user=user).exists()

    def is_inspector(self, user):
        return self.acceptances.filter(role__in=[Membership.Role.INSPECTOR], user=user).exists()

    def is_associate(self, user):
        return self.acceptances.filter(role__in=[Membership.Role.ASSOCIATE], user=user).exists()

    def is_promoter(self, user):
        return self.acceptances.filter(role__in=[Membership.Role.PROMOTER], user=user).exists()

    def is_on_team(self, user):
        return self.acceptances.filter(user=user).exists()

    def add_member(self, user, role=None, state=None, by=None):
        # we do this, rather than put the Membership constants in declaration
        # because Membership is not yet defined
        if role is None:
            role = Membership.Role.MEMBER
        if state is None:
            state = Membership.State.AUTO_JOINED

        membership, created = self.memberships.get_or_create(
            team=self,
            user=user,
            defaults={"role": role, "state": state},
        )
        signals.added_member.send(sender=self, membership=membership, by=by)
        return membership

    def add_user(self, user, role, by=None):
        state = Membership.State.INVITED
        membership, _ = self.memberships.get_or_create(user=user, defaults={"role": role, "state": state})
        signals.added_member.send(sender=self, membership=membership, by=by)
        return membership

    def invite_user(self, from_user, to_email, role, message=None):
        if not JoinInvitation.objects.filter(signup_code__email=to_email).exists():
            invite = JoinInvitation.invite(from_user, to_email, message, send=False)
            membership, _ = self.memberships.get_or_create(
                invite=invite, defaults={"role": role, "state": Membership.State.INVITED}
            )
            invite.send_invite()
            signals.invited_user.send(sender=self, membership=membership, by=from_user)
            return membership

    def for_user(self, user):
        try:
            return self.memberships.get(user=user)
        except ObjectDoesNotExist:
            pass

    def state_for(self, user):
        membership = self.for_user(user=user)
        if membership:
            return membership.state

    def role_for(self, user):
        membership = self.for_user(user)
        if membership:
            return membership.role


class Membership(models.Model):
    team = models.ForeignKey(Team, related_name="memberships", on_delete=models.CASCADE)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name="memberships", null=True, blank=True, on_delete=models.SET_NULL
    )
    invite = models.ForeignKey(
        JoinInvitation, related_name="memberships", null=True, blank=True, on_delete=models.SET_NULL
    )

    class State(models.IntegerChoices):
        APPLIED = 0
        INVITED = 1
        DECLINED = 2
        REJECTED = 3
        ACCEPTED = 4
        WAITLISTED = 5
        AUTO_JOINED = 6

    class Role(models.IntegerChoices):
        VIEWER = 0
        ADMIN = 1
        # TEAM_ADMIN = ?

        # Inspection bodies and associate companies
        TECHNICAL_SIGNATORY = 2
        INSPECTOR = 3
        ASSOCIATE = 4  # Admin by invitation to company/building

        # Super or system owner
        # Approves Associate team promotion requests and company/building verifications
        PROMOTER = 5

    state = models.IntegerField(choices=State.choices)
    role = models.IntegerField(choices=Role.choices, default=Role.VIEWER)
    created = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return f"{self.user} in {self.team}"

    class Meta:
        unique_together = [("team", "user", "invite")]

    def is_viewer(self):
        return self.role == Membership.Role.VIEWER

    def is_admin(self):
        return self.role == Membership.Role.ADMIN

    def is_technical_signatory(self):
        return self.role == Membership.Role.TECHNICAL_SIGNATORY

    def is_inspector(self):
        return self.role == Membership.Role.INSPECTOR

    def is_associate(self):
        return self.role == Membership.Role.ASSOCIATE

    def is_promoter(self):
        return self.role == Membership.Role.PROMOTER

    def promote(self, by):
        role = self.team.role_for(by)
        if role in [Membership.Role.ADMIN]:
            if self.role == Membership.Role.VIEWER:
                self.role = Membership.Role.ADMIN
                self.save()
                signals.promoted_member.send(sender=self, membership=self, by=by)
                return True
        return False

    def demote(self, by):
        role = self.team.role_for(by)
        if role in [Membership.Role.ADMIN]:
            if self.role == Membership.Role.ADMIN:
                self.role = Membership.Role.VIEWER
                self.save()
                signals.demoted_member.send(sender=self, membership=self, by=by)
                return True
        return False

    def accept(self, by):
        role = self.team.role_for(by)
        if self.state in [Membership.State.APPLIED, Membership.State.INVITED]:
            self.state = Membership.State.ACCEPTED
            self.user = by
            self.save()
            return True
        return False

    def reject(self, by):
        role = self.team.role_for(by)
        if self.state in [Membership.State.APPLIED, Membership.State.INVITED]:
            self.state = Membership.State.REJECTED
            self.save()
            signals.rejected_membership.send(sender=self, membership=self)
            return True
        return False

    def joined(self):
        self.user = self.invite.to_user
        self.state = Membership.State.AUTO_JOINED
        self.save()

    def status(self):
        if self.user:
            return self.get_state_display()
        if self.invite:
            return self.invite.get_status_display()
        return "Unknown"

    def resend_invite(self, by=None):
        if self.invite is not None:
            code = self.invite.signup_code
            code.expiry = timezone.now() + datetime.timedelta(days=5)
            code.save()
            code.send()
            signals.resent_invite.send(sender=self, membership=self, by=by)

    def remove(self, by=None):
        if self.invite is not None:
            self.invite.signup_code.delete()
            self.invite.delete()
        self.delete()
        signals.removed_membership.send(sender=self, team=self.team, user=self.user, invitee=self.invitee, by=by)

    @property
    def invitee(self):
        return self.user or self.invite.to_user_email()


reversion.register(Membership)
