#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 90-peers — 42i-hc Drop-in: net-Collector. Fragt eine konfigurierte Liste von
# Peers (deren hc auf :4711) per HTTP ab und haengt deren Subtrees als children
# ein.
# hc-tags: recursive, children, net
# hc-ttl: 20
#
# Macht diesen Host zum Aggregator "alles in meinem Netz" (z. B. auf der
# keepalived-VIP 192.168.1.2 / home-a|b). Ohne Config ein No-op (leere
# children) — so ist das Script ueberall mitlieferbar und nur dort aktiv, wo
# eine peers-Config liegt.
#
# Config (YAML), Default /etc/42i-hc/peers.yaml (override HC_PEERS):
#   port: 4711            # optional, Default 4711
#   peers: [10.18.5.40, 192.168.1.3, s18p1.lan]
#
# Gegen Collector-Rekursion werden die Peers mit ?skip=net abgefragt (ihr
# eigenes 90-peers laeuft dann nicht). Schema: 42i/intern#236. Geliefert von
# 42i-hc.

import json
import os
import sys
import concurrent.futures
import urllib.error
import urllib.request

PEERS_FILE = os.environ.get("HC_PEERS") or "/etc/42i-hc/peers.yaml"
TIMEOUT = float(os.environ.get("HC_PEER_TIMEOUT") or 8)

try:
    import yaml
except Exception:
    yaml = None


def load_config():
    """Laedt die peers-Config (Liste von Adressen + optionaler Port)."""
    if not (yaml and os.path.isfile(PEERS_FILE)):
        return [], 4711
    try:
        with open(PEERS_FILE, encoding="utf-8") as fh:
            data = yaml.safe_load(fh) or {}
    except Exception:
        return [], 4711
    peers = data.get("peers") or []
    port = data.get("port") or 4711
    return [str(p).strip() for p in peers if str(p).strip()], int(port)


def peer_url(peer, port):
    """Baut die Abfrage-URL eines Peers; ?skip=net verhindert Collector-Rekursion."""
    if peer.startswith("http://") or peer.startswith("https://"):
        base = peer.rstrip("/")
    else:
        base = "http://%s:%d" % (peer, port)
    return base + "/?skip=net"


def fetch(peer, port):
    """Holt den Subtree eines Peers; bei Fehler ein unreachable-Knoten.

    HTTP 503 ist KEIN Transportfehler, sondern eine valide hc-Antwort
    (Gesamt-Status `fail`) mit vollem Body — sonst verschwaende ein Peer aus
    dem Inventar, sobald eines seiner Kinder `fail` ist. Nur echte
    Verbindungsfehler (Timeout/Refused) werden zu `unreachable`.
    """
    url = peer_url(peer, port)
    try:
        try:
            resp = urllib.request.urlopen(url, timeout=TIMEOUT)
        except urllib.error.HTTPError as http_err:
            resp = http_err  # 503 trägt den vollen JSON-Body
        with resp:
            node = json.loads(resp.read().decode("utf-8"))
        if isinstance(node, dict):
            node.setdefault("host", {}).setdefault("peer", peer)
            return node
        raise ValueError("kein JSON-Objekt")
    except Exception as exc:
        return {"schema": "42i-hc/1", "status": "unreachable",
                "host": {"name": peer, "ips": [peer], "peer": peer,
                         "status": "unreachable"},
                "message": str(exc)[:200]}


def main():
    """Fragt alle Peers parallel ab und gibt {"children": [...]} als JSON aus."""
    peers, port = load_config()
    children = []
    if peers:
        with concurrent.futures.ThreadPoolExecutor(max_workers=min(16, len(peers))) as ex:
            futures = [ex.submit(fetch, p, port) for p in peers]
            for fut in concurrent.futures.as_completed(futures):
                children.append(fut.result())
    json.dump({"children": children}, sys.stdout, ensure_ascii=False)
    sys.stdout.write("\n")


if __name__ == "__main__":
    main()
