"""Birth: key -> mailbox -> announce (docs/RELAY-CENSUS.md §4.1).

A brain announces *that it exists* to its relay — the same relay every rendered
config already points at — so the relay can be the census of the fleet. This is
the one new moment in the engine: today the identity key and the relay mailbox are
minted lazily on the first pairing; :func:`announce` makes them exist at birth and
adds a birth record on top. It is idempotent (``load_or_create`` never regenerates
a key, ``ensure_mailbox`` reuses the cached bearer, the relay's PUT refreshes) and
fail-soft (a relay that is down is a one-line notice, never a failed install, boot
or tick).

What leaves the brain is the closed whitelist in :data:`FIELDS` and nothing else —
listed verbatim in docs/FEDERATION.md ("What the relay learns about your brain").
No owner name, no vault name, nothing from a vault. It is on by default because the
relay address already is; ``federation.announce: false`` turns it off, and a brain
with no ``relay_url`` announces nothing because it has nowhere to announce to.

The two federation laws are untouched: an announcement is not content and no peer
reads anything — the census read surface is the relay operator's, and a brain can
never list it (§3).
"""
import os
import platform as _platform
import re
import sys
import time

from alexandria_vault.federation import identity, pairing, state
from alexandria_vault.federation import relay_client as rc

# The fields a birth record carries — the schema IS the whitelist (§7.3). Adding
# one means a bound here, in relay/core.BIRTH_FIELDS, and a line in FEDERATION.md.
FIELDS = ("engine", "channel", "platform", "tenant_id", "rack")
CHANNELS = ("local", "rack", "console", "upgrade")
# A successful announcement is "fresh" this long for the non-forced callers (the
# cron tick), so a 5-minute tick does not PUT every 5 minutes into a 6/h bucket.
REANNOUNCE_SECONDS = 24 * 3600
# After a failed attempt, the non-forced callers wait this long before retrying.
RETRY_SECONDS = 600
_PLATFORM_RE = re.compile(r"[^a-z0-9-]+")
TENANT_ENV = "VAULT_TENANT"       # set by rackd for every racked brain (provision.py)
RACK_ENV = "VAULT_RACK"           # optional: the rack's base_domain, if the rack sets it
# The relay's bounds for the two optional fields (relay/core.BIRTH_FIELDS), mirrored
# here because the relay is standalone and the engine cannot import it — pinned equal
# by tests/test_federation_birth.py. A value outside them is OMITTED, never truncated
# or sent: a truncated tenant id is a different id the console cannot join, and an
# over-shape value is a 400 the brain would retry forever.
TENANT_RE = re.compile(r"\A[a-z0-9][a-z0-9-]{0,63}\Z")
RACK_RE = re.compile(r"\A[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\Z")


def engine_version():
    """``scripts.__version__`` (the single source), shaped for the wire."""
    try:
        from scripts import __version__
        ver = str(__version__)
    except Exception:  # noqa: BLE001 — an engine without the scripts package
        ver = "unknown"
    ver = re.sub(r"[^0-9A-Za-z.+-]", "-", ver)
    return ver[:24] or "unknown"


def host_platform():
    """``<os>-<arch>``, e.g. ``darwin-arm64`` / ``linux-x86-64`` — an enum-ish
    label, not a fingerprint of the machine (§9.1)."""
    arch = (_platform.machine() or "unknown").lower().replace("_", "-")
    label = _PLATFORM_RE.sub("-", f"{sys.platform}-{arch}".lower()).strip("-")
    return label[:24] or "unknown"


def default_channel():
    """The canal a running brain reports from: ``rack`` when rackd's tenant id is
    in the environment, else ``upgrade`` — a brain already alive when the census
    shipped, or a local brain re-announcing on boot (the relay keeps the first real
    channel, so a ``local`` birth is never overwritten by this)."""
    return "rack" if os.environ.get(TENANT_ENV) else "upgrade"


def enabled(cfg):
    """Announce iff there is a relay to announce to and the owner has not turned it
    off (``federation.announce: false``)."""
    fed = (cfg or {}).get("federation") or {}
    if not isinstance(fed, dict) or not fed.get("relay_url"):
        return False
    return fed.get("announce", True) is not False


def record(channel, *, engine=None, platform=None, env=None):
    """The birth record — exactly :data:`FIELDS`, optionals omitted when unset."""
    if channel not in CHANNELS:
        raise ValueError(f"channel must be one of {CHANNELS}, got {channel!r}")
    env = os.environ if env is None else env
    rec = {"engine": engine or engine_version(), "channel": channel,
           "platform": platform or host_platform()}
    tenant = (env.get(TENANT_ENV) or "").strip()
    if tenant and TENANT_RE.match(tenant):
        rec["tenant_id"] = tenant
    rack = (env.get(RACK_ENV) or "").strip().lower()
    if rack and len(rack) <= 64 and RACK_RE.match(rack):
        rec["rack"] = rack
    return rec


def effective_config(cfg):
    """The config a running brain announces FROM: a copy with the default federation
    block seeded when the block is absent or carries no ``relay_url`` key at all — a
    vault rendered before 4.0, whose ``config.json`` no ``update`` ever rewrote. An
    explicit ``relay_url: null`` is an opt-out and is left alone (the same rule as the
    CLI's ``_fed_setup``). In-memory only: serve never rewrites a vault's config; the
    federation CLI remains the one place the block is persisted."""
    from scripts import config as cfgmod
    cfg = dict(cfg or {})
    fed = cfg.get("federation")
    if fed is None or (isinstance(fed, dict) and "relay_url" not in fed):
        cfg["federation"] = dict(fed) if isinstance(fed, dict) else None
        cfgmod.ensure_federation_defaults(cfg)
    return cfg


def make_client(relay_url):
    """The relay client a caller gets when it passes none — a seam the test suite
    replaces with an in-process relay core (or an offline stub)."""
    return rc.RelayClient(relay_url)


def _skip(fp, why):
    return {"fingerprint": fp, "announced": False, "status": None, "skipped": why,
            "error": None}


def announce(cfg, relay_client=None, *, channel, now=None, force=True):
    """Key -> mailbox -> birth record. Idempotent; safe to call on every boot.

    Returns a dict: ``fingerprint``, ``announced`` (bool), ``status`` (the relay's
    answer, or None), ``skipped`` (why nothing was sent: ``disabled`` /
    ``no-relay`` / ``fresh`` / ``backoff``, or None) and ``error`` (a transport or
    pairing failure, or None). With ``force=False`` a recent success (within
    :data:`REANNOUNCE_SECONDS`, on the same engine) or a recent failure (within
    :data:`RETRY_SECONDS`) is skipped — the cron tick and the worker's retry use
    that; birth canals and a boot pass ``force=True`` so every boot refreshes
    ``last_announced``.

    Three things the state file guarantees: ``engine`` is only ever the version the
    relay ACCEPTED (a failed attempt after an upgrade must not look fresh); a real
    channel that could not be sent (an offline ``init``) is offered again by the
    next canal instead of being overwritten by ``upgrade``; and a 401 on a bearer
    we hold means the relay forgot the mailbox (its idle GC), so the bearer is
    dropped and the mailbox registered afresh, once.

    Never regenerates a key (§7.1): identity comes from ``load_or_create``.
    Transport and pairing failures are returned, not raised; anything else (a
    corrupt key file, PyNaCl missing) is recorded as the attempt's error and then
    propagates, so the caller's fail-soft wrapper can report it — and so a caller
    that backs off on ``attempted_at`` still does."""
    ts = float(now) if now is not None else time.time()
    if not enabled(cfg):
        fed = (cfg or {}).get("federation") or {}
        return _skip(None, "disabled" if isinstance(fed, dict) and fed.get("relay_url")
                     else "no-relay")
    relay_url = pairing._relay_url(cfg)
    prior = state.birth_for(relay_url)
    engine = engine_version()
    entry = {"fingerprint": prior.get("fingerprint"), "engine": prior.get("engine"),
             "channel": channel, "attempted_at": ts, "announced_at": None,
             "status": None, "error": None}
    try:
        _, pub = identity.load_or_create()                  # 1. the key (exactly once, ever)
    except Exception as exc:                                # noqa: BLE001 — record, then raise
        entry["error"] = f"identity: {exc}"
        state.birth_save(relay_url, entry)
        raise
    fp = identity.fingerprint(pub)
    same = prior.get("fingerprint") == fp
    if not same:                                            # a new identity starts clean
        prior = {}
    done = prior.get("announced_at")
    # An unsent real channel (an `init` that ran offline) outranks the running
    # brain's `upgrade` placeholder — the relay keeps the first channel it hears.
    if (channel == "upgrade" and not done and prior.get("channel") in CHANNELS
            and prior["channel"] != "upgrade"):
        channel = prior["channel"]
    if not force:
        if done and prior.get("engine") == engine and ts - float(done) < REANNOUNCE_SECONDS:
            return _skip(fp, "fresh")
        tried = prior.get("attempted_at")
        if tried and not (done and float(done) >= float(tried)) \
                and ts - float(tried) < RETRY_SECONDS:
            return _skip(fp, "backoff")
    client = relay_client or make_client(relay_url)
    entry.update(fingerprint=fp, channel=channel, announced_at=done)
    try:
        fp, bearer, _ = pairing.ensure_mailbox(cfg, client, now=ts)          # 2. the mailbox
        status, _payload = client.birth(fp, bearer, record(channel, engine=engine))  # 3.
        if status == 401:
            # The relay no longer knows the mailbox our bearer was minted for — its
            # idle GC reclaimed it. Register once more and announce with the new
            # bearer — WITHOUT forgetting the cached one first (a failed registration
            # or a racing sibling must never lose a minted-once bearer; a 409 here is
            # a genuine lost bearer and surfaces as the PairingError it always did).
            fp, bearer, _ = pairing.reregister_mailbox(cfg, client, now=ts, stale=bearer)
            status, _payload = client.birth(fp, bearer, record(channel, engine=engine))
    except (rc.RelayUnavailable, pairing.PairingError) as exc:
        entry["error"] = str(exc)
        state.birth_save(relay_url, entry)
        return {"fingerprint": fp, "announced": False, "status": None, "skipped": None,
                "error": str(exc)}
    entry["status"] = status
    announced = status in (200, 201)
    if announced:
        entry["announced_at"] = ts
        entry["engine"] = engine                     # the version the relay now holds
    else:
        entry["error"] = f"relay answered {status}"
    state.birth_save(relay_url, entry)               # remember that it landed (or not)
    return {"fingerprint": fp, "announced": announced, "status": status, "skipped": None,
            "error": entry["error"]}
