"""Auth endpoints for the client portals - JWT (SimpleJWT) based.

Operator portal: POST /api/auth/login  (password)
                    -> device already trusted?  JWT pair directly
                    -> otherwise: OTP emailed + signed ticket
                 POST /api/auth/verify-otp (ticket + code) -> JWT pair
                    (also trusts this browser for DEVICE_TRUST_DAYS)
Admin portal:    POST /api/auth/login  (password)  -> JWT pair directly
Both:            GET  /api/auth/me   ·   POST /api/auth/logout
                 POST /api/auth/refresh        (silent access-token renewal)
                 POST /api/auth/logout-all     (FR-3: blacklist every session)

All checks (lockout, account status, password hashing, OTP rules) reuse the
exact helpers from testing_app.views so web and API behave identically.
"""
from datetime import timedelta

from django.core import signing
from django.utils import timezone
from django.utils.crypto import constant_time_compare
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.exceptions import TokenError
from rest_framework_simplejwt.tokens import RefreshToken

from testing_app.models import LoginOTP, User, UserRoleAccess
from testing_app.views import (
    LOCKOUT_MINUTES, MAX_LOGIN_ATTEMPTS, OTP_MAX_TRIES,
    _generate_and_send_otp, grant_role, log_login, verify_password,
)

from .auth import body, err, jwt_required, ok
from .jwt_auth import (check_device_trust, grant_device_trust, issue_tokens,
                       is_refresh_token_revoked, revoke_refresh_token,
                       user_public_dict)

OTP_TICKET_MAX_AGE = 10 * 60  # seconds a login ticket stays valid
PORTAL_ROLES = {'operator': 'OPERATOR', 'admin': 'ADMIN'}


def _allowed_roles(user):
    roles = {user.role.name}
    roles.update(
        UserRoleAccess.objects.filter(user=user).values_list('role__name', flat=True)
    )
    return roles


def _token_response(request, user, role):
    """Build the full login payload: JWT pair + the user dict the client stores."""
    tokens = issue_tokens(user, role)
    u = user_public_dict(user, role)
    if user.avatar:
        u['avatar_url'] = request.build_absolute_uri(user.avatar.url)
    return {**tokens, 'user': u}


@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
    data = body(request)
    portal = str(data.get('portal', '')).lower()
    email = str(data.get('email', '')).strip()
    password = str(data.get('password', ''))
    device_id = str(data.get('device_id', '') or '').strip()[:100]

    role_needed = PORTAL_ROLES.get(portal)
    if role_needed is None:
        return err("portal must be 'operator' or 'admin'.")
    if not email or not password:
        return err('Please enter both email and password.')

    user = User.objects.select_related('role').filter(email__iexact=email).first()
    if user is None:
        log_login(request, 'FAILED', email=email, message=f'API {portal}: unknown email')
        return err('Invalid email or password.', status=401)

    now = timezone.now()

    if user.account_locked_until and user.account_locked_until > now:
        remaining = int((user.account_locked_until - now).total_seconds() // 60) + 1
        log_login(request, 'LOCKED', user=user, message=f'API {portal}: attempt while locked')
        return err(f'Account locked. Try again in {remaining} minute(s).', status=423)

    if user.status != 'ACTIVE':
        log_login(request, 'FAILED', user=user, message=f'API {portal}: account {user.status}')
        return err(f'Your account is {user.status}. Contact the administrator.', status=403)

    if (user.password_hash or '').startswith('!'):
        log_login(request, 'FAILED', user=user, message=f'API {portal}: Google-only account')
        return err('This account was created with Google and has no password. Use the website Google login, or ask the admin to set a password.', status=403)

    if not verify_password(user, password):
        user.failed_login_attempts += 1
        if user.failed_login_attempts >= MAX_LOGIN_ATTEMPTS:
            user.account_locked_until = now + timedelta(minutes=LOCKOUT_MINUTES)
            user.save(update_fields=['failed_login_attempts', 'account_locked_until'])
            log_login(request, 'LOCKED', user=user,
                      message=f'API {portal}: locked after {MAX_LOGIN_ATTEMPTS} failed attempts')
            return err(f'Too many failed attempts. Account locked for {LOCKOUT_MINUTES} minutes.', status=423)
        user.save(update_fields=['failed_login_attempts'])
        log_login(request, 'FAILED', user=user,
                  message=f'API {portal}: wrong password (attempt {user.failed_login_attempts}/{MAX_LOGIN_ATTEMPTS})')
        return err(f'Invalid email or password. Attempt {user.failed_login_attempts} of {MAX_LOGIN_ATTEMPTS}.', status=401)

    # Correct password — role gate for the portal
    if role_needed == 'ADMIN' and user.role.name != 'ADMIN':
        log_login(request, 'FAILED', user=user, message='API admin portal: not an admin account')
        return err('This account does not have ADMIN access.', status=403)
    if role_needed == 'OPERATOR' and 'OPERATOR' not in _allowed_roles(user):
        log_login(request, 'FAILED', user=user, message='API operator portal: no OPERATOR access')
        return err('This account does not have OPERATOR access.', status=403)

    user.failed_login_attempts = 0
    user.account_locked_until = None
    user.save(update_fields=['failed_login_attempts', 'account_locked_until'])

    if role_needed == 'ADMIN':
        user.last_login_at = now
        user.save(update_fields=['last_login_at'])
        log_login(request, 'SUCCESS', user=user, message='API login (admin portal)', role_name='ADMIN')
        return ok(_token_response(request, user, 'ADMIN'))

    # Operator portal - skip OTP entirely if this browser was verified in the last 7 days
    if check_device_trust(user, device_id):
        grant_role(user, 'OPERATOR')
        user.last_login_at = now
        user.save(update_fields=['last_login_at'])
        log_login(request, 'SUCCESS', user=user,
                  message='API login (operator portal, trusted device - no OTP)', role_name='OPERATOR')
        return ok(_token_response(request, user, 'OPERATOR'))

    # Otherwise: second step, email OTP (reuses the web helper + rate limit)
    sent, otp_error = _generate_and_send_otp(request, user)
    if not sent:
        return err(otp_error, status=429)
    ticket = signing.dumps({'uid': user.id, 'p': 'otp', 'role': 'OPERATOR', 'device_id': device_id})
    return ok({'otp_required': True, 'ticket': ticket, 'email': user.email})


@api_view(['POST'])
@permission_classes([AllowAny])
def verify_otp(request):
    data = body(request)
    ticket = data.get('ticket', '')
    code = str(data.get('code', '')).strip()

    try:
        payload = signing.loads(ticket, max_age=OTP_TICKET_MAX_AGE)
        assert payload.get('p') == 'otp'
    except Exception:
        return err('Login expired. Please start again.', status=401)

    user = User.objects.select_related('role').filter(id=payload.get('uid')).first()
    if user is None or user.status != 'ACTIVE':
        return err('Login expired. Please start again.', status=401)

    otp = LoginOTP.objects.filter(user=user, used=False).order_by('-created_at').first()

    if otp is None or otp.expires_at < timezone.now():
        log_login(request, 'FAILED', user=user, message='API OTP expired or missing')
        return err('That code has expired. Use "Resend code" to get a new one.', status=410)

    if otp.attempts >= OTP_MAX_TRIES:
        otp.used = True
        otp.save(update_fields=['used'])
        log_login(request, 'FAILED', user=user, message='API: too many wrong OTP entries')
        return err('Too many wrong tries. Use "Resend code" to get a new one.', status=410)

    if not constant_time_compare(otp.code, code):
        otp.attempts += 1
        otp.save(update_fields=['attempts'])
        log_login(request, 'FAILED', user=user,
                  message=f'API: wrong OTP code (try {otp.attempts}/{OTP_MAX_TRIES})')
        return err(f'Wrong code. Try {otp.attempts} of {OTP_MAX_TRIES}.', status=401)

    otp.used = True
    otp.save(update_fields=['used'])
    if not user.email_verified:
        user.email_verified = True
        user.save(update_fields=['email_verified'])

    grant_role(user, 'OPERATOR')  # ensures the Operator company profile exists

    # Signup tickets carry the company name chosen on the form
    company = str(payload.get('company', '') or '').strip()
    if company:
        from testing_app.models import Operator
        Operator.objects.filter(user=user).update(company_name=company)

    # Remember this browser for DEVICE_TRUST_DAYS so future logins skip OTP
    device_id = str(payload.get('device_id', '') or '').strip()
    grant_device_trust(user, device_id)

    user.last_login_at = timezone.now()
    user.save(update_fields=['last_login_at'])
    log_login(request, 'SUCCESS', user=user, message='API OTP verified (operator portal)', role_name='OPERATOR')
    return ok(_token_response(request, user, 'OPERATOR'))


@api_view(['POST'])
@permission_classes([AllowAny])
def resend_otp(request):
    data = body(request)
    try:
        payload = signing.loads(data.get('ticket', ''), max_age=OTP_TICKET_MAX_AGE)
        assert payload.get('p') == 'otp'
    except Exception:
        return err('Login expired. Please start again.', status=401)
    user = User.objects.select_related('role').filter(id=payload.get('uid')).first()
    if user is None:
        return err('Login expired. Please start again.', status=401)
    sent, otp_error = _generate_and_send_otp(request, user, purpose='resend')
    if not sent:
        return err(otp_error, status=429)
    return ok({'resent': True, 'email': user.email})


@api_view(['POST'])
@permission_classes([AllowAny])
def refresh(request):
    """Silently renew an access token using the refresh token (rotated: old
    one is revoked, a brand new pair is issued)."""
    data = body(request)
    raw = str(data.get('refresh', '') or '')
    if not raw:
        return err('Missing refresh token.', status=401)

    try:
        old = RefreshToken(raw)
    except TokenError as e:
        return err(str(e) or 'Session expired. Please login again.', status=401)

    if is_refresh_token_revoked(old):
        return err('Session expired. Please login again.', status=401)

    role = old.get('role')
    user = User.objects.select_related('role').filter(id=old.get('user_id')).first()
    if user is None or user.status != 'ACTIVE':
        return err('Session expired. Please login again.', status=401)
    if old.get('tv') != user.token_version:
        return err('Session was signed out remotely. Please login again.', status=401)

    revoke_refresh_token(old)  # rotation: the old refresh token can't be reused
    return ok(_token_response(request, user, role))


@api_view(['GET'])
@jwt_required()
def me(request):
    u = request.api_user
    token = request.api_token
    from datetime import datetime, timezone as dt_tz
    expires_at = datetime.fromtimestamp(token['exp'], tz=dt_tz.utc)
    return ok({'id': u.id, 'full_name': u.full_name, 'email': u.email,
               'role': token.get('role'),
               'expires_at': timezone.localtime(expires_at).strftime('%Y-%m-%d %H:%M')})


@api_view(['POST'])
@jwt_required()
def logout(request):
    data = body(request)
    raw_refresh = str(data.get('refresh', '') or '')
    if raw_refresh:
        try:
            revoke_refresh_token(RefreshToken(raw_refresh))
        except TokenError:
            pass  # already invalid/expired - fine, we're logging out anyway
    log_login(request, 'LOGOUT', user=request.api_user,
              message=f'API logout ({request.api_token.get("role", "").lower()} portal)',
              role_name=request.api_token.get('role'))
    return ok({'logged_out': True})


@api_view(['POST'])
@jwt_required()
def logout_all(request):
    """FR-3: instantly invalidate every access/refresh token this user holds,
    on every device, by bumping their token_version (embedded in every JWT)."""
    user = request.api_user
    user.token_version += 1
    user.save(update_fields=['token_version'])
    log_login(request, 'LOGOUT', user=user, message='API logout-all (every session revoked)',
              role_name=request.api_token.get('role'))
    return ok({'revoked': True})


# ---------------------------------------------------------------------------
# Google sign-in for the OPERATOR portal (mirrors the website's Google flow,
# but returns JWT tokens to the client instead of a session).
# ---------------------------------------------------------------------------

DEFAULT_CLIENT_LOGIN = 'http://localhost:8090/operator/pages/login.html'


def _client_next(request):
    nxt = request.GET.get('next', '')
    if nxt.startswith('http://localhost') or nxt.startswith('http://127.0.0.1'):
        return nxt
    return DEFAULT_CLIENT_LOGIN


def google_start(request):
    """Send the operator to Google's consent screen (API-client flow)."""
    from urllib.parse import urlencode
    from django.conf import settings
    from django.shortcuts import redirect

    device_id = str(request.GET.get('device_id', '') or '').strip()[:100]
    state = signing.dumps({'p': 'gapi', 'next': _client_next(request), 'device_id': device_id})
    params = {
        'client_id': settings.GOOGLE_CLIENT_ID,
        'redirect_uri': request.build_absolute_uri('/api/auth/google/callback'),
        'response_type': 'code',
        'scope': 'openid email profile',
        'state': state,
        'prompt': 'select_account',
    }
    return redirect('https://accounts.google.com/o/oauth2/v2/auth?' + urlencode(params))


def google_callback(request):
    """Google returns here; we log the operator in and hand tokens to the client."""
    import requests
    from urllib.parse import urlencode
    from django.conf import settings
    from django.contrib.auth.hashers import make_password
    from django.shortcuts import redirect
    from testing_app.models import Role

    # Recover where to send the user back, even on errors
    try:
        payload = signing.loads(request.GET.get('state', ''), max_age=600)
        assert payload.get('p') == 'gapi'
        nxt = payload.get('next') or DEFAULT_CLIENT_LOGIN
        device_id = str(payload.get('device_id', '') or '').strip()
    except Exception:
        nxt = DEFAULT_CLIENT_LOGIN
        payload = None
        device_id = ''

    def back(**frag):
        return redirect(nxt + '#' + urlencode(frag))

    if payload is None:
        return back(gerror='Google login failed (state mismatch). Please try again.')
    if request.GET.get('error'):
        return back(gerror=f"Google login cancelled ({request.GET['error']}).")
    code = request.GET.get('code')
    if not code:
        return back(gerror='Google login failed (no code returned).')

    try:
        token_resp = requests.post(
            'https://oauth2.googleapis.com/token',
            data={
                'client_id': settings.GOOGLE_CLIENT_ID,
                'client_secret': settings.GOOGLE_CLIENT_SECRET,
                'code': code,
                'grant_type': 'authorization_code',
                'redirect_uri': request.build_absolute_uri('/api/auth/google/callback'),
            },
            timeout=15,
        )
        access_token = token_resp.json().get('access_token')
        if not access_token:
            return back(gerror='Google login failed (could not get token).')
        info = requests.get(
            'https://www.googleapis.com/oauth2/v3/userinfo',
            headers={'Authorization': f'Bearer {access_token}'},
            timeout=15,
        ).json()
    except requests.RequestException:
        return back(gerror='Could not reach Google. Check your internet and try again.')

    email = info.get('email')
    if not email:
        return back(gerror='Google did not return an email address.')

    user = User.objects.select_related('role').filter(email__iexact=email).first()

    if user is None:
        # First Google login on the operator portal -> create an operator account
        role_obj, _ = Role.objects.get_or_create(name='OPERATOR')
        user = User.objects.create(
            role=role_obj,
            full_name=info.get('name') or email.split('@')[0],
            email=email,
            password_hash=make_password(None),  # Google-only until admin/self sets one
            email_verified=True,
            status='ACTIVE',
        )

    if user.role.name == 'ADMIN':
        log_login(request, 'FAILED', user=user, message='API Google login not allowed for admin accounts')
        return back(gerror='Admin accounts must login with email + password.')
    if user.status != 'ACTIVE':
        log_login(request, 'FAILED', user=user, message=f'API Google login blocked, account {user.status}')
        return back(gerror=f'Your account is {user.status}. Contact the administrator.')

    grant_role(user, 'OPERATOR')  # ensures the Operator company profile exists

    # Trusted browser -> skip OTP entirely, issue tokens right away
    if check_device_trust(user, device_id):
        user.last_login_at = timezone.now()
        user.save(update_fields=['last_login_at'])
        log_login(request, 'SUCCESS', user=user,
                  message='API Google login (trusted device - no OTP)', role_name='OPERATOR')
        tokens = issue_tokens(user, 'OPERATOR')
        return back(gaccess=tokens['access'], grefresh=tokens['refresh'],
                    gname=user.full_name, gemail=user.email)

    # Same rule as email login otherwise: still needs the email OTP step
    sent, otp_error = _generate_and_send_otp(request, user)
    if not sent:
        return back(gerror=otp_error)
    ticket = signing.dumps({'uid': user.id, 'p': 'otp', 'role': 'OPERATOR', 'device_id': device_id})
    return back(gticket=ticket, gemail=user.email)


@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
    """Operator signup with email + password. Finishes with the same OTP step."""
    from django.contrib.auth.hashers import make_password
    from testing_app.models import Role

    data = body(request)
    full_name = str(data.get('full_name', '')).strip()
    company_name = str(data.get('company_name', '')).strip()
    email = str(data.get('email', '')).strip()
    phone = str(data.get('phone', '') or '').strip()
    password = str(data.get('password', ''))
    device_id = str(data.get('device_id', '') or '').strip()[:100]

    if not full_name or not company_name or not email or not password:
        return err('Please fill in your name, company name, email and password.')
    if '@' not in email or '.' not in email:
        return err('Please enter a valid email address.')
    if len(password) < 6:
        return err('Password must be at least 6 characters.')
    if User.objects.filter(email__iexact=email).exists():
        return err('This email is already registered. Please sign in instead.', status=409)

    role_obj, _ = Role.objects.get_or_create(name='OPERATOR')
    user = User.objects.create(
        role=role_obj, full_name=full_name, email=email,
        phone=phone or None,
        password_hash=make_password(password),
        email_verified=False, status='ACTIVE',
    )

    sent, otp_error = _generate_and_send_otp(request, user, purpose='signup')
    if not sent:
        return err(f'Account created, but the code email failed ({otp_error}). '
                   'Go to Sign in and login - a new code will be sent.', status=502)
    ticket = signing.dumps({'uid': user.id, 'p': 'otp', 'role': 'OPERATOR',
                            'company': company_name, 'device_id': device_id})
    return ok({'otp_required': True, 'ticket': ticket, 'email': user.email})
