"""Certificate parsing utilities for ECP Auth.

Provides functions to parse PKCS#12 containers and X.509 certificates,
extract subject information, validity dates, and key usage extensions.

Uses the ``cryptography`` library for PKCS#12 and X.509 parsing,
and ``asn1crypto`` for low-level ASN.1 structure inspection.
"""

from __future__ import annotations

import hashlib
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any

from asn1crypto import x509 as asn1_x509
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.x509.oid import NameOID

from ecp_auth.exceptions import CertificateParsingError

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


@dataclass(frozen=True)
class CertificateInfo:
    """Parsed certificate information.

    Attributes:
        subject_dn: The certificate subject distinguished name as a string.
        issuer_dn: The certificate issuer distinguished name as a string.
        serial_number: Certificate serial number as a hex string.
        not_valid_before: Start of the certificate's validity period.
        not_valid_after: End of the certificate's validity period.
        fingerprint_sha256: SHA-256 fingerprint of the DER-encoded certificate.
        common_name: Subject common name (CN), if present.
        organization: Subject organization (O), if present.
        organizational_unit: Subject organizational unit (OU), if present.
        email: Subject email address, if present.
        key_usage: Set of key usage flags from the KeyUsage extension.
        extended_key_usage: Set of extended key usage OIDs.
        is_self_signed: Whether the certificate is self-signed.
        pem_data: PEM-encoded certificate data.
        der_data: DER-encoded certificate data.
    """

    subject_dn: str
    issuer_dn: str
    serial_number: str
    not_valid_before: datetime
    not_valid_after: datetime
    fingerprint_sha256: str
    common_name: str = ""
    organization: str = ""
    organizational_unit: str = ""
    email: str = ""
    key_usage: set[str] = field(default_factory=set)
    extended_key_usage: set[str] = field(default_factory=set)
    is_self_signed: bool = False
    pem_data: str = ""
    der_data: bytes = b""


def parse_pkcs12(
    pkcs12_data: bytes,
    password: str | None = None,
) -> tuple[Any, x509.Certificate, list[x509.Certificate]]:
    """Parse a PKCS#12 file and extract the private key, certificate, and CA chain.

    Args:
        pkcs12_data: Raw bytes of the PKCS#12 (.p12/.pfx) file.
        password: Password to decrypt the PKCS#12 container.
            Use ``None`` for unprotected files.

    Returns:
        A tuple of (private_key, certificate, ca_certificates).
        ``ca_certificates`` may be an empty list if no CA certs are present.

    Raises:
        CertificateParsingError: If the PKCS#12 file cannot be parsed
            or does not contain the required components.

    Example:
        >>> with open("user.p12", "rb") as f:
        ...     key, cert, cas = parse_pkcs12(f.read(), "mypassword")
    """
    try:
        password_bytes: bytes | None = (
            password.encode("utf-8") if password else None
        )

        private_key, certificate, additional_certs = pkcs12.load_key_and_certificates(
            pkcs12_data, password_bytes
        )

        if certificate is None:
            raise CertificateParsingError(
                "PKCS#12 file does not contain a certificate."
            )

        if private_key is None:
            raise CertificateParsingError(
                "PKCS#12 file does not contain a private key."
            )

        ca_certs: list[x509.Certificate] = (
            list(additional_certs) if additional_certs else []
        )

        logger.info(
            "Successfully parsed PKCS#12 file. Subject: %s",
            certificate.subject.rfc4514_string(),
        )

        return private_key, certificate, ca_certs

    except CertificateParsingError:
        raise
    except Exception as exc:
        logger.error("Failed to parse PKCS#12 file: %s", exc)
        raise CertificateParsingError(
            f"Failed to parse PKCS#12 file: {exc}"
        ) from exc


def parse_pem_certificate(pem_data: bytes | str) -> x509.Certificate:
    """Parse a PEM-encoded X.509 certificate.

    Args:
        pem_data: PEM-encoded certificate data as bytes or string.

    Returns:
        Parsed X.509 certificate object.

    Raises:
        CertificateParsingError: If the PEM data is invalid.
    """
    try:
        if isinstance(pem_data, str):
            pem_data = pem_data.encode("utf-8")

        certificate: x509.Certificate = x509.load_pem_x509_certificate(pem_data)
        return certificate

    except Exception as exc:
        logger.error("Failed to parse PEM certificate: %s", exc)
        raise CertificateParsingError(
            f"Failed to parse PEM certificate: {exc}"
        ) from exc


def parse_der_certificate(der_data: bytes) -> x509.Certificate:
    """Parse a DER-encoded X.509 certificate.

    Args:
        der_data: DER-encoded certificate data.

    Returns:
        Parsed X.509 certificate object.

    Raises:
        CertificateParsingError: If the DER data is invalid.
    """
    try:
        certificate: x509.Certificate = x509.load_der_x509_certificate(der_data)
        return certificate

    except Exception as exc:
        logger.error("Failed to parse DER certificate: %s", exc)
        raise CertificateParsingError(
            f"Failed to parse DER certificate: {exc}"
        ) from exc


def _extract_name_attribute(name: x509.Name, oid: x509.ObjectIdentifier) -> str:
    """Extract a single attribute value from an X.509 Name.

    Args:
        name: The X.509 Name to extract from.
        oid: The OID of the attribute to extract.

    Returns:
        The attribute value as a string, or empty string if not found.
    """
    try:
        attrs = name.get_attributes_for_oid(oid)
        if attrs:
            return str(attrs[0].value)
    except Exception:
        pass
    return ""


def _extract_key_usage(certificate: x509.Certificate) -> set[str]:
    """Extract key usage flags from the certificate's KeyUsage extension.

    Args:
        certificate: The X.509 certificate to inspect.

    Returns:
        Set of key usage flag names.
    """
    usage_flags: set[str] = set()
    try:
        key_usage_ext = certificate.extensions.get_extension_for_class(x509.KeyUsage)
        ku = key_usage_ext.value

        usage_mapping: dict[str, bool] = {
            "digital_signature": ku.digital_signature,
            "content_commitment": ku.content_commitment,
            "key_encipherment": ku.key_encipherment,
            "data_encipherment": ku.data_encipherment,
            "key_agreement": ku.key_agreement,
            "key_cert_sign": ku.key_cert_sign,
            "crl_sign": ku.crl_sign,
        }

        for flag_name, flag_value in usage_mapping.items():
            if flag_value:
                usage_flags.add(flag_name)

    except x509.ExtensionNotFound:
        logger.debug("KeyUsage extension not found in certificate.")

    return usage_flags


def _extract_extended_key_usage(certificate: x509.Certificate) -> set[str]:
    """Extract extended key usage OIDs from the certificate.

    Args:
        certificate: The X.509 certificate to inspect.

    Returns:
        Set of extended key usage OID dotted strings.
    """
    eku_oids: set[str] = set()
    try:
        eku_ext = certificate.extensions.get_extension_for_class(
            x509.ExtendedKeyUsage
        )
        for usage in eku_ext.value:
            eku_oids.add(usage.dotted_string)

    except x509.ExtensionNotFound:
        logger.debug("ExtendedKeyUsage extension not found in certificate.")

    return eku_oids


def extract_certificate_info(certificate: x509.Certificate) -> CertificateInfo:
    """Extract detailed information from an X.509 certificate.

    Parses the certificate and returns a structured ``CertificateInfo``
    dataclass containing all relevant fields.

    Args:
        certificate: The X.509 certificate to extract info from.

    Returns:
        A ``CertificateInfo`` instance with certificate details.

    Raises:
        CertificateParsingError: If critical fields cannot be extracted.

    Example:
        >>> info = extract_certificate_info(cert)
        >>> print(info.common_name, info.fingerprint_sha256)
    """
    try:
        # Basic fields
        subject_dn: str = certificate.subject.rfc4514_string()
        issuer_dn: str = certificate.issuer.rfc4514_string()
        serial_number: str = format(certificate.serial_number, "x")

        # Validity dates — ensure timezone-aware
        not_valid_before: datetime = certificate.not_valid_before_utc
        not_valid_after: datetime = certificate.not_valid_after_utc

        # DER and PEM encoding
        der_data: bytes = certificate.public_bytes(serialization.Encoding.DER)
        pem_data: str = certificate.public_bytes(
            serialization.Encoding.PEM
        ).decode("utf-8")

        # Fingerprint
        fingerprint: str = hashlib.sha256(der_data).hexdigest()

        # Subject attributes
        common_name: str = _extract_name_attribute(
            certificate.subject, NameOID.COMMON_NAME
        )
        organization: str = _extract_name_attribute(
            certificate.subject, NameOID.ORGANIZATION_NAME
        )
        organizational_unit: str = _extract_name_attribute(
            certificate.subject, NameOID.ORGANIZATIONAL_UNIT_NAME
        )
        email: str = _extract_name_attribute(
            certificate.subject, NameOID.EMAIL_ADDRESS
        )

        # Key usage
        key_usage: set[str] = _extract_key_usage(certificate)
        extended_key_usage: set[str] = _extract_extended_key_usage(certificate)

        # Self-signed check
        is_self_signed: bool = certificate.issuer == certificate.subject

        return CertificateInfo(
            subject_dn=subject_dn,
            issuer_dn=issuer_dn,
            serial_number=serial_number,
            not_valid_before=not_valid_before,
            not_valid_after=not_valid_after,
            fingerprint_sha256=fingerprint,
            common_name=common_name,
            organization=organization,
            organizational_unit=organizational_unit,
            email=email,
            key_usage=key_usage,
            extended_key_usage=extended_key_usage,
            is_self_signed=is_self_signed,
            pem_data=pem_data,
            der_data=der_data,
        )

    except CertificateParsingError:
        raise
    except Exception as exc:
        logger.error("Failed to extract certificate info: %s", exc)
        raise CertificateParsingError(
            f"Failed to extract certificate information: {exc}"
        ) from exc


def get_asn1_certificate_info(der_data: bytes) -> dict[str, Any]:
    """Extract certificate information using ASN.1 parsing via asn1crypto.

    Provides lower-level access to certificate fields using ASN.1
    structure parsing. Useful for inspecting non-standard extensions.

    Args:
        der_data: DER-encoded certificate data.

    Returns:
        Dictionary containing ASN.1-parsed certificate fields.

    Raises:
        CertificateParsingError: If ASN.1 parsing fails.
    """
    try:
        cert = asn1_x509.Certificate.load(der_data)
        tbs = cert["tbs_certificate"]

        return {
            "version": tbs["version"].native,
            "serial_number": tbs["serial_number"].native,
            "signature_algorithm": tbs["signature"]["algorithm"].native,
            "issuer": tbs["issuer"].human_friendly,
            "subject": tbs["subject"].human_friendly,
            "not_before": tbs["validity"]["not_before"].native,
            "not_after": tbs["validity"]["not_after"].native,
            "public_key_algorithm": (
                tbs["subject_public_key_info"]["algorithm"]["algorithm"].native
            ),
            "public_key_bit_size": tbs["subject_public_key_info"].bit_size,
        }

    except Exception as exc:
        logger.error("ASN.1 certificate parsing failed: %s", exc)
        raise CertificateParsingError(
            f"ASN.1 certificate parsing failed: {exc}"
        ) from exc
