from collections.abc import Mapping
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, TokenRefreshSerializer
from rest_framework_simplejwt.tokens import RefreshToken

from .authentication import AccountJWTAuthentication
from .models import User
from .services import AccountService


class StrictInputMixin:
    def to_internal_value(self, data):
        if isinstance(data, Mapping):
            allowed = {name for name, field in self.fields.items() if not field.read_only}
            unexpected = set(data) - allowed
            if unexpected:
                raise serializers.ValidationError({key: ["This field cannot be supplied."] for key in unexpected})
        return super().to_internal_value(data)


class TimezoneMixin:
    def validate_timezone(self, value):
        try:
            ZoneInfo(value)
        except (ZoneInfoNotFoundError, ValueError) as exc:
            raise serializers.ValidationError("Use a valid IANA timezone, such as Asia/Tehran.") from exc
        return value


class RegistrationSerializer(StrictInputMixin, TimezoneMixin, serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, trim_whitespace=False, max_length=128)
    password_confirm = serializers.CharField(write_only=True, trim_whitespace=False, max_length=128)

    class Meta:
        model = User
        fields = ["email", "username", "password", "password_confirm", "first_name", "last_name",
                  "display_name", "language", "timezone"]

    def validate_email(self, value):
        value = User.objects.normalize_email(value)
        if User.objects.filter(email__iexact=value).exists():
            raise serializers.ValidationError("Email is already in use.")
        return value

    def validate(self, attrs):
        if attrs["password"] != attrs.pop("password_confirm"):
            raise serializers.ValidationError({"password_confirm": ["Passwords do not match."]})
        return attrs

    def create(self, validated_data):
        return AccountService.register(**validated_data)


class ProfileSerializer(StrictInputMixin, TimezoneMixin, serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "email", "username", "first_name", "last_name", "display_name", "language",
                  "timezone", "is_email_verified", "date_joined", "updated_at"]
        read_only_fields = ["id", "email", "username", "is_email_verified", "date_joined", "updated_at"]

    def update(self, instance, validated_data):
        # Do not save stale password/auth_version fields while editing a profile.
        with transaction.atomic():
            user = User.objects.select_for_update().get(pk=instance.pk)
            for name, value in validated_data.items():
                setattr(user, name, value)
            user.save(update_fields=[*validated_data, "updated_at"])
        return user


class LoginSerializer(StrictInputMixin, TokenObtainPairSerializer):
    otp = serializers.CharField(required=False,allow_blank=True,max_length=128,write_only=True)

    def validate(self, attrs):
        from django.contrib.auth import authenticate
        from .security_mfa import verify_factor
        from .security_sessions import issue
        attrs["email"] = User.objects.normalize_email(attrs["email"])
        if len(attrs["password"]) > 128:
            raise AuthenticationFailed("Invalid credentials.")
        request=self.context.get('request')
        user=authenticate(request=request,email=attrs['email'],password=attrs['password'])
        if user is None or not user.is_active:
            raise AuthenticationFailed('Invalid credentials.')
        if not user.is_email_verified:
            raise PermissionDenied('Verify your email before signing in.',code='email_unverified')
        with transaction.atomic():
            fresh=User.objects.select_for_update().get(pk=user.pk)
            if fresh.auth_version!=user.auth_version or fresh.password!=user.password:
                raise AuthenticationFailed('Credentials changed. Sign in again.')
            verify_factor(fresh,attrs.get('otp',''))
            self.user=fresh
            return issue(fresh,request)

    @classmethod
    def get_token(cls, user):
        if not user.is_email_verified:
            raise PermissionDenied("Verify your email before signing in.", code="email_unverified")
        from .security_sessions import issue
        return RefreshToken(issue(user)['refresh'])


class RefreshSerializer(StrictInputMixin, TokenRefreshSerializer):
    @transaction.atomic
    def validate(self, attrs):
        token = RefreshToken(attrs["refresh"])
        # Serialize rotation with password changes and logout-all on PostgreSQL.
        user = User.objects.select_for_update().filter(pk=token.get("user_id")).first()
        if user is None:
            raise AuthenticationFailed("Session expired.")
        AccountJWTAuthentication().get_user(token)
        # Revalidate blacklist inside the lock; a concurrent refresh may have used it.
        return super().validate(attrs)


class DetailSerializer(serializers.Serializer):
    detail = serializers.CharField(read_only=True)


class EmailSerializer(StrictInputMixin, serializers.Serializer):
    email = serializers.EmailField(max_length=254)

    def validate_email(self, value):
        return User.objects.normalize_email(value)


class TokenConfirmationSerializer(StrictInputMixin, serializers.Serializer):
    uid = serializers.CharField(max_length=100)
    token = serializers.CharField(max_length=2048)


class NewPasswordSerializer(StrictInputMixin, serializers.Serializer):
    password = serializers.CharField(write_only=True, trim_whitespace=False, max_length=128)
    password_confirm = serializers.CharField(write_only=True, trim_whitespace=False, max_length=128)

    def validate(self, attrs):
        if attrs["password"] != attrs.pop("password_confirm"):
            raise serializers.ValidationError({"password_confirm": ["Passwords do not match."]})
        return attrs


class PasswordResetSerializer(NewPasswordSerializer):
    uid = serializers.CharField(max_length=100)
    token = serializers.CharField(max_length=2048)


class PasswordChangeSerializer(NewPasswordSerializer):
    current_password = serializers.CharField(write_only=True, trim_whitespace=False, max_length=128)
    otp = serializers.CharField(required=False,allow_blank=True,write_only=True,max_length=128)


class EmailChangeSerializer(EmailSerializer):
    current_password = serializers.CharField(write_only=True, trim_whitespace=False, max_length=128)
    otp = serializers.CharField(required=False,allow_blank=True,write_only=True,max_length=128)


class LogoutSerializer(StrictInputMixin, serializers.Serializer):
    refresh = serializers.CharField(max_length=4096)
