"""Microsoft Azure AD / Office 365 OAuth login."""

import base64
import json
import secrets
from urllib.parse import urlencode, quote

import requests
from django.conf import settings
from django.contrib.auth import get_user_model, login
from django.http import HttpResponseRedirect
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView


User = get_user_model()

# Multi-tenant values are unsafe for callback validation: any Microsoft account
# in any tenant could authenticate. Force a specific tenant GUID or domain.
MULTI_TENANT_VALUES = {"common", "organizations", "consumers"}


def _authority():
    return f"https://login.microsoftonline.com/{settings.MICROSOFT_TENANT}"


def _scopes():
    return "openid profile email User.Read offline_access"


def _decode_jwt_payload(token: str) -> dict:
    """Decode JWT payload without signature verification. Used only to read tid claim
    from id_token already received over TLS from Microsoft token endpoint."""
    try:
        _, payload_b64, _ = token.split(".")
        padded = payload_b64 + "=" * (-len(payload_b64) % 4)
        return json.loads(base64.urlsafe_b64decode(padded))
    except Exception:
        return {}


class MicrosoftLoginView(APIView):
    """Start Microsoft OAuth flow. Redirects browser to Microsoft consent screen."""

    permission_classes = [AllowAny]

    def get(self, request):
        if not settings.MICROSOFT_TENANT or settings.MICROSOFT_TENANT in MULTI_TENANT_VALUES:
            return self._fail("Microsoft OAuth misconfigured: MICROSOFT_TENANT must be a specific tenant GUID")

        state = secrets.token_urlsafe(32)
        nonce = secrets.token_urlsafe(32)
        request.session["ms_oauth_state"] = state
        request.session["ms_oauth_nonce"] = nonce
        next_url = request.GET.get("next") or "/project/dashboard"
        request.session["ms_oauth_next"] = next_url

        params = {
            "client_id": settings.MICROSOFT_CLIENT_ID,
            "response_type": "code",
            "redirect_uri": settings.MICROSOFT_REDIRECT_URI,
            "response_mode": "query",
            "scope": _scopes(),
            "state": state,
            "nonce": nonce,
            "prompt": "select_account",
        }
        url = f"{_authority()}/oauth2/v2.0/authorize?{urlencode(params)}"
        return HttpResponseRedirect(url)

    def _fail(self, msg: str):
        return HttpResponseRedirect(f"{settings.FRONTEND_URL}/login?error={quote(msg)}")


class MicrosoftCallbackView(APIView):
    """Handle Microsoft OAuth callback. Exchange code, fetch user, login session."""

    permission_classes = [AllowAny]

    def get(self, request):
        code = request.GET.get("code")
        state = request.GET.get("state")
        error = request.GET.get("error_description") or request.GET.get("error")

        if error:
            return self._fail(f"OAuth error: {error}")

        if not code:
            return self._fail("Missing authorization code")

        saved_state = request.session.pop("ms_oauth_state", None)
        if not saved_state or saved_state != state:
            return self._fail("Invalid state parameter")

        saved_nonce = request.session.pop("ms_oauth_nonce", None)

        token_resp = requests.post(
            f"{_authority()}/oauth2/v2.0/token",
            data={
                "client_id": settings.MICROSOFT_CLIENT_ID,
                "client_secret": settings.MICROSOFT_CLIENT_SECRET,
                "code": code,
                "redirect_uri": settings.MICROSOFT_REDIRECT_URI,
                "grant_type": "authorization_code",
                "scope": _scopes(),
            },
            timeout=10,
        )
        if token_resp.status_code != 200:
            return self._fail("Token exchange failed")

        token_data = token_resp.json()
        access_token = token_data.get("access_token")
        id_token = token_data.get("id_token")
        if not access_token or not id_token:
            return self._fail("Missing tokens from Microsoft")

        # Validate id_token claims: tenant, audience, nonce.
        # Token came over TLS from Microsoft endpoint authenticated by client_secret —
        # signature check is defense in depth; claim validation is the load-bearing check.
        claims = _decode_jwt_payload(id_token)
        if claims.get("tid") != settings.MICROSOFT_TENANT:
            return self._fail("Tenant mismatch")
        if claims.get("aud") != settings.MICROSOFT_CLIENT_ID:
            return self._fail("Audience mismatch")
        if saved_nonce and claims.get("nonce") != saved_nonce:
            return self._fail("Nonce mismatch")

        me_resp = requests.get(
            "https://graph.microsoft.com/v1.0/me",
            headers={"Authorization": f"Bearer {access_token}"},
            timeout=10,
        )
        if me_resp.status_code != 200:
            return self._fail("Graph /me failed")

        me = me_resp.json()
        # `mail` is the verified, organizationally-assigned address.
        # `userPrincipalName` can be a sign-in alias; do not trust as identity.
        email = (me.get("mail") or "").lower()
        if not email:
            return self._fail("Microsoft account has no verified email")

        first = me.get("givenName") or ""
        last = me.get("surname") or ""
        display = me.get("displayName") or email

        try:
            user = User.objects.get(email=email)
            # Block silent takeover: existing password-account cannot be auto-linked
            # to a Microsoft identity. User must explicitly link from settings.
            if user.has_usable_password() and not getattr(user, "microsoft_linked", False):
                return self._fail("Account exists with password login. Sign in with password, then link Microsoft from settings.")
        except User.DoesNotExist:
            user = User.objects.create_user(email=email, password=None)
            user.set_unusable_password()
            user.first_name = first
            user.last_name = last
            user.save()

        if not user.first_name and first:
            user.first_name = first
        if not user.last_name and last:
            user.last_name = last
        if hasattr(user, "full_name") and not getattr(user, "full_name", None):
            user.full_name = display
        user.save()

        login(request, user)

        next_url = request.session.pop("ms_oauth_next", "/project/dashboard")
        return HttpResponseRedirect(f"{settings.FRONTEND_URL}{next_url}")

    def _fail(self, msg: str):
        return HttpResponseRedirect(
            f"{settings.FRONTEND_URL}/login?error={quote(msg)}"
        )
