#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.

"""
SecuBox-Deb :: sbx-fleetctl — box operator CLI for fleet metrics publishing

Thin CLI over annuaire.metrics_collect.collect_snapshot + annuaire.fleet.
sign_snapshot + annuaire.fleet_store.write — signs THIS box's own operational
snapshot with its own sovereign node key and persists it locally at
FLEET_SELF_PATH, where the annuaire API's public GET /fleet/self serves it
verbatim to mesh peers on :8799 (mirrors sbx-centersctl's shape: _die,
_is_dryrun, a box key that is loaded — never generated — from
ANNUAIRE_KEY_PATH).

Commands:
  publish   Sign + persist a fresh MetricSnapshot for this box. A no-op
            ({"skipped": "fleet_publish disabled"}, rc0) when
            [metrics] fleet_publish = false in secubox.conf. Meant to be
            invoked periodically (systemd timer), never itself a daemon.

Paths (override via env — the box key is NEVER generated by this CLI; it is
the box's sovereign identity, provisioned once elsewhere):
  ANNUAIRE_KEY_PATH  default /etc/secubox/secrets/annuaire/node.key
                     (32-byte raw Ed25519 private key, 64 hex chars) — the
                     SAME file annuairectl init / sbx-centersctl use. One
                     box, one sovereign identity.
  ANNUAIRE_LIB       default /usr/lib/secubox/annuaire  (import root, prod)
  SECUBOX_CONF       default /etc/secubox/secubox.conf  ([metrics] section)
  FLEET_SELF_PATH    default /var/lib/secubox/annuaire/fleet/self.json
                     (read by annuaire.fleet_store; set here via env so this
                     CLI and the API agree on where the signed record lives)

  DRYRUN=1           on publish: do NOT collect metrics or write self.json —
                      print the action that WOULD have been taken as
                      {"dryrun": true, "would": "publish", "node_did": ...}
                      and exit 0.
"""
from __future__ import annotations

import argparse
import json
import os
import sys

LIB = os.environ.get("ANNUAIRE_LIB", "/usr/lib/secubox/annuaire")
if LIB not in sys.path:
    sys.path.insert(0, LIB)

# Same env var + default as sbx-centersctl's KEY_PATH and api/main.py's
# node-key lookups: one box, one sovereign identity.
KEY_PATH = os.environ.get("ANNUAIRE_KEY_PATH", "/etc/secubox/secrets/annuaire/node.key")
SECUBOX_CONF = os.environ.get("SECUBOX_CONF", "/etc/secubox/secubox.conf")


def _die(msg: str, code: int = 1) -> "None":
    print(json.dumps({"error": msg}), file=sys.stderr)
    raise SystemExit(code)


def _is_dryrun() -> bool:
    """True iff DRYRUN=1 — publish must not collect or write anything."""
    return os.environ.get("DRYRUN") == "1"


def _load_box_priv() -> bytes:
    """Load the box's own 32-byte raw Ed25519 private key (hex-encoded on disk).

    This is the SAME node identity key `annuairectl init` generates/reads at
    ANNUAIRE_KEY_PATH — the box's one sovereign identity, provisioned once
    (image firstboot / `annuairectl init`), never minted here. A missing key
    is a hard configuration error, not something this CLI silently papers
    over by generating a fresh one.
    """
    if not os.path.exists(KEY_PATH):
        _die(
            f"no box key at {KEY_PATH} — run `annuairectl init` first (this "
            "CLI never generates one; it reuses the box's sovereign node "
            "identity)"
        )
    with open(KEY_PATH, "r", encoding="ascii") as fh:
        hexstr = fh.read().strip()
    try:
        priv = bytes.fromhex(hexstr)
    except ValueError:
        _die(f"box key at {KEY_PATH} is not valid hex")
        raise  # unreachable — _die raises SystemExit; satisfies type-checkers
    if len(priv) != 32:
        _die(f"box key must be 32 raw bytes (64 hex chars), got {len(priv)}")
        raise  # unreachable
    return priv


def _self_did(priv: bytes) -> str:
    from annuaire.crypto import did_from_pubkey, public_from_private  # noqa: PLC0415
    return did_from_pubkey(public_from_private(priv))


def _fleet_publish_enabled() -> bool:
    """[metrics] fleet_publish from SECUBOX_CONF — defaults True when the key,
    the [metrics] section, or the whole file is absent/unreadable."""
    if not os.path.exists(SECUBOX_CONF):
        return True
    try:
        import tomllib  # py3.11+  # noqa: PLC0415
        with open(SECUBOX_CONF, "rb") as f:
            data = tomllib.load(f)
        return bool(data.get("metrics", {}).get("fleet_publish", True))
    except Exception:
        return True


# --------------------------------------------------------------------------- #
# commands
# --------------------------------------------------------------------------- #

def cmd_publish(args) -> int:
    if not _fleet_publish_enabled():
        print(json.dumps({"skipped": "fleet_publish disabled"}))
        return 0

    priv = _load_box_priv()
    node_did = _self_did(priv)

    if _is_dryrun():
        print(json.dumps({"dryrun": True, "would": "publish", "node_did": node_did}, indent=2))
        return 0

    from annuaire import fleet, fleet_store  # noqa: PLC0415
    from annuaire.metrics_collect import collect_snapshot  # noqa: PLC0415

    fields = collect_snapshot(node_did)
    rec = fleet.sign_snapshot(priv, fields)
    fleet_store.write(rec)
    print(json.dumps({"published": node_did}, indent=2))
    return 0


def main(argv=None) -> int:
    p = argparse.ArgumentParser(
        prog="sbx-fleetctl",
        description="Box operator CLI for fleet metrics publishing (publish)",
    )
    sub = p.add_subparsers(dest="cmd", required=True)

    sub.add_parser(
        "publish", help="sign + persist a fresh MetricSnapshot for this box"
    ).set_defaults(fn=cmd_publish)

    args = p.parse_args(argv)
    return args.fn(args)


if __name__ == "__main__":
    raise SystemExit(main())
