"""JWT authentication for the client portals, built on SimpleJWT + DRF.

Design notes:
- testing_app.User is NOT a Django auth user (no AbstractBaseUser). Rather than
  forcing that invasive change, this module authenticates JWTs manually and
  resolves the `user_id` claim straight to testing_app.models.User. Nothing
  about the existing user/password/session architecture changes.
- Every access/refresh token also carries a `role` claim ('OPERATOR' or
  'ADMIN') identifying which portal it was issued for - the same concept the
  old ApiToken.role field held. Permission classes check this claim.
- Device trust: a `DeviceTrust` row remembers a browser (via a client-generated
  device_id) for DEVICE_TRUST_DAYS, letting the operator portal skip OTP on
  logins from that browser until it expires (same moment the refresh token
  itself expires, by design).
"""
from datetime import timedelta

from django.conf import settings
from django.utils import timezone
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.permissions import BasePermission
from rest_framework.response import Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework_simplejwt.tokens import AccessToken, RefreshToken

from testing_app.models import DeviceTrust, User


# ---------------------------------------------------------------------------
# Authentication
# ---------------------------------------------------------------------------

class YatraJWTAuthentication(BaseAuthentication):
    """Reads `Authorization: Bearer <access token>`, resolves to a User."""

    def authenticate_header(self, request):
        # Without this, DRF silently turns every AuthenticationFailed (expired/
        # invalid token) into a 403 instead of a 401 - which breaks the client's
        # "401 -> try to refresh, else log out" logic. See DRF's
        # APIView.handle_exception: it checks get_authenticate_header() and
        # coerces to 403 whenever it's falsy.
        return 'Bearer'

    def authenticate(self, request):
        header = request.META.get('HTTP_AUTHORIZATION', '')
        if not header.startswith('Bearer '):
            return None  # let DRF fall through to "not authenticated"

        raw = header[7:].strip()
        if not raw:
            return None

        try:
            token = AccessToken(raw)
        except TokenError as e:
            raise AuthenticationFailed(str(e) or 'Invalid or expired token.')

        user = User.objects.select_related('role').filter(id=token.get('user_id')).first()
        if user is None:
            raise AuthenticationFailed('Account no longer exists.')
        if user.status != 'ACTIVE':
            raise AuthenticationFailed(f'Your account is {user.status}.')
        if token.get('tv') != user.token_version:
            raise AuthenticationFailed('Session was signed out remotely. Please login again.')

        return (user, token)  # token becomes request.auth


class IsOperatorPortal(BasePermission):
    message = 'This action needs OPERATOR access.'

    def has_permission(self, request, view):
        return bool(request.auth) and request.auth.get('role') == 'OPERATOR'


class IsAdminPortal(BasePermission):
    message = 'This action needs ADMIN access.'

    def has_permission(self, request, view):
        return bool(request.auth) and request.auth.get('role') == 'ADMIN'


# ---------------------------------------------------------------------------
# Token issuance
# ---------------------------------------------------------------------------

def issue_tokens(user, role):
    """Create a fresh access+refresh JWT pair for a portal login."""
    refresh = RefreshToken.for_user(user)
    refresh['role'] = role
    refresh['tv'] = user.token_version
    access = refresh.access_token
    access['role'] = role
    access['tv'] = user.token_version
    return {
        'access': str(access),
        'refresh': str(refresh),
        'expires_in': int(settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'].total_seconds()),
        'refresh_expires_at': timezone.localtime(
            timezone.now() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME']
        ).strftime('%Y-%m-%d %H:%M'),
    }


def revoke_refresh_token(token):
    """Revoke a single decoded RefreshToken (used by /auth/logout and rotation)."""
    from .models import RevokedToken
    RevokedToken.objects.get_or_create(
        jti=token['jti'],
        defaults={'user_id': token.get('user_id')},
    )


def is_refresh_token_revoked(token):
    from .models import RevokedToken
    return RevokedToken.objects.filter(jti=token['jti']).exists()


def user_public_dict(user, role):
    return {
        'id': user.id, 'full_name': user.full_name, 'email': user.email, 'role': role,
        'avatar_color': user.avatar_color,
        'avatar_url': '',  # filled in by caller when a request is available (needs build_absolute_uri)
        'has_password': not (user.password_hash or '').startswith('!'),
    }


# ---------------------------------------------------------------------------
# Device trust (operator OTP-skip on a remembered browser)
# ---------------------------------------------------------------------------

def check_device_trust(user, device_id):
    """True if this (user, device_id) pair is currently trusted."""
    if not device_id:
        return False
    trust = DeviceTrust.objects.filter(user=user, device_id=device_id).first()
    if trust is None or not trust.is_valid():
        return False
    trust.last_used_at = timezone.now()
    trust.save(update_fields=['last_used_at'])
    return True


def grant_device_trust(user, device_id):
    """(Re)issue a 7-day trust window for this browser, tied to the same
    lifetime as the refresh token so both expire together."""
    if not device_id:
        return
    expires = timezone.now() + timedelta(days=settings.DEVICE_TRUST_DAYS)
    DeviceTrust.objects.update_or_create(
        user=user, device_id=device_id,
        defaults={'expires_at': expires, 'last_used_at': timezone.now()},
    )


# ---------------------------------------------------------------------------
# Exception handling - keep the existing {ok:false, error:"..."} envelope
# ---------------------------------------------------------------------------

def yatra_exception_handler(exc, context):
    from rest_framework.views import exception_handler as drf_exception_handler
    response = drf_exception_handler(exc, context)
    if response is None:
        return None

    detail = response.data
    if isinstance(detail, dict) and 'detail' in detail:
        message = str(detail['detail'])
    elif isinstance(detail, dict) and detail:
        # Serializer-style field errors -> take the first message
        first_key = next(iter(detail))
        val = detail[first_key]
        message = str(val[0]) if isinstance(val, list) else str(val)
    elif isinstance(detail, list) and detail:
        message = str(detail[0])
    else:
        message = 'Request failed.'

    return Response({'ok': False, 'error': message}, status=response.status_code)
