"""Self-signed TLS for LAN HTTPS.

Companies on a trusted LAN often want HTTPS without running an internal CA. We
generate a self-signed certificate whose SAN lists the server's hostname and
every local IPv4, so a client connecting by IP or name passes hostname
verification. The desktop client trusts it via trust-on-first-use (it pins the
certificate the first time, like SSH known_hosts).

For a public deployment, terminate TLS at a reverse proxy instead (see README).
"""
from __future__ import annotations

import datetime as _dt
import hashlib
import ipaddress
import socket
from pathlib import Path

from loguru import logger


def _local_ipv4s() -> list[str]:
    ips: list[str] = []
    try:
        for info in socket.getaddrinfo(socket.gethostname(), None,
                                       socket.AF_INET):
            ip = info[4][0]
            if not ip.startswith("127.") and ip not in ips:
                ips.append(ip)
    except OSError:
        pass
    return ips


def cert_fingerprint_sha256(cert_pem: bytes) -> str:
    """Lowercase hex SHA-256 of the certificate DER (what the client pins and
    shows the operator). Accepts PEM bytes."""
    from cryptography import x509
    from cryptography.hazmat.primitives.serialization import Encoding
    cert = x509.load_pem_x509_certificate(cert_pem)
    return hashlib.sha256(cert.public_bytes(Encoding.DER)).hexdigest()


def generate_self_signed(cert_path: Path, key_path: Path, *,
                         now: _dt.datetime, valid_days: int = 3650) -> str:
    """Write a self-signed cert+key (PEM) and return the SHA-256 fingerprint.

    `now` is injected (the workflow/runtime forbids implicit clock reads in
    some contexts, and it makes the notBefore deterministic in tests)."""
    from cryptography import x509
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.x509.oid import NameOID

    hostname = socket.gethostname()
    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, hostname),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "VoxCast Server"),
    ])

    # SAN: hostname + localhost + every local IPv4. A client connecting by IP
    # or name then passes hostname verification against the pinned cert.
    sans: list[x509.GeneralName] = [
        x509.DNSName(hostname), x509.DNSName("localhost"),
        x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
    ]
    for ip in _local_ipv4s():
        try:
            sans.append(x509.IPAddress(ipaddress.IPv4Address(ip)))
        except ValueError:
            pass

    cert = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(now - _dt.timedelta(minutes=5))
        .not_valid_after(now + _dt.timedelta(days=valid_days))
        .add_extension(x509.SubjectAlternativeName(sans), critical=False)
        .add_extension(x509.BasicConstraints(ca=False, path_length=None),
                       critical=True)
        .sign(key, hashes.SHA256())
    )

    cert_pem = cert.public_bytes(serialization.Encoding.PEM)
    key_pem = key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.NoEncryption(),
    )
    cert_path.parent.mkdir(parents=True, exist_ok=True)
    cert_path.write_bytes(cert_pem)
    key_path.write_bytes(key_pem)
    try:
        import os
        if os.name == "posix":
            os.chmod(key_path, 0o600)
    except OSError:
        pass
    fp = cert_fingerprint_sha256(cert_pem)
    logger.info("Generated self-signed TLS certificate (SHA-256 {})", fp)
    return fp


def ensure_cert(cert_path: Path, key_path: Path, *,
                now: _dt.datetime) -> str:
    """Return the fingerprint of an existing cert, or generate one. Regenerates
    if either file is missing, unreadable, OR the key does not load and pair
    with the cert, so a half-written pair (new cert + stale key from a crash
    between the two writes) self-heals instead of failing uvicorn's TLS start."""
    if cert_path.exists() and key_path.exists():
        try:
            fp = cert_fingerprint_sha256(cert_path.read_bytes())
            # Load the pair the way uvicorn will, so a mismatch is caught here
            # (and repaired) rather than surfacing as an opaque SSL error.
            import ssl
            ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
            ctx.load_cert_chain(str(cert_path), str(key_path))
            return fp
        except Exception as e:  # noqa: BLE001 (corrupt/mismatched -> regenerate)
            logger.warning("Existing certificate/key unusable ({}); "
                           "regenerating", e)
    return generate_self_signed(cert_path, key_path, now=now)
