import uuid
from datetime import timedelta
from urllib.parse import urlencode

from django.conf import settings
from django.contrib.auth import password_validation
from django.core import signing
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError, transaction
from django.utils import timezone
from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from rest_framework.exceptions import ValidationError

from .models import AccountEmail, User
from .tokens import email_verification_token, password_reset_token


def validate_password(password, user):
    try:
        password_validation.validate_password(password, user)
    except DjangoValidationError as exc:
        raise ValidationError({"password": exc.messages}) from exc


def queue_email(*, user, purpose, token, recipient=None):
    uid = urlsafe_base64_encode(force_bytes(user.pk))
    link = f"{settings.ACCOUNT_FRONTEND_URL.rstrip('/')}/{purpose}?{urlencode({'uid': uid, 'token': token})}"
    AccountEmail.objects.create(
        recipient=recipient or user.email,
        subject=f"FTG: {purpose}",
        body=f"FTG account request: {purpose}\n\n{link}\n\nuid: {uid}\ntoken: {token}\n\n"
             "If you did not request this, ignore this email.\n",
        expires_at=timezone.now() + timedelta(seconds=settings.PASSWORD_RESET_TIMEOUT),
        next_attempt_at=timezone.now(),
    )


def _locked_user(uid):
    try:
        pk = force_str(urlsafe_base64_decode(uid))
        return User.objects.select_for_update().get(pk=pk, is_active=True)
    except (ValueError, TypeError, UnicodeError, DjangoValidationError, User.DoesNotExist) as exc:
        raise ValidationError({"token": ["Invalid or expired link."]}) from exc


def _check_password(user, password):
    if not user.check_password(password):
        raise ValidationError({"current_password": ["Incorrect password."]})


class AccountService:
    @staticmethod
    @transaction.atomic
    def register(**data):
        password = data.pop("password")
        user = User(**data)
        validate_password(password, user)
        user.set_password(password)
        try:
            with transaction.atomic():
                user.save()
        except IntegrityError as exc:
            raise ValidationError({"detail": "Email or username is already in use."}) from exc
        queue_email(user=user, purpose="verify-email", token=email_verification_token.make_token(user))
        return user

    @staticmethod
    @transaction.atomic
    def request_email(*, email, purpose):
        user = User.objects.filter(email__iexact=email, is_active=True).first()
        if user is None or not user.has_usable_password():
            return
        if purpose == "verify-email":
            if user.is_email_verified:
                return
            generator = email_verification_token
        else:
            generator = password_reset_token
        queue_email(user=user, purpose=purpose, token=generator.make_token(user))

    @staticmethod
    @transaction.atomic
    def verify_email(*, uid, token):
        user = _locked_user(uid)
        if user.is_email_verified or not email_verification_token.check_token(user, token):
            raise ValidationError({"token": ["Invalid or expired link."]})
        user.is_email_verified = True
        user.save(update_fields=["is_email_verified", "updated_at"])

    @staticmethod
    @transaction.atomic
    def reset_password(*, uid, token, password):
        user = _locked_user(uid)
        if not password_reset_token.check_token(user, token):
            raise ValidationError({"token": ["Invalid or expired link."]})
        validate_password(password, user)
        user.set_password(password)
        user.auth_version = uuid.uuid4()
        user.save(update_fields=["password", "auth_version", "updated_at"])
        from .security_sessions import revoke_all
        revoke_all(user)
        password_validation.password_changed(password, user)

    @staticmethod
    @transaction.atomic
    def change_password(*, user, current_password, password, otp=''):
        user = User.objects.select_for_update().get(pk=user.pk)
        _check_password(user, current_password)
        from .security_mfa import verify_factor
        verify_factor(user,otp)
        validate_password(password, user)
        if user.check_password(password):
            raise ValidationError({"password": ["Choose a different password."]})
        user.set_password(password)
        user.auth_version = uuid.uuid4()
        user.save(update_fields=["password", "auth_version", "updated_at"])
        from .security_sessions import revoke_all
        revoke_all(user)
        password_validation.password_changed(password, user)

    @staticmethod
    @transaction.atomic
    def logout_all(*, user):
        User.objects.filter(pk=user.pk).update(auth_version=uuid.uuid4(), updated_at=timezone.now())
        from .security_sessions import revoke_all
        revoke_all(user)

    @staticmethod
    @transaction.atomic
    def request_email_change(*, user, email, current_password, otp=''):
        user = User.objects.select_for_update().get(pk=user.pk)
        _check_password(user, current_password)
        from .security_mfa import verify_factor
        verify_factor(user,otp)
        email = User.objects.normalize_email(email)
        if User.objects.filter(email__iexact=email).exists():
            raise ValidationError({"email": ["Email is already in use."]})
        token = signing.dumps({"user": str(user.pk), "email": email, "version": str(user.auth_version),
                               "credential": user.get_session_auth_hash(), "previous_email": user.email},
                              salt="users.email-change.v1")
        queue_email(user=user, recipient=email, purpose="change-email", token=token)

    @staticmethod
    @transaction.atomic
    def confirm_email_change(*, uid, token):
        user = _locked_user(uid)
        try:
            data = signing.loads(token, salt="users.email-change.v1", max_age=settings.PASSWORD_RESET_TIMEOUT)
        except signing.BadSignature as exc:
            raise ValidationError({"token": ["Invalid or expired link."]}) from exc
        if (data.get("user") != str(user.pk) or data.get("version") != str(user.auth_version)
                or data.get("credential") != user.get_session_auth_hash()
                or data.get("previous_email") != user.email):
            raise ValidationError({"token": ["Invalid or expired link."]})
        user.email = data["email"]
        user.is_email_verified = True
        user.auth_version = uuid.uuid4()
        try:
            with transaction.atomic():
                user.save(update_fields=["email", "is_email_verified", "auth_version", "updated_at"])
                from .security_sessions import revoke_all
                revoke_all(user)
        except IntegrityError as exc:
            raise ValidationError({"email": ["Email is already in use."]}) from exc
