"""Federation — node identity, signed feeds, peer registry.

Identity layer for internet-wide federation of psyc nodes. Each node owns
an Ed25519 keypair persisted under DATA_DIR/federation/. The public key
fingerprint (first 16 bytes of SHA256(raw_pubkey) hex-encoded) goes into a
DNS TXT record so peers can discover and authenticate the node, and the
private key signs the outbound feed at /federation/feed.

This module is the *identity* primitives only — discovery walkers,
vouching/quorum, transparency log and auto-pull live in later stages.
"""

from __future__ import annotations

import hashlib
import os
from typing import Tuple

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519

from psyc import DATA_DIR, log


_log = log.get(__name__)

FED_DIR = DATA_DIR / "federation"
PRIVATE_KEY_PATH = FED_DIR / "node.key"
PUBLIC_KEY_PATH = FED_DIR / "node.pub"


# ---------- keypair persistence -----------------------------------------

def _ensure_dir() -> None:
    FED_DIR.mkdir(parents=True, exist_ok=True)


def node_keypair() -> Tuple[ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey]:
    """Return the node's Ed25519 keypair, generating + persisting it on first call.

    Private key lands at data/federation/node.key (PEM, chmod 0600); public
    at data/federation/node.pub (PEM). Idempotent — subsequent calls load
    the existing files instead of generating new ones.
    """
    _ensure_dir()
    if PRIVATE_KEY_PATH.exists() and PUBLIC_KEY_PATH.exists():
        priv_pem = PRIVATE_KEY_PATH.read_bytes()
        priv = serialization.load_pem_private_key(priv_pem, password=None)
        if not isinstance(priv, ed25519.Ed25519PrivateKey):
            raise RuntimeError(f"federation key at {PRIVATE_KEY_PATH} is not Ed25519")
        return priv, priv.public_key()

    priv = ed25519.Ed25519PrivateKey.generate()
    priv_pem = priv.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )
    pub = priv.public_key()
    pub_pem = pub.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    PRIVATE_KEY_PATH.write_bytes(priv_pem)
    os.chmod(PRIVATE_KEY_PATH, 0o600)
    PUBLIC_KEY_PATH.write_bytes(pub_pem)
    _log.info("federation.keypair.generated", path=str(PRIVATE_KEY_PATH))
    return priv, pub


def public_key_pem() -> str:
    """PEM-encoded public key as text — what peers store + verify against."""
    _, pub = node_keypair()
    return pub.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    ).decode("ascii")


def _raw_pubkey_bytes(pub: ed25519.Ed25519PublicKey) -> bytes:
    return pub.public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )


def node_fingerprint() -> str:
    """Short stable id for the node — first 16 bytes of SHA256(raw_pubkey), hex.

    Lives in DNS TXT records; 32 hex chars is short enough to fit but long
    enough to be collision-safe for any plausible peer population.
    """
    _, pub = node_keypair()
    digest = hashlib.sha256(_raw_pubkey_bytes(pub)).digest()
    return digest[:16].hex()
