import secrets
from django.contrib.auth.hashers import make_password
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import permissions, serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.throttling import UserRateThrottle
from .browser_views import BrowserAccountView, set_refresh_cookie
from .serializers import StrictInputMixin
from .security_models import AccountSecurity, AccountSession, LoginIdentity, PasskeyCredential, AccountDeletionRequest
from .security_config import origin_for, value
from .security_mfa import step_up
from . import security_mfa as mfa, security_identities as identities, security_passkeys as passkeys, security_sms as sms
from . import security_deletion as deletion
from .throttles import AccountIPThrottle, AccountSecurityUserThrottle


class BeginInput(StrictInputMixin,serializers.Serializer):
    current_password=serializers.CharField(required=False,max_length=128,trim_whitespace=False)
    otp=serializers.CharField(required=False,allow_blank=True,max_length=128)
    address=serializers.CharField(required=False,max_length=64)
    phone=serializers.CharField(required=False,max_length=16)


class FinishInput(StrictInputMixin,serializers.Serializer):
    challenge_id=serializers.UUIDField()
    signature=serializers.CharField(required=False,max_length=256)
    credential=serializers.JSONField(required=False)
    code=serializers.CharField(required=False,max_length=20)
    otp=serializers.CharField(required=False,allow_blank=True,max_length=128)
    name=serializers.CharField(required=False,max_length=80)


class StepInput(BeginInput):
    reason=serializers.CharField(required=False,allow_blank=True,max_length=500)


def validated(serializer,data):
    s=serializer(data=data);s.is_valid(raise_exception=True);return s.validated_data


def require(data,key):
    if key not in data:
        raise serializers.ValidationError({key:'This field is required.'})
    return data[key]


def begin(kind,data,request,user=None):
    if kind=='solana':
        return identities.wallet_begin(origin_for(request),require(data,'address'),user)
    if kind=='google':
        return identities.google_begin(origin_for(request),user)
    if kind=='phone':
        return identities.phone_begin(require(data,'phone'),user)
    if kind=='passkey':
        return passkeys.begin(origin_for(request),user)
    raise serializers.ValidationError('Unknown login method.')


def finish(kind,data,request,user=None):
    common={'challenge_id':data['challenge_id'],'request':request,'user':user,'otp':data.get('otp','')}
    if kind=='solana':
        return identities.wallet_finish(signature=require(data,'signature'),origin=origin_for(request),**common)
    if kind=='google':
        credential=require(data,'credential')
        if not isinstance(credential,str) or len(credential)>20000:
            raise serializers.ValidationError('Invalid Google credential.')
        return identities.google_finish(credential=credential,origin=origin_for(request),**common)
    if kind=='phone':
        return identities.phone_finish(code=require(data,'code'),**common)
    if kind=='passkey':
        credential=require(data,'credential')
        if not isinstance(credential,dict):
            raise serializers.ValidationError('Invalid passkey credential.')
        return passkeys.finish(credential=credential,origin=origin_for(request),name=data.get('name','Passkey'),**common)
    raise serializers.ValidationError('Unknown login method.')


class LoginOptionsView(APIView):
    authentication_classes=[]
    permission_classes=[permissions.AllowAny]

    def get(self,request):
        return Response({'google_client_id':value('ACCOUNT_GOOGLE_CLIENT_ID'),'sms_available':sms.configured(),
                         'wallet_network':'SOLANA','solana_chain':value('ACCOUNT_SOLANA_CHAIN','solana:mainnet')})


class BrowserIdentityView(BrowserAccountView):
    throttle_scope='account_login'

    def post(self,request,kind,stage):
        self.enforce_csrf(request)
        if stage=='begin':
            return Response(begin(kind,validated(BeginInput,request.data),request))
        tokens=finish(kind,validated(FinishInput,request.data),request)
        response=Response({'access':tokens['access']})
        set_refresh_cookie(response,tokens['refresh']);response['Cache-Control']='no-store'
        return response


class PrivateSecurityView(APIView):
    permission_classes=[permissions.IsAuthenticated]
    throttle_classes=[AccountIPThrottle,AccountSecurityUserThrottle]
    throttle_scope='account_security'

    def reauthenticate(self,request,data):
        return step_up(request.user,require(data,'current_password'),data.get('otp',''))


class SecuritySummaryView(PrivateSecurityView):
    throttle_classes=[UserRateThrottle]
    def get(self,request):
        current=request.auth.get('session_id') if request.auth else None
        security=AccountSecurity.objects.filter(user=request.user).first()
        pending=AccountDeletionRequest.objects.filter(user=request.user,status='PENDING').first()
        response=Response({'totp_enabled':bool(security and security.totp_enabled),
            'recovery_codes_remaining':len(security.recovery_hashes) if security else 0,
            'sessions':[{'id':str(s.pk),'current':str(s.pk)==str(current),'method':s.method,'user_agent':s.user_agent,
                'ip_address':s.ip_address,'created_at':s.created_at.isoformat(),'last_seen_at':s.last_seen_at.isoformat(),
                'expires_at':s.expires_at.isoformat()} for s in AccountSession.objects.filter(user=request.user,revoked_at__isnull=True,expires_at__gt=timezone.now()).order_by('-last_seen_at')],
            'identities':list(LoginIdentity.objects.filter(user=request.user).values('id','provider','subject','label')),
            'passkeys':list(PasskeyCredential.objects.filter(user=request.user).values('id','name','created_at','last_used_at')),
            'deletion_request':{'id':str(pending.pk),'eligible_after':pending.eligible_after.isoformat()} if pending else None})
        response['Cache-Control']='no-store';return response


class IdentityLinkView(PrivateSecurityView):
    def post(self,request,kind,stage):
        if stage=='begin':
            data=validated(BeginInput,request.data);user=self.reauthenticate(request,data)
            result=begin(kind,data,request,user)
        else:
            result=finish(kind,validated(FinishInput,request.data),request,request.user)
        response=Response(result);response['Cache-Control']='no-store';return response


class SecurityActionView(PrivateSecurityView):
    def post(self,request,action,object_id=None):
        if action=='totp-confirm':
            data=validated(FinishInput,request.data)
            result=mfa.finish(request.user,data['challenge_id'],require(data,'code'),request.auth.get('session_id'))
        else:
            data=validated(StepInput,request.data);user=self.reauthenticate(request,data)
            if action=='totp-begin':
                result=mfa.begin(user)
            elif action=='totp-disable':
                with transaction.atomic():
                    AccountSecurity.objects.filter(user=user).update(totp_enabled=False,totp_secret='',recovery_hashes=[],last_totp_counter=-1)
                    AccountSession.objects.filter(user=user,revoked_at__isnull=True).exclude(pk=request.auth.get('session_id')).update(revoked_at=timezone.now())
                result={'enabled':False}
            elif action=='recovery-rotate':
                if not AccountSecurity.objects.filter(user=user,totp_enabled=True).exists():
                    raise serializers.ValidationError('Enable the authenticator first.')
                codes=[secrets.token_hex(10) for _ in range(8)]
                AccountSecurity.objects.filter(user=user).update(recovery_hashes=[make_password(c) for c in codes])
                result={'recovery_codes':codes}
            elif action=='session-revoke':
                session=get_object_or_404(AccountSession,pk=object_id,user=user)
                AccountSession.objects.filter(pk=session.pk,revoked_at__isnull=True).update(revoked_at=timezone.now())
                result={'revoked':True,'current':str(session.pk)==str(request.auth.get('session_id'))}
            elif action in {'identity-unlink','passkey-delete'}:
                model=LoginIdentity if action=='identity-unlink' else PasskeyCredential
                obj=get_object_or_404(model,pk=object_id,user=user)
                with transaction.atomic():
                    obj.delete()
                    AccountSession.objects.filter(user=user,revoked_at__isnull=True).exclude(pk=request.auth.get('session_id')).update(revoked_at=timezone.now())
                result={'removed':True}
            elif action=='deletion-request':
                result=deletion.request_deletion(user,data.get('reason',''))
            elif action=='deletion-cancel':
                with transaction.atomic():
                    rows=AccountDeletionRequest.objects.select_for_update().filter(user=user,status='PENDING')
                    count=rows.update(status='CANCELLED',resolved_at=timezone.now())
                result={'cancelled':bool(count)}
            else:
                raise serializers.ValidationError('Unknown security action.')
        response=Response(result);response['Cache-Control']='no-store';return response
