from django.contrib.auth import get_user_model from django.db import transaction from drf_spectacular.utils import OpenApiExample, OpenApiResponse, extend_schema, inline_serializer from rest_framework import serializers as drf_serializers from rest_framework import status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_simplejwt.exceptions import TokenError from rest_framework_simplejwt.tokens import RefreshToken from ..models import Role, TeacherProfile from ..services.otp import ( OTPCooldownError, OTPVerificationError, send_otp, verify_otp, ) from ..services.sms import SMSError from .serializers import ( OTPSendSerializer, OTPVerifySerializer, UserSerializer, issue_tokens_for, ) from .throttling import OTPSendByIPThrottle, OTPSendByPhoneThrottle User = get_user_model() _TOKEN_PAIR = inline_serializer( name="TokenPair", fields={ "access": drf_serializers.CharField(), "refresh": drf_serializers.CharField(), }, ) class OTPSendView(APIView): permission_classes = [AllowAny] throttle_classes = [OTPSendByPhoneThrottle, OTPSendByIPThrottle] @extend_schema( tags=["auth"], summary="Send OTP", description=( "Generate a 4-digit OTP and dispatch it to the given Iranian phone number " "via the configured SMS backend. Phone is normalized to `09xxxxxxxxx`. " "Per-phone cooldown of 60 seconds; per-phone hourly cap of 5; per-IP cap of 30/min." ), request=OTPSendSerializer, responses={ 200: OpenApiResponse( response=inline_serializer( name="OTPSendResponse", fields={ "detail": drf_serializers.CharField(), "phone_number": drf_serializers.CharField(), }, ), description="OTP issued and SMS dispatched.", ), 400: OpenApiResponse(description="Phone number is missing or invalid."), 429: OpenApiResponse( response=inline_serializer( name="OTPCooldownResponse", fields={ "detail": drf_serializers.CharField(), "retry_after": drf_serializers.IntegerField(), }, ), description="Cooldown active or hourly cap reached.", ), 502: OpenApiResponse(description="SMS provider is unreachable or returned an error."), }, examples=[ OpenApiExample( "Iran phone, canonical", value={"phone_number": "09121234567"}, request_only=True, ), OpenApiExample( "Iran phone, E.164", value={"phone_number": "+989121234567"}, request_only=True, ), ], ) def post(self, request): ser = OTPSendSerializer(data=request.data) ser.is_valid(raise_exception=True) phone = ser.validated_data["phone_number"] try: send_otp(phone) except OTPCooldownError as e: return Response( {"detail": "Too soon", "retry_after": e.retry_after}, status=status.HTTP_429_TOO_MANY_REQUESTS, ) except SMSError as e: return Response( {"detail": "Could not send SMS", "error": str(e)}, status=status.HTTP_502_BAD_GATEWAY, ) return Response({"detail": "Code sent", "phone_number": phone}) class OTPVerifyView(APIView): permission_classes = [AllowAny] @extend_schema( tags=["auth"], summary="Verify OTP and login or signup", description=( "Consumes the latest active OTP for the phone.\n\n" "- **Existing user** → login. Body needs only `phone_number` and `code`.\n" "- **New user** → signup. Body must also include `full_name` and `role` (`student` or `teacher`). " "If `role=teacher`, a `TeacherProfile` is auto-created with `is_approved=False` " "(an admin must approve before the teacher can publish courses).\n\n" "Returns access + refresh JWTs on success." ), request=OTPVerifySerializer, responses={ 200: OpenApiResponse( response=inline_serializer( name="OTPVerifyResponse", fields={ "created": drf_serializers.BooleanField(), "user": UserSerializer(), "tokens": _TOKEN_PAIR, }, ), description="Login or signup succeeded; tokens issued.", ), 400: OpenApiResponse( description=( "Possible reasons: invalid code, code expired, no active code for phone, " "too many wrong attempts, or signup fields missing for new account." ) ), }, examples=[ OpenApiExample( "Existing user — login", value={"phone_number": "09121234567", "code": "1234"}, request_only=True, ), OpenApiExample( "New user — signup as teacher", value={ "phone_number": "09121234567", "code": "1234", "full_name": "Ali Reza", "role": "teacher", }, request_only=True, ), ], ) def post(self, request): ser = OTPVerifySerializer(data=request.data) ser.is_valid(raise_exception=True) phone = ser.validated_data["phone_number"] code = ser.validated_data["code"] try: verify_otp(phone, code) except OTPVerificationError as e: return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) user = User.objects.filter(phone_number=phone).first() created = False if user is None: with transaction.atomic(): user = User.objects.create_user( phone_number=phone, full_name=ser.validated_data["full_name"], role=ser.validated_data["role"], ) if user.role == Role.TEACHER: TeacherProfile.objects.create(user=user) created = True tokens = issue_tokens_for(user) return Response( { "created": created, "user": UserSerializer(user).data, "tokens": tokens, } ) class MeView(APIView): permission_classes = [IsAuthenticated] @extend_schema( tags=["auth"], summary="Current user", description="Returns the authenticated user's profile (id, phone, full name, role, email, language).", responses={200: UserSerializer, 401: OpenApiResponse(description="Missing or invalid token.")}, ) def get(self, request): return Response(UserSerializer(request.user).data) class LogoutView(APIView): permission_classes = [IsAuthenticated] @extend_schema( tags=["auth"], summary="Logout (blacklist refresh token)", description="Blacklists the supplied refresh token so it cannot be rotated again. The access token remains valid until natural expiry.", request=inline_serializer( name="LogoutRequest", fields={"refresh": drf_serializers.CharField()}, ), responses={ 204: OpenApiResponse(description="Refresh token blacklisted."), 400: OpenApiResponse(description="Missing or invalid refresh token."), }, ) def post(self, request): refresh = request.data.get("refresh") if not refresh: return Response({"detail": "refresh required"}, status=status.HTTP_400_BAD_REQUEST) try: RefreshToken(refresh).blacklist() except TokenError as e: return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_204_NO_CONTENT)