"""Certificate management for cluster mTLS.

Generates self-signed CA, issues node certificates, and verifies
certificate chains. Uses the cryptography library.
"""
from __future__ import annotations

import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

try:
    from cryptography import x509
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.x509.oid import NameOID

    CRYPTO_AVAILABLE = True
except ImportError:
    CRYPTO_AVAILABLE = False


class CertificateError(Exception):
    """Raised when certificate operations fail."""
    pass


def generate_ca(
    common_name: str = "GreyMatter Cluster CA",
    days_valid: int = 3650,
) -> tuple[bytes, bytes]:
    """Generate a self-signed CA certificate and private key.

    Returns (ca_cert_pem, ca_key_pem).
    """
    _require_crypto()

    key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, common_name),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "GreyMatter"),
    ])

    cert = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.now(timezone.utc))
        .not_valid_after(datetime.now(timezone.utc) + timedelta(days=days_valid))
        .add_extension(
            x509.BasicConstraints(ca=True, path_length=0), critical=True,
        )
        .add_extension(
            x509.KeyUsage(
                digital_signature=True, key_cert_sign=True, crl_sign=True,
                content_commitment=False, key_encipherment=False,
                data_encipherment=False, key_agreement=False,
                encipher_only=False, decipher_only=False,
            ),
            critical=True,
        )
        .sign(key, hashes.SHA256())
    )

    cert_pem = cert.public_bytes(serialization.Encoding.PEM)
    key_pem = key.private_bytes(
        serialization.Encoding.PEM,
        serialization.PrivateFormat.PKCS8,
        serialization.NoEncryption(),
    )
    return cert_pem, key_pem


def issue_node_cert(
    ca_cert_pem: bytes,
    ca_key_pem: bytes,
    node_id: str,
    addresses: list[str] | None = None,
    days_valid: int = 365,
) -> tuple[bytes, bytes]:
    """Issue a node certificate signed by the CA.

    Args:
        ca_cert_pem: CA certificate PEM bytes.
        ca_key_pem: CA private key PEM bytes.
        node_id: Unique node identifier (used as CN).
        addresses: IP addresses or hostnames for SAN.
        days_valid: Certificate validity period.

    Returns (node_cert_pem, node_key_pem).
    """
    _require_crypto()

    ca_cert = x509.load_pem_x509_certificate(ca_cert_pem)
    ca_key = serialization.load_pem_private_key(ca_key_pem, password=None)

    node_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    subject = x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, node_id),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "GreyMatter"),
    ])

    builder = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(ca_cert.subject)
        .public_key(node_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.now(timezone.utc))
        .not_valid_after(datetime.now(timezone.utc) + timedelta(days=days_valid))
        .add_extension(
            x509.BasicConstraints(ca=False, path_length=None), critical=True,
        )
        .add_extension(
            x509.KeyUsage(
                digital_signature=True, key_encipherment=True,
                content_commitment=False, key_cert_sign=False, crl_sign=False,
                data_encipherment=False, key_agreement=False,
                encipher_only=False, decipher_only=False,
            ),
            critical=True,
        )
        .add_extension(
            x509.ExtendedKeyUsage([
                x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
                x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
            ]),
            critical=False,
        )
    )

    # Add Subject Alternative Names
    if addresses:
        import ipaddress
        san_entries = []
        for addr in addresses:
            try:
                san_entries.append(x509.IPAddress(ipaddress.ip_address(addr)))
            except ValueError:
                san_entries.append(x509.DNSName(addr))
        if san_entries:
            builder = builder.add_extension(
                x509.SubjectAlternativeName(san_entries), critical=False,
            )

    cert = builder.sign(ca_key, hashes.SHA256())

    cert_pem = cert.public_bytes(serialization.Encoding.PEM)
    key_pem = node_key.private_bytes(
        serialization.Encoding.PEM,
        serialization.PrivateFormat.PKCS8,
        serialization.NoEncryption(),
    )
    return cert_pem, key_pem


def verify_cert(cert_pem: bytes, ca_cert_pem: bytes) -> dict[str, Any]:
    """Verify a certificate against a CA.

    Returns dict with verification results.
    """
    _require_crypto()

    try:
        cert = x509.load_pem_x509_certificate(cert_pem)
        ca_cert = x509.load_pem_x509_certificate(ca_cert_pem)

        # Check issuer matches CA subject
        issuer_match = cert.issuer == ca_cert.subject

        # Check validity period
        now = datetime.now(timezone.utc)
        not_expired = cert.not_valid_after_utc > now
        not_before_ok = cert.not_valid_before_utc <= now

        # Extract CN
        cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
        common_name = cn[0].value if cn else ""

        return {
            "valid": issuer_match and not_expired and not_before_ok,
            "common_name": common_name,
            "issuer_match": issuer_match,
            "not_expired": not_expired,
            "not_before_ok": not_before_ok,
            "not_valid_after": cert.not_valid_after_utc.isoformat(),
        }
    except Exception as e:
        return {"valid": False, "error": str(e)}


def save_cert_files(
    cert_dir: str | Path,
    ca_cert_pem: bytes,
    node_cert_pem: bytes,
    node_key_pem: bytes,
) -> dict[str, str]:
    """Save certificate files to a directory.

    Returns dict mapping file type to path.
    """
    cert_dir = Path(cert_dir)
    cert_dir.mkdir(parents=True, exist_ok=True)

    paths = {}
    for name, data in [
        ("ca.crt", ca_cert_pem),
        ("node.crt", node_cert_pem),
        ("node.key", node_key_pem),
    ]:
        path = cert_dir / name
        path.write_bytes(data)
        if name == "node.key":
            path.chmod(0o600)
        paths[name] = str(path)

    return paths


def revoke_cert(
    cert_pem: bytes,
    ca_cert_pem: bytes,
    ca_key_pem: bytes,
    revoked_store: str | Path | None = None,
) -> dict[str, Any]:
    """Revoke a node certificate by recording its serial number.

    Appends the serial number to a revoked serials file. Returns
    revocation info dict.

    Args:
        cert_pem: The certificate to revoke.
        ca_cert_pem: CA certificate (for verification).
        ca_key_pem: CA private key (not used currently but reserved for CRL signing).
        revoked_store: Path to revoked serials file. Defaults to ~/.greymatter/ca/revoked.txt.
    """
    _require_crypto()

    cert = x509.load_pem_x509_certificate(cert_pem)
    serial = cert.serial_number

    # Extract CN for logging
    cn_attrs = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
    cn = cn_attrs[0].value if cn_attrs else "unknown"

    # Write serial to revoked store
    if revoked_store is None:
        revoked_store = Path.home() / ".greymatter" / "ca" / "revoked.txt"
    store_path = Path(revoked_store)
    store_path.parent.mkdir(parents=True, exist_ok=True)

    with open(store_path, "a") as f:
        f.write(f"{serial:x}\n")

    logger.info("Certificate revoked: CN=%s serial=%x", cn, serial)

    return {
        "revoked": True,
        "common_name": cn,
        "serial_number": f"{serial:x}",
    }


def is_revoked(
    cert_pem: bytes,
    revoked_store: str | Path | None = None,
) -> bool:
    """Check if a certificate's serial number is in the revoked store."""
    _require_crypto()

    cert = x509.load_pem_x509_certificate(cert_pem)
    serial_hex = f"{cert.serial_number:x}"

    if revoked_store is None:
        revoked_store = Path.home() / ".greymatter" / "ca" / "revoked.txt"
    store_path = Path(revoked_store)

    if not store_path.exists():
        return False

    revoked_serials = store_path.read_text().strip().splitlines()
    return serial_hex in revoked_serials


def _require_crypto():
    if not CRYPTO_AVAILABLE:
        raise CertificateError(
            "cryptography library required. Install with: "
            "pip install 'greymatter-core[distributed]'"
        )
