import logging

from asgiref.sync import sync_to_async
from django.contrib.admin import display
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import UserManager as _UserManager, AbstractUser
from django.core.exceptions import ValidationError

from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber

from account.models import BonusProgramMixin
from store.utils import concat_not_null_values
from tg_bot.tasks import send_tg_message

logger = logging.getLogger(__name__)


class UserQuerySet(models.QuerySet):
    # TODO: optimize queries
    def with_base_related(self):
        return self


class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
    def _create_user(self, email, password, **extra_fields):
        if email:
            email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.password = make_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", False)
        return self._create_user(email, password, **extra_fields)

    def create_draft_user(self, **extra_fields):
        extra_fields.setdefault("is_staff", False)
        extra_fields.setdefault("is_draft_user", True)

        return self._create_user(email=None, password=None, **extra_fields)

    def create_superuser(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("role", User.ADMIN)

        if extra_fields.get("is_staff") is not True:
            raise ValueError("Superuser must have is_staff=True.")

        return self._create_user(email, password, **extra_fields)

    def invite_user(self, referral_code, user_id):
        inviter = User.objects.filter(referral_code=referral_code).first()
        user_to_invite = User.objects.filter(id=user_id).first()
        if inviter is None or user_to_invite is None:
            return

        if inviter.id == user_to_invite.id:
            logger.warning(f"User #{inviter.id} tried to invite himself via referral code {referral_code}")
            return

        obj, created = ReferralRelationship.objects.get_or_create(inviter_id=inviter.id, invited_id=user_to_invite.id)
        if not created:
            logger.warning(f"User #{inviter.id} already invited user #{user_id}")
            return

    async def bind_tg_user(self, tg_user_id, phone, referral_code=None) -> bool:
        # Normalize phone: 79111234567 -> +79111234567
        phone = PhoneNumber.from_string(phone).as_e164

        """
        1) No user with given phone or tg_user_id -> create draft user, add tg_user_id & phone
        2) User exists with given phone, but no tg_user_id -> add tg_user_id to User
        3) User exists with tg_user_id, but no phone -> add phone to User
        4) User exists with given tg_user_id & phone -> just authorize
        """

        user = await User.objects.filter(
            Q(phone=phone) | (Q(tg_user_id=tg_user_id))
        ).afirst()

        freshly_created = False

        # Sign up through Telegram bot
        if user is None:
            user = await sync_to_async(self.create_draft_user)(phone=phone, tg_user_id=tg_user_id)
            logger.info(f"tgbot: Created draft user #{user.id} for phone [{phone}]")
            freshly_created = True

        # First-time binding Telegram <-> User ?
        if freshly_created or user.tg_user_id is None:
            # Add bonus for Telegram login
            await sync_to_async(user.add_signup_bonus)()

        # Create referral relationship
        # Only for fresh registration
        if freshly_created and referral_code is not None:
            await sync_to_async(User.objects.invite_user)(referral_code, user.id)

        # Bind Telegram chat to user
        if not freshly_created:
            user.phone = phone
            user.tg_user_id = tg_user_id
            await user.asave()

        logger.info(f"tgbot: Telegram user #{tg_user_id} was bound to user #{user.id}")

        return freshly_created


class User(BonusProgramMixin, AbstractUser):
    ADMIN = "admin"
    ORDER_MANAGER = "ordermanager"
    PRODUCT_MANAGER = "productmanager"
    CLIENT = "client"

    ROLE_CHOICES = (
        (ADMIN, 'Администратор'),
        (ORDER_MANAGER, 'Менеджер по заказам'),
        (PRODUCT_MANAGER, 'Менеджер по закупкам'),
        (CLIENT, 'Клиент'),
    )

    # Login by email
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['phone']
    username = None

    email = models.EmailField("Эл. почта", blank=True, null=True, unique=True)

    # Base info
    first_name = models.CharField(_("first name"), max_length=150, blank=True, null=True)
    last_name = models.CharField(_("last name"), max_length=150, blank=True, null=True)
    middle_name = models.CharField("Отчество", max_length=150, blank=True, null=True)
    role = models.CharField("Роль", max_length=30, choices=ROLE_CHOICES, default=CLIENT, editable=False)

    # Contacts
    phone = PhoneNumberField('Телефон', null=True, blank=True, unique=True)
    telegram = models.CharField('Telegram', max_length=100, null=True, blank=True)

    # Bot-related

    # User is created via Telegram bot and has no password yet.
    # User can set initial password via /users/set_initial_password/
    is_draft_user = models.BooleanField("Черновик пользователя", default=False)

    tg_user_id = models.BigIntegerField("id пользователя в Telegram", null=True, blank=True, unique=True)

    objects = UserManager()

    def __str__(self):
        value = self.email or self.phone or self.id
        return str(value)

    @property
    def is_superuser(self):
        return self.role == self.ADMIN

    @property
    def is_manager(self):
        return self.role in (self.ADMIN, self.ORDER_MANAGER, self.PRODUCT_MANAGER)

    @display(description='ФИО')
    def full_name(self):
        return concat_not_null_values(self.last_name, self.first_name, self.middle_name)

    @property
    def invited_users(self):
        return User.objects.filter(user_inviter__inviter=self.id)

    @property
    def inviter(self):
        return User.objects.filter(user_invited__invited=self.id).first()

    def notify_tg_bot(self, message, **kwargs):
        if self.tg_user_id is None:
            return

        send_tg_message.delay(self.tg_user_id, message, **kwargs)

    def save(self, *args, **kwargs):
        # If password changed, it is no longer a draft User
        if self._password is not None:
            self.is_draft_user = False

        super().save(*args, **kwargs)


class ReferralRelationship(models.Model):
    invited_at = models.DateTimeField(auto_now_add=True)
    inviter = models.ForeignKey('User', on_delete=models.CASCADE, related_name="user_invited")
    invited = models.ForeignKey('User', on_delete=models.CASCADE, related_name="user_inviter")

    class Meta:
        unique_together = (('inviter', 'invited'),)

    def clean(self):
        if self.inviter_id == self.invited_id:
            raise ValidationError("User can't invite himself")
