"""Certificate management module for mTLS certificate rotation.

This module provides the CertificateManager class for managing mTLS certificates
with support for:
- Certificate expiry monitoring and warnings
- Hot-reload of certificates without SDK restart
- Security validation (file permissions, chain validation)
- Observer pattern for notifying transport layers
- Fallback to previous valid certificate on errors
- Internal telemetry emission for observability

SECURITY LIMITATIONS:
- Certificate Revocation: This implementation does NOT check CRL (Certificate
  Revocation Lists) or OCSP (Online Certificate Status Protocol). If a certificate
  is compromised and revoked by the CA, this SDK will continue using it until
  natural expiry. For production deployments requiring revocation checking,
  implement additional validation or use a proxy/gateway that performs OCSP
  stapling.
"""

import logging
import os
import ssl
import stat
import threading
from datetime import datetime, timedelta
from typing import Callable, List, Optional

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization

try:
    from opentelemetry.metrics import get_meter

    _OTEL_AVAILABLE = True
except ImportError:
    _OTEL_AVAILABLE = False

from ..utils.exceptions import (
    CertificateExpiredError,
    CertificateLoadError,
    CertificateNotLoadedError,
    CertificateValidationError,
    ConfigurationError,
    SecurityError,
)

logger = logging.getLogger(__name__)


class CertificateManager:
    """Manages mTLS certificates with rotation support and hot-reload capability.

    This class provides:
    - Certificate expiry monitoring
    - Automatic file polling for certificate updates
    - Observer pattern for notifying transport layers
    - Security validation (file permissions, chain validation)
    - Fallback to previous valid certificate on errors
    - Internal telemetry emission

    Example:
        >>> manager = CertificateManager(
        ...     cert_path="/etc/certs/client.pem",
        ...     key_path="/etc/certs/client-key.pem",
        ...     enforce_key_permissions=True
        ... )
        >>> manager.check_expiry()
        >>> manager.start_monitoring()
        >>> manager.register_observer(on_certificate_reload)
    """

    EXPIRY_WARNING_DAYS = 30
    DEFAULT_POLL_INTERVAL = 60  # 1 minute (more responsive for critical security component)

    def __init__(
        self,
        cert_path: str,
        key_path: str,
        ca_bundle_path: Optional[str] = None,
        poll_interval: int = DEFAULT_POLL_INTERVAL,
        enforce_key_permissions: bool = True,
        key_password: Optional[bytes] = None,
    ):
        """Initialize certificate manager.

        Args:
            cert_path: Path to PEM-encoded certificate file
            key_path: Path to PEM-encoded private key file
            ca_bundle_path: Optional path to CA bundle for chain validation
            poll_interval: Seconds between file modification checks (default: 300)
            enforce_key_permissions: Fail on insecure key permissions (default: True)
            key_password: Optional password for encrypted private key (as bytes)

        Raises:
            ConfigurationError: If key file not found or has insecure permissions
            CertificateLoadError: If certificate cannot be loaded or is expired
        """
        self._cert_path = cert_path
        self._key_path = key_path
        self._ca_bundle_path = ca_bundle_path
        self._poll_interval = poll_interval
        self._enforce_key_permissions = enforce_key_permissions
        self._key_password = key_password

        # State management
        self._cert = None
        self._key = None
        self._ssl_context = None
        self._observers: List[Callable[[ssl.SSLContext], None]] = []
        self._running = False
        self._monitor_thread = None
        self._stop_event = threading.Event()
        self._lock = threading.Lock()

        # Initialize telemetry instruments
        self._setup_telemetry()

        # Initial load with validation
        self._load_certificate()

        # Fail fast if certificate is expired at startup
        try:
            self.check_expiry()
        except CertificateExpiredError as e:
            raise CertificateLoadError(f"Cannot initialize with expired certificate: {e}")

    def _validate_file_permissions(self, fd: int) -> None:
        """Validate private key file has secure permissions (HIPAA requirement).

        Uses file descriptor to avoid TOCTOU vulnerability.

        Args:
            fd: Open file descriptor for the key file

        Raises:
            SecurityError: If key file has insecure permissions and enforcement enabled
        """
        try:
            key_stat = os.fstat(fd)
            permissions = stat.filemode(key_stat.st_mode)

            # Check if world-readable or group-readable
            if key_stat.st_mode & (stat.S_IRWXG | stat.S_IRWXO):
                msg = (
                    f"Private key file {self._key_path} has insecure permissions: {permissions}. "
                    f"Recommended: 0600 (owner read/write only)"
                )
                if self._enforce_key_permissions:
                    raise SecurityError(msg)
                else:
                    logger.warning(msg)
        except Exception as e:
            if isinstance(e, SecurityError):
                raise
            raise ConfigurationError(f"Failed to validate key file permissions: {e}")

    def _load_certificate(self) -> None:
        """Load certificate and key from disk with validation.

        Raises:
            CertificateLoadError: If certificate/key cannot be loaded or validated
            ConfigurationError: If key file not found
            SecurityError: If key file has insecure permissions
        """
        try:
            # Load certificate
            with open(self._cert_path, "rb") as f:
                cert_data = f.read()
                self._cert = x509.load_pem_x509_certificate(cert_data, default_backend())

            # Load private key with permission validation (TOCTOU-safe)
            # Open file first, validate permissions on descriptor, then read
            try:
                key_fd = os.open(self._key_path, os.O_RDONLY)
            except FileNotFoundError:
                raise ConfigurationError(f"Private key file not found: {self._key_path}")

            try:
                # Validate permissions on open file descriptor (no TOCTOU)
                self._validate_file_permissions(key_fd)

                # Read key from file descriptor
                with os.fdopen(key_fd, "rb") as f:
                    key_data = f.read()
                    self._key = serialization.load_pem_private_key(
                        key_data, password=self._key_password, backend=default_backend()
                    )
            except Exception:
                # Ensure fd is closed on error
                try:
                    os.close(key_fd)
                except OSError:
                    pass  # Already closed or invalid fd
                raise

            # Validate key matches certificate
            self._validate_key_cert_match()

            # Validate certificate chain if CA bundle provided
            if self._ca_bundle_path:
                self._validate_certificate_chain()

            # Generate new SSL context
            self._ssl_context = self._create_ssl_context()

            # Log certificate info (NEVER log actual cert contents)
            serial = self._cert.serial_number
            fingerprint = self._cert.fingerprint(hashes.SHA256()).hex()[:16]
            logger.info(
                f"Certificate loaded successfully. "
                f"Serial: {serial}, Fingerprint: {fingerprint}..., "
                f"Expires: {self._cert.not_valid_after_utc}"
            )

        except Exception as e:
            raise CertificateLoadError(f"Failed to load certificate: {e}")

    def _validate_key_cert_match(self) -> None:
        """Verify private key matches public certificate.

        Raises:
            CertificateValidationError: If key/cert mismatch detected
        """
        # Compare public key from cert with public key from private key
        cert_public_key = self._cert.public_key()
        key_public_key = self._key.public_key()

        cert_public_bytes = cert_public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        key_public_bytes = key_public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )

        if cert_public_bytes != key_public_bytes:
            raise CertificateValidationError("Private key does not match certificate public key")

    def _validate_certificate_chain(self) -> None:
        """Validate certificate against CA bundle.

        NOTE: This validation checks chain of trust but does NOT perform
        revocation checking (CRL/OCSP). See module docstring for details.

        Raises:
            CertificateValidationError: If chain validation fails
        """
        # Implementation: Use ssl.SSLContext to validate chain
        # This prevents loading invalid certs that collector will reject
        try:
            context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            context.load_verify_locations(self._ca_bundle_path)
            context.load_cert_chain(self._cert_path, self._key_path)
        except Exception as e:
            raise CertificateValidationError(f"Certificate chain validation failed: {e}")

    def _create_ssl_context(self) -> ssl.SSLContext:
        """Create SSLContext for use by OTLP exporters.

        Returns:
            Configured SSLContext ready for gRPC/HTTP transport
        """
        context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        context.load_cert_chain(self._cert_path, self._key_path)

        if self._ca_bundle_path:
            context.load_verify_locations(self._ca_bundle_path)

        return context

    def check_expiry(self) -> Optional[timedelta]:
        """Check certificate expiry and emit telemetry.

        Returns:
            Time until expiry, or None if no certificate loaded

        Raises:
            CertificateExpiredError: If certificate has expired
        """
        with self._lock:
            if not self._cert:
                return None

            expiry = self._cert.not_valid_after_utc
            now = datetime.now(expiry.tzinfo)

            # Check for clock skew
            not_before = self._cert.not_valid_before_utc
            if now < not_before:
                logger.warning(
                    f"Certificate not yet valid. System clock may be incorrect. "
                    f"Not valid before: {not_before}, Current time: {now}"
                )

            if now >= expiry:
                raise CertificateExpiredError(
                    f"Certificate expired on {expiry}. "
                    f"Follow rotation procedure: docs/certificate-rotation-procedure.md"
                )

            time_until_expiry = expiry - now

            if time_until_expiry.days <= self.EXPIRY_WARNING_DAYS:
                logger.warning(
                    f"Certificate expiring in {time_until_expiry.days} days on {expiry}. "
                    "Rotation recommended."
                )

            # Emit internal telemetry (AC: Observability)
            self._emit_metric("mca.sdk.certificate.days_remaining", time_until_expiry.days)

            return time_until_expiry

    def reload(self) -> bool:
        """Reload certificate from disk with fallback protection.

        If new certificate is corrupt/invalid, SDK continues using previous
        valid certificate and logs critical error (AC: #5).

        Returns:
            True if certificate was reloaded successfully
        """
        with self._lock:
            try:
                # Cache previous state
                old_cert = self._cert
                old_key = self._key
                old_context = self._ssl_context
                old_expiry = self._cert.not_valid_after_utc if self._cert else None

                # Attempt to load new certificate
                self._load_certificate()
                new_expiry = self._cert.not_valid_after_utc

                if old_expiry != new_expiry:
                    logger.info(f"Certificate reloaded successfully. " f"New expiry: {new_expiry}")

                    # Notify all observers (transport layers)
                    self._notify_observers()

                    # Emit telemetry
                    self._emit_event("certificate_reload_success")

                    return True

                return False

            except Exception as e:
                # FALLBACK: Restore previous valid certificate (AC: #5)
                logger.critical(
                    f"Failed to reload certificate: {e}. "
                    f"Continuing with previous valid certificate."
                )
                self._cert = old_cert
                self._key = old_key
                self._ssl_context = old_context

                # Emit telemetry
                self._emit_event("certificate_reload_failure", {"error": str(e)})

                return False

    def start_monitoring(self) -> None:
        """Start background thread to monitor certificate file for changes.

        Polls file modification time every poll_interval seconds.
        Supports atomic updates (symlink swaps, K8s cert-manager pattern).
        """
        if self._running:
            logger.warning("Certificate monitoring already running")
            return

        self._running = True
        self._monitor_thread = threading.Thread(
            target=self._monitor_loop, daemon=True, name="CertificateMonitor"
        )
        self._monitor_thread.start()
        logger.info(f"Certificate monitoring started. Poll interval: {self._poll_interval}s")

    def stop_monitoring(self) -> None:
        """Stop background monitoring thread."""
        self._running = False
        self._stop_event.set()  # Wake up sleeping thread immediately
        if self._monitor_thread:
            self._monitor_thread.join(timeout=5)
        logger.info("Certificate monitoring stopped")

    def _monitor_loop(self) -> None:
        """Background loop that polls for file changes and certificate expiry.

        Checks both file modification (for rotation) and certificate expiry
        (for proactive warnings and metrics updates).
        """
        last_mtime = os.path.getmtime(self._cert_path)
        consecutive_errors = 0
        max_consecutive_errors = 5

        while self._running:
            try:
                # Wait for poll interval or stop event (whichever comes first)
                if self._stop_event.wait(timeout=self._poll_interval):
                    # Stop event was set, exit immediately
                    break

                if not self._running:
                    break

                # Check for file modification (certificate rotation)
                current_mtime = os.path.getmtime(self._cert_path)
                if current_mtime != last_mtime:
                    logger.info("Certificate file modification detected")
                    self.reload()
                    last_mtime = current_mtime
                    consecutive_errors = 0  # Reset error counter on success

                # Check certificate expiry (updates metrics and warnings)
                # This ensures days_remaining metric updates and expiry warnings are logged
                try:
                    self.check_expiry()
                except CertificateExpiredError as e:
                    # Certificate expired - log critical error but keep monitoring
                    # (operator may rotate while process running)
                    logger.critical(f"Certificate expired: {e}")

            except Exception as e:
                consecutive_errors += 1
                if consecutive_errors <= max_consecutive_errors:
                    logger.error(
                        f"Error in certificate monitoring loop (attempt {consecutive_errors}/{max_consecutive_errors}): {e}"
                    )
                elif consecutive_errors == max_consecutive_errors + 1:
                    logger.error(
                        f"Certificate monitoring encountered {max_consecutive_errors} consecutive errors. "
                        "Silencing further errors (monitoring continues)."
                    )
                # Continue monitoring but stop spamming logs after threshold

    def register_observer(self, callback: Callable[[ssl.SSLContext], None]) -> None:
        """Register callback to be invoked when certificate is reloaded.

        Args:
            callback: Function accepting SSLContext, called on successful reload
        """
        self._observers.append(callback)

    def _notify_observers(self) -> None:
        """Notify all registered observers of certificate update.

        Used to signal OTLP exporters to reset connections with new SSL context.
        """
        if not self._ssl_context:
            logger.warning("No SSL context available to notify observers")
            return

        for callback in self._observers:
            try:
                callback(self._ssl_context)
            except Exception as e:
                logger.error(
                    f"Error notifying observer of certificate rotation: {e}", exc_info=True
                )

    def get_ssl_context(self) -> ssl.SSLContext:
        """Get current SSL context for use by OTLP exporters.

        Returns:
            Current SSLContext, ready for gRPC/HTTP transport layer

        Raises:
            CertificateNotLoadedError: If no certificate loaded
        """
        if not self._ssl_context:
            raise CertificateNotLoadedError("No certificate loaded")
        return self._ssl_context

    def _setup_telemetry(self) -> None:
        """Initialize OpenTelemetry instruments for certificate monitoring."""
        if _OTEL_AVAILABLE:
            try:
                meter = get_meter("mca_sdk.security.certificates")

                # Create gauge for days remaining
                self._days_remaining_gauge = meter.create_gauge(
                    name="mca.sdk.certificate.days_remaining",
                    description="Days until certificate expires",
                    unit="days",
                )

                # Create counter for reload events
                self._reload_counter = meter.create_counter(
                    name="mca.sdk.certificate.reload_total",
                    description="Total certificate reload attempts",
                    unit="1",
                )

                self._telemetry_enabled = True
            except Exception as e:
                logger.warning(f"Failed to initialize certificate telemetry: {e}")
                self._telemetry_enabled = False
        else:
            self._telemetry_enabled = False
            logger.debug("OpenTelemetry not available, certificate telemetry disabled")

    def _emit_metric(self, name: str, value: float) -> None:
        """Emit internal SDK metric for certificate health monitoring.

        Args:
            name: Metric name (e.g., "mca.sdk.certificate.days_remaining")
            value: Metric value
        """
        if not self._telemetry_enabled:
            return

        try:
            if "days_remaining" in name:
                self._days_remaining_gauge.set(value, {"cert_path": self._cert_path})
        except Exception as e:
            logger.debug(f"Failed to emit metric {name}: {e}")

    def _emit_event(self, event: str, attributes: dict = None) -> None:
        """Emit internal SDK event for certificate lifecycle tracking.

        Args:
            event: Event name (e.g., "certificate_reload_success")
            attributes: Optional event attributes
        """
        if not self._telemetry_enabled:
            return

        try:
            event_attributes = {"event": event, "cert_path": self._cert_path}
            if attributes:
                event_attributes.update(attributes)

            self._reload_counter.add(1, event_attributes)
        except Exception as e:
            logger.debug(f"Failed to emit event {event}: {e}")
