"""
EDS (Electronic Digital Signature) Authentication Backend for Django.

Implements BaseBackend for authenticating users based on digital signatures
over challenge-response nonces.

Security Design:
1. User requests login → Server generates random nonce
2. Client signs nonce with their private key
3. Client sends: username, signed_nonce, certificate
4. Server verifies:
   - Certificate validity and trusted status
   - Signature validity over nonce
   - Nonce uniqueness (not already used)
5. On success: Nonce is invalidated, user is authenticated
"""

import logging
import secrets
from datetime import timedelta
from pathlib import Path
from typing import Any, Dict, Optional

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend
from django.http import HttpRequest
from django.utils import timezone

from .audit import log_security_event
from .models import (
    AuthenticationNonce,
    EDSCertificate,
    FailedAuthenticationAttempt,
    SignatureVerificationLog,
)
from .rate_limiting import RateLimitConfig, RateLimiter, get_client_ip
from .signature_verification import (
    CertificateValidationError,
    EDSCertificateValidator,
    EDSSignatureVerifier,
    SignatureVerificationError,
)

logger = logging.getLogger(__name__)
User = get_user_model()

# Configuration
NONCE_VALIDITY_SECONDS = 300  # 5 minutes
NONCE_LENGTH = 32


class EDSAuthBackend(BaseBackend):
    """
    Custom authentication backend for EDS-based login.

    Authenticates users based on:
    1. Valid EDS certificate associated with user
    2. Valid signature over a server-generated nonce
    3. Certificate signed by trusted CA (if configured)
    """

    def __init__(self, ca_certs_pem: Optional[str] = None):
        """
        Initialize backend.

        Args:
            ca_certs_pem: PEM-encoded CA certificates for chain validation
        """
        self._configuration_error: Optional[str] = None

        if ca_certs_pem is None:
            (
                ca_certs_pem,
                configuration_error,
            ) = self._load_trusted_ca_bundle_from_settings()
            self._configuration_error = configuration_error

        self.ca_certs_pem = ca_certs_pem
        self.validator = EDSCertificateValidator(ca_certs_pem=ca_certs_pem)

        # Initialize rate limiter with custom config
        rate_limit_config = RateLimitConfig(
            max_attempts=5,
            lockout_duration=900,  # 15 minutes
            reset_after=3600,  # 1 hour
        )
        self.rate_limiter = RateLimiter(config=rate_limit_config)

    def authenticate(
        self,
        request: Optional[HttpRequest] = None,
        username: Optional[str] = None,
        certificate_pem: Optional[str] = None,
        signed_nonce_hex: Optional[str] = None,
        nonce: Optional[str] = None,
        **kwargs,
    ) -> Optional[Any]:
        """
        Authenticate user based on EDS certificate and signed nonce.

        Args:
            request: Django request object (for audit logging)
            username: User's username
            certificate_pem: PEM-encoded X.509 certificate
            signed_nonce_hex: Hex-encoded signature of nonce
            nonce: Original nonce value that was signed

        Returns:
            Authenticated User object or None
        """
        has_eds_payload = any([certificate_pem, signed_nonce_hex, nonce])
        if not has_eds_payload:
            return None

        if not username:
            logger.warning("Missing authentication parameters")
            log_security_event(
                event_type="invalid_login",
                request=request,
                details={"reason": "Missing username for EDS authentication"},
            )
            return None

        client_ip = get_client_ip(request) if request else "unknown"

        # Check rate limit before payload validation to include malformed attempts
        is_allowed, rate_limit_info = self.rate_limiter.check_rate_limit(
            username, "eds_auth"
        )

        if not is_allowed:
            self._log_failed_attempt(
                request,
                username,
                client_ip,
                "rate_limit_locked",
                f"Account locked due to multiple failed attempts. "
                f"Retry in {rate_limit_info['remaining_seconds']}s",
                rate_limit_number=rate_limit_info["attempts"],
                is_locked=True,
            )
            logger.warning(f"Authentication attempt on rate-limited account: {username}")
            log_security_event(
                event_type="rate_limit_blocked",
                request=request,
                ip_address=client_ip,
                details={
                    "username": username,
                    "remaining_seconds": rate_limit_info["remaining_seconds"],
                    "attempt_number": rate_limit_info["attempts"],
                },
            )
            return None

        missing_fields = []
        if not certificate_pem:
            missing_fields.append("certificate_pem")
        if not signed_nonce_hex:
            missing_fields.append("signed_nonce_hex")
        if not nonce:
            missing_fields.append("nonce")

        if missing_fields:
            if "certificate_pem" in missing_fields:
                attempt_type = "certificate_missing"
            elif "signed_nonce_hex" in missing_fields:
                attempt_type = "signature_invalid"
            else:
                attempt_type = "invalid_nonce"

            self._record_failed_attempt_and_check_limit(
                request,
                username,
                client_ip,
                attempt_type,
                f"Missing required EDS fields: {', '.join(missing_fields)}",
            )
            return None

        if self._configuration_error:
            logger.error(
                "EDSAuthBackend blocked authentication due to CA configuration error: %s",
                self._configuration_error,
            )
            self._log_verification(
                request,
                None,
                "error",
                f"CA bundle configuration error: {self._configuration_error}",
            )
            return None

        try:
            # Step 1: Get user
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                # ✅ Record failed attempt
                self._record_failed_attempt_and_check_limit(
                    request, username, client_ip, "user_not_found", "User not found"
                )
                self._log_verification(request, None, "error", "User not found")
                logger.warning(f"User not found: {username}")
                return None

            # Step 2: Get user's certificate
            try:
                user_cert = EDSCertificate.objects.get(user=user)
            except EDSCertificate.DoesNotExist:
                self._record_failed_attempt_and_check_limit(
                    request,
                    username,
                    client_ip,
                    "certificate_missing",
                    "User has no certificate registered",
                )
                self._log_verification(
                    request, None, "failed_cert", "User has no certificate registered"
                )
                logger.warning(f"User has no certificate: {username}")
                return None

            # Step 3: Validate certificate (not revoked, not expired)
            if not user_cert.is_valid():
                reason = (
                    "Certificate revoked"
                    if user_cert.is_revoked
                    else "Certificate expired"
                )
                attempt_type = (
                    "certificate_revoked"
                    if user_cert.is_revoked
                    else "certificate_expired"
                )
                status = "failed_revoked" if user_cert.is_revoked else "failed_expired"

                self._record_failed_attempt_and_check_limit(
                    request,
                    username,
                    client_ip,
                    attempt_type,
                    reason,
                    certificate_serial=user_cert.serial_number,
                )
                self._log_verification(request, user_cert, status, reason)
                logger.warning(f"{reason}: {username}")
                return None

            # Step 4: Validate provided certificate matches stored certificate
            provided_thumbprint = self._extract_thumbprint(certificate_pem)
            if not provided_thumbprint:
                self._record_failed_attempt_and_check_limit(
                    request,
                    username,
                    client_ip,
                    "invalid_pem_key",
                    "Invalid certificate PEM format",
                    certificate_serial=user_cert.serial_number,
                )
                self._log_verification(
                    request,
                    user_cert,
                    "failed_invalid_format",
                    "Invalid certificate PEM format",
                )
                logger.warning(f"Invalid PEM certificate for user: {username}")
                return None

            if provided_thumbprint.lower() != user_cert.thumbprint.lower():
                self._record_failed_attempt_and_check_limit(
                    request,
                    username,
                    client_ip,
                    "certificate_mismatch",
                    "Certificate does not match stored certificate",
                    certificate_serial=user_cert.serial_number,
                )
                self._log_verification(
                    request,
                    user_cert,
                    "failed_cert",
                    "Certificate does not match stored certificate",
                )
                logger.warning(f"Certificate mismatch for user: {username}")
                return None

            # Step 5: Validate nonce
            is_nonce_valid, nonce_reason = self._validate_nonce(nonce, user)
            if not is_nonce_valid:
                self._record_failed_attempt_and_check_limit(
                    request,
                    username,
                    client_ip,
                    "invalid_nonce",
                    nonce_reason,
                    certificate_serial=user_cert.serial_number,
                )
                self._log_verification(request, user_cert, "failed_nonce", nonce_reason)
                logger.warning(
                    f"Nonce validation failed for {username}: {nonce_reason}"
                )
                return None

            # Step 6: Verify signature
            is_sig_valid, sig_reason = self._verify_signature(
                certificate_pem, nonce, signed_nonce_hex
            )

            if not is_sig_valid:
                self._record_failed_attempt_and_check_limit(
                    request,
                    username,
                    client_ip,
                    "signature_invalid",
                    sig_reason,
                    certificate_serial=user_cert.serial_number,
                )
                self._log_verification(
                    request, user_cert, "failed_signature", sig_reason
                )
                logger.warning(
                    f"Signature verification failed for {username}: {sig_reason}"
                )
                return None

            # Step 7: Invalidate nonce (prevent replay attacks)
            self._invalidate_nonce(nonce)

            # ✅ Step 8: Success! Clear rate limiting for this user
            self.rate_limiter.clear_failed_attempts(username, "eds_auth")

            # Step 9: Log successful authentication
            self._log_verification(
                request, user_cert, "success", "User authenticated via EDS"
            )
            logger.info(f"User authenticated via EDS: {username}")
            return user

        except Exception as e:
            logger.exception(f"Unexpected error during EDS authentication: {e}")
            self._log_verification(
                request, None, "error", f"Authentication error: {str(e)}"
            )
            return None

    def get_user(self, user_id: int) -> Optional[Any]:
        """Retrieve user by ID."""
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

    def _validate_nonce(self, nonce: str, user: Any) -> tuple[bool, str]:
        """
        Validate nonce for authentication attempt.

        Returns:
            Tuple of (is_valid: bool, reason: str)
        """
        try:
            nonce_obj = AuthenticationNonce.objects.get(nonce=nonce, user=user)
        except AuthenticationNonce.DoesNotExist:
            return False, "Nonce not found"

        if not nonce_obj.is_valid():
            if nonce_obj.is_used:
                return False, "Nonce already used (replay attack detected)"
            else:
                return False, "Nonce expired"

        return True, "Nonce is valid"

    def _verify_signature(
        self, certificate_pem: str, nonce: str, signed_nonce_hex: str
    ) -> tuple[bool, str]:
        """
        Verify digital signature over nonce.

        Returns:
            Tuple of (is_valid: bool, reason: str)
        """
        try:
            # Validate certificate format
            try:
                cert = self.validator.load_certificate(certificate_pem)
            except CertificateValidationError as e:
                return False, f"Invalid certificate: {e}"

            # Validate certificate itself
            is_cert_valid, cert_reason = self.validator.validate_certificate(cert)
            if not is_cert_valid:
                return False, f"Certificate validation failed: {cert_reason}"

            # Verify signature
            verifier = EDSSignatureVerifier(certificate_pem)
            is_sig_valid, sig_reason = verifier.verify_signature(
                nonce, signed_nonce_hex, algorithm="sha256"
            )

            return is_sig_valid, sig_reason

        except SignatureVerificationError as e:
            return False, str(e)
        except Exception as e:
            logger.exception(f"Error during signature verification: {e}")
            return False, f"Signature verification error: {e}"

    @staticmethod
    def _load_trusted_ca_bundle_from_settings() -> tuple[Optional[str], Optional[str]]:
        """
        Load trusted CA bundle from Django settings.

        Returns:
            Tuple: (ca_bundle_pem or None, configuration_error or None)
        """
        ca_bundle_path = getattr(settings, "EDS_AUTH_TRUSTED_CA_CERTS_PATH", None)
        if not ca_bundle_path:
            return None, None

        path = Path(ca_bundle_path)
        if not path.is_absolute():
            base_dir = getattr(settings, "BASE_DIR", None)
            if base_dir is not None:
                path = Path(base_dir) / path

        try:
            return path.read_text(encoding="utf-8"), None
        except Exception as exc:
            return None, f"cannot read EDS_AUTH_TRUSTED_CA_CERTS_PATH '{path}': {exc}"

    def _extract_thumbprint(self, certificate_pem: str) -> Optional[str]:
        """Extract SHA-256 thumbprint from PEM certificate."""
        try:
            cert = self.validator.load_certificate(certificate_pem)
            cert_info = self.validator.extract_certificate_info(cert)
            return cert_info.get("thumbprint")
        except CertificateValidationError as exc:
            logger.warning("Failed to parse client certificate for thumbprint: %s", exc)
            return None

    def _invalidate_nonce(self, nonce: str) -> None:
        """Mark nonce as used to prevent replay attacks."""
        try:
            nonce_obj = AuthenticationNonce.objects.get(nonce=nonce)
            nonce_obj.mark_as_used()
        except AuthenticationNonce.DoesNotExist:
            logger.warning(f"Attempted to invalidate non-existent nonce: {nonce}")

    def _log_verification(
        self,
        request: Optional[HttpRequest],
        certificate: Optional[EDSCertificate],
        status: str,
        message: str,
    ) -> None:
        """Log signature verification attempt for audit trail."""
        user = certificate.user if certificate and certificate.user else None

        try:
            log_data: Dict[str, Any] = {
                "status": status,
                "message": message,
                "certificate": certificate,
            }

            if request:
                log_data["ip_address"] = self._get_client_ip(request)
                log_data["user_agent"] = request.META.get("HTTP_USER_AGENT", "")[:500]

            if user:
                log_data["user"] = user

            SignatureVerificationLog.objects.create(**log_data)
        except Exception as e:
            logger.exception(f"Failed to log verification attempt: {e}")

        event_type_map = {
            "success": "auth_success",
            "error": "backend_error",
            "failed_nonce": "invalid_nonce",
            "failed_signature": "invalid_signature",
            "failed_cert": "certificate_mismatch",
            "failed_revoked": "certificate_revoked",
            "failed_expired": "certificate_expired",
            "failed_invalid_format": "invalid_pem_key",
        }
        log_security_event(
            event_type=event_type_map.get(status, "backend_error"),
            request=request,
            user=user,
            details={
                "status": status,
                "reason": message,
            },
        )

    def _record_failed_attempt_and_check_limit(
        self,
        request: Optional[HttpRequest],
        username: str,
        client_ip: str,
        attempt_type: str,
        reason: str,
        certificate_serial: str = "",
    ) -> Dict[str, Any]:
        """
        Record failed authentication attempt and update rate limit.

        Args:
            request: Django request
            username: Username attempted
            client_ip: Client IP address
            attempt_type: Type of failure (certificate_mismatch, signature_invalid, etc.)
            reason: Reason for failure
            certificate_serial: Certificate serial number (if available)

        Returns:
            Rate limit status dict
        """
        # Record the failed attempt
        rate_limit_result = self.rate_limiter.record_failed_attempt(
            username, "eds_auth"
        )

        attempt_number = rate_limit_result["attempts"]
        is_now_locked = rate_limit_result["locked"]

        # Log to database
        self._log_failed_attempt(
            request,
            username,
            client_ip,
            attempt_type,
            reason,
            rate_limit_number=attempt_number,
            is_locked=is_now_locked,
            certificate_serial=certificate_serial,
        )

        return rate_limit_result

    def _log_failed_attempt(
        self,
        request: Optional[HttpRequest],
        username: str,
        client_ip: str,
        attempt_type: str,
        reason: str,
        rate_limit_number: int = 0,
        is_locked: bool = False,
        certificate_serial: str = "",
    ) -> None:
        """
        Log failed authentication attempt to database.

        Args:
            request: Django request
            username: Username attempted
            client_ip: Client IP address
            attempt_type: Type of failure
            reason: Reason string
            rate_limit_number: Attempt number in current cycle
            is_locked: Whether account is now locked
            certificate_serial: Certificate serial number if available
        """
        try:
            user_agent = (
                request.META.get("HTTP_USER_AGENT", "")[:500] if request else ""
            )

            FailedAuthenticationAttempt.objects.create(
                username=username,
                ip_address=client_ip,
                attempt_type=attempt_type,
                reason=reason,
                attempt_number=rate_limit_number,
                is_locked=is_locked,
                user_agent=user_agent,
                certificate_serial=certificate_serial,
            )

            if is_locked:
                logger.error(
                    f"🔒 ACCOUNT LOCKED: {username} from {client_ip} "
                    f"(attempt #{rate_limit_number}, reason: {attempt_type})"
                )
        except Exception as e:
            logger.exception(f"Failed to log failed attempt: {e}")

        event_type_map = {
            "user_not_found": "invalid_login",
            "certificate_missing": "certificate_missing",
            "invalid_pem_key": "invalid_pem_key",
            "certificate_mismatch": "certificate_mismatch",
            "invalid_nonce": "invalid_nonce",
            "certificate_revoked": "certificate_revoked",
            "certificate_expired": "certificate_expired",
            "signature_invalid": "invalid_signature",
            "rate_limit_locked": "rate_limit_locked",
        }

        mapped_event_type = event_type_map.get(attempt_type, "backend_error")

        log_security_event(
            event_type=mapped_event_type,
            request=request,
            ip_address=client_ip,
            details={
                "username": username,
                "attempt_type": attempt_type,
                "reason": reason,
                "attempt_number": rate_limit_number,
                "is_locked": is_locked,
            },
        )

        if not is_locked and rate_limit_number > 0:
            log_security_event(
                event_type="rate_limit_attempt",
                request=request,
                ip_address=client_ip,
                details={
                    "username": username,
                    "attempt_number": rate_limit_number,
                    "max_attempts": self.rate_limiter.config.max_attempts,
                },
            )

    @staticmethod
    def _get_client_ip(request: HttpRequest) -> str:
        """Extract client IP from request."""
        x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
        if x_forwarded_for:
            return x_forwarded_for.split(",")[0].strip()
        return request.META.get("REMOTE_ADDR", "")


class NonceManager:
    """
    Generates and manages authentication nonces.

    Each login attempt should:
    1. Request a nonce via get_nonce()
    2. Client signs the nonce with their private key
    3. Nonce is used in authenticate() call
    4. Nonce is invalidated after authentication attempt
    """

    @staticmethod
    def generate_nonce(user: Any) -> str:
        """
        Generate a new authentication nonce for user.

        Args:
            user: User object

        Returns:
            Nonce string
        """
        nonce_value = secrets.token_urlsafe(NONCE_LENGTH)
        expires_at = timezone.now() + timedelta(seconds=NONCE_VALIDITY_SECONDS)

        AuthenticationNonce.objects.create(
            user=user, nonce=nonce_value, expires_at=expires_at
        )

        logger.debug(f"Generated nonce for user {user.username}")
        return nonce_value

    @staticmethod
    def get_nonce(nonce_value: str) -> Optional[AuthenticationNonce]:
        """Retrieve nonce object by value."""
        try:
            return AuthenticationNonce.objects.get(nonce=nonce_value)
        except AuthenticationNonce.DoesNotExist:
            return None

    @staticmethod
    def cleanup_expired_nonces() -> int:
        """
        Delete expired nonces from database.

        Should be run periodically via management command or Celery task.

        Returns:
            Number of nonces deleted
        """
        expired = AuthenticationNonce.objects.filter(
            expires_at__lt=timezone.now(), is_used=False
        )
        count, _ = expired.delete()
        logger.info(f"Cleaned up {count} expired nonces")
        return count
