"""Class-Based Views for ECP Auth.

Provides views for the complete ECP authentication flow:
login, registration, challenge-response, and logout.
"""

from __future__ import annotations

import logging
from typing import Any

from cryptography import x509 as crypto_x509

from django.contrib.auth import get_user_model, login, logout
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_http_methods

from ecp_auth.crypto import (
    Challenge,
    decode_signature_b64,
    generate_challenge,
    verify_challenge_not_expired,
    verify_signature,
)
from ecp_auth.exceptions import (
    CertificateParsingError,
    CertificateValidationError,
    ECPAuthError,
    SignatureVerificationError,
)
from ecp_auth.forms import CertificateUploadForm, RegisterForm
from ecp_auth.models import UserCertificate
from ecp_auth.parsers import CertificateInfo, extract_certificate_info, parse_pkcs12
from ecp_auth.settings import get_login_redirect_url, get_logout_redirect_url
from ecp_auth.signals import (
    ecp_certificate_linked,
    ecp_login_failed,
    ecp_login_success,
    ecp_logout,
    ecp_user_registered,
)
from ecp_auth.utils import (
    clear_ecp_session,
    get_client_ip,
    get_from_session,
    remove_from_session,
    store_in_session,
)
from ecp_auth.validators import validate_certificate

logger: logging.Logger = logging.getLogger(__name__)

User = get_user_model()


@method_decorator(csrf_protect, name="dispatch")
class ECPLoginView(View):
    """View for ECP certificate-based login.

    Handles the two-step login flow:
    1. POST with PKCS#12 file → validates cert, generates challenge
    2. POST with signed challenge → verifies signature, logs in user

    Template: ``ecp_auth/login.html``
    """

    template_name: str = "ecp_auth/login.html"

    def get(self, request: HttpRequest) -> HttpResponse:
        """Render the login page with the certificate upload form.

        Args:
            request: The HTTP request.

        Returns:
            Rendered login page.
        """
        if request.user.is_authenticated:
            return redirect(get_login_redirect_url())

        form = CertificateUploadForm()
        return render(request, self.template_name, {"form": form, "step": "upload"})

    def post(self, request: HttpRequest) -> HttpResponse:
        """Handle login form submission.

        Dispatches to the appropriate handler based on the current step:
        - Certificate upload → ``_handle_certificate_upload``
        - Challenge response → ``_handle_challenge_response``

        Args:
            request: The HTTP POST request.

        Returns:
            Rendered page or redirect.
        """
        step: str = request.POST.get("step", "upload")

        if step == "upload":
            return self._handle_certificate_upload(request)
        elif step == "challenge":
            return self._handle_challenge_response(request)
        else:
            return render(
                request,
                self.template_name,
                {
                    "form": CertificateUploadForm(),
                    "step": "upload",
                    "error": "Invalid request.",
                },
            )

    def _handle_certificate_upload(self, request: HttpRequest) -> HttpResponse:
        """Process the PKCS#12 certificate upload for login.

        Validates the certificate and generates a challenge nonce.

        Args:
            request: The HTTP request with the uploaded file.

        Returns:
            Challenge page or login page with errors.
        """
        form = CertificateUploadForm(request.POST, request.FILES)

        if not form.is_valid():
            return render(
                request,
                self.template_name,
                {"form": form, "step": "upload"},
            )

        try:
            # Parse PKCS#12 file
            pkcs12_file = form.cleaned_data["pkcs12_file"]
            pkcs12_password: str = form.cleaned_data.get("pkcs12_password", "")
            pkcs12_data: bytes = pkcs12_file.read()

            private_key, certificate, ca_certs = parse_pkcs12(
                pkcs12_data, pkcs12_password or None
            )

            # Validate certificate
            cert_info: CertificateInfo = validate_certificate(certificate)

            # Check if certificate is registered
            try:
                user_cert = UserCertificate.objects.select_related("user").get(
                    fingerprint_sha256=cert_info.fingerprint_sha256,
                    is_active=True,
                )
            except UserCertificate.DoesNotExist:
                ecp_login_failed.send(
                    sender=self.__class__,
                    request=request,
                    reason="Certificate not registered",
                    certificate_info=cert_info,
                )
                return render(
                    request,
                    self.template_name,
                    {
                        "form": CertificateUploadForm(),
                        "step": "upload",
                        "error": (
                            "This certificate is not registered. "
                            "Please register first."
                        ),
                    },
                )

            # Generate challenge
            challenge: Challenge = generate_challenge()

            # Sign the challenge with the private key to prove ownership
            from ecp_auth.crypto import sign_data, encode_signature_b64

            client_signature: bytes = sign_data(private_key, challenge.nonce_bytes)
            client_signature_b64: str = encode_signature_b64(client_signature)

            # Store in session
            store_in_session(request, "challenge", challenge.to_dict())
            store_in_session(
                request, "login_certificate_pem", cert_info.pem_data
            )
            store_in_session(
                request, "login_fingerprint", cert_info.fingerprint_sha256
            )

            return render(
                request,
                self.template_name,
                {
                    "step": "challenge",
                    "challenge_nonce": challenge.nonce,
                    "signature": client_signature_b64,
                    "cert_cn": cert_info.common_name,
                    "cert_org": cert_info.organization,
                },
            )

        except CertificateParsingError as exc:
            logger.warning("Certificate parsing failed during login: %s", exc.message)
            return render(
                request,
                self.template_name,
                {
                    "form": CertificateUploadForm(),
                    "step": "upload",
                    "error": f"Certificate error: {exc.message}",
                },
            )
        except CertificateValidationError as exc:
            logger.warning("Certificate validation failed during login: %s", exc.message)
            return render(
                request,
                self.template_name,
                {
                    "form": CertificateUploadForm(),
                    "step": "upload",
                    "error": f"Validation error: {exc.message}",
                },
            )
        except ECPAuthError as exc:
            logger.error("ECP error during login: %s", exc.message)
            return render(
                request,
                self.template_name,
                {
                    "form": CertificateUploadForm(),
                    "step": "upload",
                    "error": f"Authentication error: {exc.message}",
                },
            )

    def _handle_challenge_response(self, request: HttpRequest) -> HttpResponse:
        """Process the signed challenge response and complete login.

        Args:
            request: The HTTP request with the signed challenge.

        Returns:
            Redirect to success page or login page with errors.
        """
        try:
            # Retrieve challenge and certificate from session
            challenge_data = get_from_session(request, "challenge")
            cert_pem = get_from_session(request, "login_certificate_pem")
            fingerprint = get_from_session(request, "login_fingerprint")
            signature_b64: str = request.POST.get("signature", "")

            if not challenge_data or not cert_pem or not fingerprint:
                return render(
                    request,
                    self.template_name,
                    {
                        "form": CertificateUploadForm(),
                        "step": "upload",
                        "error": "Session expired. Please start the login process again.",
                    },
                )

            # Reconstruct challenge
            challenge = Challenge.from_dict(challenge_data)
            verify_challenge_not_expired(challenge)

            # Load certificate
            from ecp_auth.parsers import parse_pem_certificate

            certificate = parse_pem_certificate(cert_pem)

            # Verify signature
            signature_bytes: bytes = decode_signature_b64(signature_b64)
            verify_signature(certificate, signature_bytes, challenge.nonce_bytes)

            # Look up user
            user_cert = UserCertificate.objects.select_related("user").get(
                fingerprint_sha256=fingerprint,
                is_active=True,
            )

            user = user_cert.user
            if not user.is_active:
                return render(
                    request,
                    self.template_name,
                    {
                        "form": CertificateUploadForm(),
                        "step": "upload",
                        "error": "Your account is inactive.",
                    },
                )

            # Log in the user
            login(request, user, backend="ecp_auth.backends.ECPAuthenticationBackend")

            # Store ECP session data
            cert_info = extract_certificate_info(certificate)
            store_in_session(request, "certificate_fingerprint", fingerprint)
            store_in_session(request, "certificate_cn", cert_info.common_name)

            # Clean up temporary session data
            remove_from_session(request, "challenge")
            remove_from_session(request, "login_certificate_pem")
            remove_from_session(request, "login_fingerprint")

            # Send signal
            ecp_login_success.send(
                sender=self.__class__,
                request=request,
                user=user,
                certificate_info=cert_info,
            )

            logger.info(
                "User %s logged in via ECP (IP: %s).",
                user.username,
                get_client_ip(request),
            )

            return redirect(get_login_redirect_url())

        except ECPAuthError as exc:
            logger.warning("Challenge verification failed: %s", exc.message)
            ecp_login_failed.send(
                sender=self.__class__,
                request=request,
                reason=exc.message,
                certificate_info=None,
            )
            return render(
                request,
                self.template_name,
                {
                    "form": CertificateUploadForm(),
                    "step": "upload",
                    "error": f"Authentication failed: {exc.message}",
                },
            )


@method_decorator(csrf_protect, name="dispatch")
class ECPRegisterView(View):
    """View for user registration with ECP certificate.

    Handles user registration by associating a new account with
    an X.509 certificate from a PKCS#12 file.

    Template: ``ecp_auth/register.html``
    """

    template_name: str = "ecp_auth/register.html"

    def get(self, request: HttpRequest) -> HttpResponse:
        """Render the registration page.

        Args:
            request: The HTTP request.

        Returns:
            Rendered registration page.
        """
        if request.user.is_authenticated:
            return redirect(get_login_redirect_url())

        form = RegisterForm()
        return render(request, self.template_name, {"form": form})

    def post(self, request: HttpRequest) -> HttpResponse:
        """Handle registration form submission.

        Creates a new user account and links it to the uploaded certificate.

        Args:
            request: The HTTP POST request.

        Returns:
            Redirect to login or registration page with errors.
        """
        form = RegisterForm(request.POST, request.FILES)

        if not form.is_valid():
            return render(request, self.template_name, {"form": form})

        try:
            # Parse PKCS#12 file
            pkcs12_file = form.cleaned_data["pkcs12_file"]
            pkcs12_password: str = form.cleaned_data.get("pkcs12_password", "")
            pkcs12_data: bytes = pkcs12_file.read()

            private_key, certificate, ca_certs = parse_pkcs12(
                pkcs12_data, pkcs12_password or None
            )

            # Validate certificate
            cert_info: CertificateInfo = validate_certificate(certificate)

            # Check if certificate is already registered
            if UserCertificate.objects.filter(
                fingerprint_sha256=cert_info.fingerprint_sha256
            ).exists():
                form.add_error(
                    None,
                    "This certificate is already registered to another user.",
                )
                return render(request, self.template_name, {"form": form})

            # Create user
            username: str = form.cleaned_data["username"]
            email: str = form.cleaned_data.get("email", "")

            user = User.objects.create_user(
                username=username,
                email=email,
            )

            # Create certificate record
            user_cert = UserCertificate.objects.create(
                user=user,
                fingerprint_sha256=cert_info.fingerprint_sha256,
                subject_dn=cert_info.subject_dn,
                issuer_dn=cert_info.issuer_dn,
                serial_number=cert_info.serial_number,
                common_name=cert_info.common_name,
                organization=cert_info.organization,
                not_valid_before=cert_info.not_valid_before,
                not_valid_after=cert_info.not_valid_after,
                certificate_pem=cert_info.pem_data,
                is_active=True,
            )

            # Send signals
            ecp_user_registered.send(
                sender=self.__class__,
                request=request,
                user=user,
                certificate_info=cert_info,
            )
            ecp_certificate_linked.send(
                sender=self.__class__,
                user=user,
                certificate_info=cert_info,
            )

            logger.info(
                "New user registered via ECP: %s (cert CN: %s, IP: %s).",
                username,
                cert_info.common_name,
                get_client_ip(request),
            )

            return redirect("ecp_auth:login")

        except CertificateParsingError as exc:
            logger.warning("Certificate parsing failed during registration: %s", exc.message)
            form.add_error(None, f"Certificate error: {exc.message}")
            return render(request, self.template_name, {"form": form})

        except CertificateValidationError as exc:
            logger.warning("Certificate validation failed during registration: %s", exc.message)
            form.add_error(None, f"Validation error: {exc.message}")
            return render(request, self.template_name, {"form": form})

        except ECPAuthError as exc:
            logger.error("ECP error during registration: %s", exc.message)
            form.add_error(None, f"Registration error: {exc.message}")
            return render(request, self.template_name, {"form": form})


class ECPLogoutView(View):
    """View for logging out from an ECP session.

    Clears all ECP-related session data and logs the user out.

    Template: redirects to login page.
    """

    def get(self, request: HttpRequest) -> HttpResponse:
        """Handle logout via GET request.

        Args:
            request: The HTTP request.

        Returns:
            Redirect to the login page.
        """
        return self._perform_logout(request)

    def post(self, request: HttpRequest) -> HttpResponse:
        """Handle logout via POST request.

        Args:
            request: The HTTP request.

        Returns:
            Redirect to the login page.
        """
        return self._perform_logout(request)

    def _perform_logout(self, request: HttpRequest) -> HttpResponse:
        """Perform the logout operation.

        Args:
            request: The HTTP request.

        Returns:
            Redirect to the logout redirect URL.
        """
        user = request.user

        if user.is_authenticated:
            # Send signal before logout
            ecp_logout.send(
                sender=self.__class__,
                request=request,
                user=user,
            )

            # Clear ECP session data
            clear_ecp_session(request)

            # Django logout
            logout(request)

            logger.info(
                "User %s logged out (IP: %s).",
                user.username,
                get_client_ip(request),
            )

        return redirect(get_logout_redirect_url())


class ECPChallengeView(View):
    """API view for generating and verifying challenges.

    Provides a JSON API for the challenge-response protocol,
    useful for JavaScript-based authentication flows.
    """

    def post(self, request: HttpRequest) -> JsonResponse:
        """Generate a new challenge or verify a signed challenge.

        The ``action`` parameter determines the operation:
        - ``generate``: Create a new challenge nonce
        - ``verify``: Verify a signed challenge

        Args:
            request: The HTTP POST request.

        Returns:
            JSON response with challenge data or verification result.
        """
        action: str = request.POST.get("action", "generate")

        if action == "generate":
            return self._generate_challenge(request)
        elif action == "verify":
            return self._verify_challenge(request)
        else:
            return JsonResponse(
                {"error": "Invalid action."},
                status=400,
            )

    def _generate_challenge(self, request: HttpRequest) -> JsonResponse:
        """Generate a new challenge nonce.

        Args:
            request: The HTTP request.

        Returns:
            JSON with the challenge nonce.
        """
        challenge: Challenge = generate_challenge()
        store_in_session(request, "challenge", challenge.to_dict())

        return JsonResponse({
            "nonce": challenge.nonce,
            "timeout": challenge.timeout,
        })

    def _verify_challenge(self, request: HttpRequest) -> JsonResponse:
        """Verify a signed challenge.

        Args:
            request: The HTTP request with signature data.

        Returns:
            JSON with verification result.
        """
        try:
            challenge_data = get_from_session(request, "challenge")
            if not challenge_data:
                return JsonResponse(
                    {"error": "No active challenge found."},
                    status=400,
                )

            challenge = Challenge.from_dict(challenge_data)
            verify_challenge_not_expired(challenge)

            signature_b64: str = request.POST.get("signature", "")
            cert_pem: str = request.POST.get("certificate", "")

            if not signature_b64 or not cert_pem:
                return JsonResponse(
                    {"error": "Missing signature or certificate."},
                    status=400,
                )

            from ecp_auth.parsers import parse_pem_certificate

            certificate = parse_pem_certificate(cert_pem)
            signature_bytes = decode_signature_b64(signature_b64)
            verify_signature(certificate, signature_bytes, challenge.nonce_bytes)

            # Clean up challenge
            remove_from_session(request, "challenge")

            return JsonResponse({"verified": True})

        except ECPAuthError as exc:
            return JsonResponse(
                {"error": exc.message, "verified": False},
                status=400,
            )
