"""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 pydantic import BaseModel

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"

FEED_VERSION = "psyc1"
FEED_ALG = "ed25519"
FEED_PATH = "/federation/feed"


# ---------- 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()


def _fingerprint_for_pubkey_pem(pubkey_pem: str) -> str:
    pub = serialization.load_pem_public_key(pubkey_pem.encode("ascii"))
    if not isinstance(pub, ed25519.Ed25519PublicKey):
        raise ValueError("not an Ed25519 public key")
    return hashlib.sha256(_raw_pubkey_bytes(pub)).digest()[:16].hex()


# ---------- DNS record format -------------------------------------------

class DNSRecord(BaseModel):
    """The SRV + TXT pair an admin pastes into their zone file."""
    srv_name: str
    srv_target: str
    srv_port: int
    srv_priority: int = 10
    srv_weight: int = 10
    txt_name: str
    txt_value: str
    human_instructions: str


def dns_record(domain: str, port: int = 443) -> DNSRecord:
    """Build the DNS-SD-style records that advertise this node at `domain`."""
    fp = node_fingerprint()
    srv_name = f"_psyc._tcp.{domain}"
    srv_target = f"{domain}."
    txt_name = srv_name
    txt_value = f"v={FEED_VERSION} fp={fp} alg={FEED_ALG} path={FEED_PATH}"
    instructions = (
        f"; psyc federation records for {domain}\n"
        f"; ----------------------------------------------------------\n"
        f"; 1) SRV record — locates this psyc node (host + port).\n"
        f'{srv_name}.  3600  IN  SRV  10 10 {port} {srv_target}\n'
        f";\n"
        f"; 2) TXT record — declares protocol version, key fingerprint,\n"
        f";    signature algorithm, and the feed endpoint path.\n"
        f'{txt_name}.  3600  IN  TXT  "{txt_value}"\n'
        f"; ----------------------------------------------------------\n"
        f"; Once these are live, federation peers can fetch:\n"
        f";   https://{domain}{FEED_PATH}   (signed feed JSON)\n"
        f";   https://{domain}/federation/key   (public key PEM)\n"
        f";   https://{domain}/federation/info  (capabilities)\n"
    )
    return DNSRecord(
        srv_name=srv_name,
        srv_target=srv_target,
        srv_port=port,
        txt_name=txt_name,
        txt_value=txt_value,
        human_instructions=instructions,
    )
