#!/usr/bin/env python3
"""Hetzner Server Auction / Robot helpers.

Default behavior is dry-run for anything destructive.
Uses the Robot Webservice for authenticated actions and the public auction feed
for unauthenticated browsing fallback.
"""

from __future__ import annotations

import argparse
import base64
import json
import os
import re
import ssl
import subprocess
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any

sys.path.insert(0, str((Path(__file__).resolve().parent / "lib")))
from state_paths import BUILDER_HOST_STATE_FILE as STATE_FILE, STATE_DIR  # noqa: E402

ROBOT_BASE = "https://robot-ws.your-server.de"
PUBLIC_AUCTION_EUR = "https://www.hetzner.com/_resources/app/data/app/live_data_sb_EUR.json"
DEFAULT_PATTERN = r"(rtx\s*6000\s*ada|ada\s*6000)"
GPU_PATTERN = r"(gpu|nvidia|rtx|a100|tesla|a6000|ada)"
BIG_VRAM_PATTERN = r"(rtx\s*6000\s*ada|rtx\s*a6000|\ba100\b)"


def env(name: str, default: str | None = None) -> str | None:
    return os.environ.get(name, default)


def load_state() -> dict[str, Any]:
    if not STATE_FILE.exists():
        return {}
    try:
        payload = json.loads(STATE_FILE.read_text())
    except json.JSONDecodeError as e:
        raise SystemExit(f"Invalid state file {STATE_FILE}: {e}")
    return payload if isinstance(payload, dict) else {}


def save_state(update: dict[str, Any], *, replace: bool = False) -> None:
    state = {} if replace else load_state()
    for key, value in update.items():
        if value is None:
            continue
        if isinstance(value, str) and not value:
            continue
        state[key] = value
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2) + "\n")
    print(f"Saved state to {STATE_FILE}")


def robot_auth() -> tuple[str, str]:
    user = env("HETZNER_ROBOT_USER")
    password = env("HETZNER_ROBOT_PASSWORD")
    if not user or not password:
        raise SystemExit("Missing HETZNER_ROBOT_USER / HETZNER_ROBOT_PASSWORD")
    return user, password


def robot_request(method: str, path: str, data: dict[str, Any] | None = None) -> Any:
    user, password = robot_auth()
    url = f"{ROBOT_BASE}{path}"
    headers = {
        "Authorization": "Basic " + base64.b64encode(f"{user}:{password}".encode()).decode(),
        "Accept": "application/json",
    }
    body = None
    if data is not None:
        pairs: list[tuple[str, str]] = []
        for k, v in data.items():
            if isinstance(v, (list, tuple)):
                for item in v:
                    pairs.append((k, str(item)))
            else:
                pairs.append((k, str(v)))
        body = urllib.parse.urlencode(pairs).encode()
        headers["Content-Type"] = "application/x-www-form-urlencoded"
    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, context=ssl.create_default_context(), timeout=30) as resp:
            return json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        detail = e.read().decode()
        if e.code == 401:
            raise SystemExit(
                "Robot API error 401 for "
                f"{path}: {detail}\n\n"
                "Hetzner Robot Webservice requires a web service user for HTTP Basic Auth.\n"
                "In Robot, open: user menu -> Settings -> Webservice and app settings.\n"
                "Store that webservice username/password in HETZNER_ROBOT_USER and HETZNER_ROBOT_PASSWORD."
            )
        raise SystemExit(f"Robot API error {e.code} for {path}: {detail}")


def public_feed() -> Any:
    with urllib.request.urlopen(PUBLIC_AUCTION_EUR, context=ssl.create_default_context(), timeout=30) as resp:
        return json.loads(resp.read().decode())


def normalize_market_products(payload: Any) -> list[dict[str, Any]]:
    if isinstance(payload, dict) and isinstance(payload.get("server"), list):
        return payload["server"]

    items: list[dict[str, Any]] = []
    if isinstance(payload, list):
        for item in payload:
            if not isinstance(item, dict):
                continue
            if "product" in item and isinstance(item["product"], dict):
                items.append(item["product"])
            elif "server_market_product" in item and isinstance(item["server_market_product"], dict):
                items.append(item["server_market_product"])
            elif "server" in item and isinstance(item["server"], dict):
                items.append(item["server"])
            else:
                items.append(item)
        return items

    if isinstance(payload, dict):
        for key in ("product", "server_market_product"):
            if isinstance(payload.get(key), dict):
                return [payload[key]]
    return []


def normalize_servers(payload: Any) -> list[dict[str, Any]]:
    servers: list[dict[str, Any]] = []
    if isinstance(payload, list):
        for item in payload:
            if not isinstance(item, dict):
                continue
            if isinstance(item.get("server"), dict):
                servers.append(item["server"])
            else:
                servers.append(item)
    elif isinstance(payload, dict):
        if isinstance(payload.get("server"), dict):
            servers.append(payload["server"])
        else:
            servers.append(payload)
    return [s for s in servers if isinstance(s, dict)]


def product_text(p: dict[str, Any]) -> str:
    bits = []
    for key in ("name", "cpu", "datacenter"):
        v = p.get(key)
        if v:
            bits.append(str(v))
    for key in ("description", "information", "ram", "hdd_arr", "specials"):
        v = p.get(key)
        if isinstance(v, list):
            bits.extend(str(x) for x in v)
        elif v:
            bits.append(str(v))
    return " | ".join(bits)


def filter_products(products: list[dict[str, Any]], pattern: str) -> list[dict[str, Any]]:
    rx = re.compile(pattern, re.I)
    return [p for p in products if rx.search(product_text(p))]


def apply_preset(preset: str, pattern: str | None) -> str:
    if pattern:
        return pattern
    if preset == "cheapest-any":
        return r"."
    if preset == "cheapest-gpu":
        return GPU_PATTERN
    if preset == "cheapest-ada":
        return DEFAULT_PATTERN
    if preset == "cheapest-big-vram":
        return BIG_VRAM_PATTERN
    return DEFAULT_PATTERN


def sort_products(products: list[dict[str, Any]], sort_key: str) -> list[dict[str, Any]]:
    if sort_key == "hourly":
        return sorted(products, key=lambda p: float(p.get("hourly_price") or 1e9))
    if sort_key == "monthly":
        return sorted(products, key=lambda p: float(p.get("price") or 1e9))
    if sort_key == "ram":
        return sorted(products, key=lambda p: float(p.get("ram_size") or 0), reverse=True)
    return products


def print_products(products: list[dict[str, Any]]) -> None:
    for p in products:
        pid = p.get("id") or p.get("product_id") or p.get("key")
        print(
            f"id={pid}  hourly={p.get('hourly_price')}  setup={p.get('setup_price')}  "
            f"dc={p.get('datacenter')}  cpu={p.get('cpu')}"
        )
        desc = p.get("description") or []
        if isinstance(desc, list):
            for line in desc:
                print(f"  - {line}")
        print()


def local_pubkey(pubkey_path: Path) -> str:
    if not pubkey_path.exists():
        raise SystemExit(f"SSH public key not found: {pubkey_path}")
    return pubkey_path.read_text().strip()


def local_md5_fingerprint(pubkey_path: Path) -> str:
    out = subprocess.check_output(["ssh-keygen", "-E", "md5", "-lf", str(pubkey_path)], text=True)
    fp = out.split()[1]
    return fp.replace("MD5:", "")


def ensure_robot_key(pubkey_path: Path, name: str, execute: bool) -> str:
    pub = local_pubkey(pubkey_path)
    fp = local_md5_fingerprint(pubkey_path)

    has_creds = bool(env("HETZNER_ROBOT_USER") and env("HETZNER_ROBOT_PASSWORD"))
    if not has_creds and not execute:
        print(f"Dry-run: would ensure Robot key '{name}' exists (fingerprint {fp})")
        return fp
    if not has_creds and execute:
        raise SystemExit("Missing HETZNER_ROBOT_USER / HETZNER_ROBOT_PASSWORD for execute mode")

    try:
        robot_request("GET", f"/key/{urllib.parse.quote(fp, safe=':')}")
        print(f"Robot key exists: {fp}")
        return fp
    except SystemExit as e:
        if "404" not in str(e):
            raise
    if not execute:
        print(f"Dry-run: would create Robot key '{name}' with fingerprint {fp}")
        return fp
    resp = robot_request("POST", "/key", {"name": name, "data": pub})
    if "key" in resp and isinstance(resp["key"], dict):
        return resp["key"].get("fingerprint", fp)
    return fp


def resolve_first_match(pattern: str | None, source: str, preset: str = "cheapest-ada") -> int:
    if source == "robot":
        payload = robot_request("GET", "/order/server_market/product")
    elif source == "public":
        payload = public_feed()
    else:
        try:
            payload = robot_request("GET", "/order/server_market/product")
        except SystemExit:
            payload = public_feed()
    products = normalize_market_products(payload)
    effective_pattern = apply_preset(preset, pattern)
    matches = sort_products(filter_products(products, effective_pattern), "hourly")
    if not matches:
        raise SystemExit(f"No products matched pattern: {pattern}")
    pid = matches[0].get("id") or matches[0].get("product_id") or matches[0].get("key")
    if not pid:
        raise SystemExit("Matched product has no id")
    return int(pid)


def extract_transaction_id(payload: Any) -> str | None:
    if isinstance(payload, dict):
        for key in ("transaction", "server_market_transaction"):
            value = payload.get(key)
            if isinstance(value, dict) and value.get("id"):
                return str(value["id"])
        if payload.get("id") and any(k in payload for k in ("status", "product_id", "created")):
            return str(payload["id"])
        for value in payload.values():
            tx_id = extract_transaction_id(value)
            if tx_id:
                return tx_id
    elif isinstance(payload, list):
        for item in payload:
            tx_id = extract_transaction_id(item)
            if tx_id:
                return tx_id
    return None


def extract_server_ref(payload: Any) -> dict[str, str]:
    if isinstance(payload, dict):
        server_number = payload.get("server_number")
        server_ip = payload.get("server_ip")
        if server_number or server_ip:
            out: dict[str, str] = {}
            if server_number:
                out["server_number"] = str(server_number)
            if server_ip:
                out["server_ip"] = str(server_ip)
            return out
        for value in payload.values():
            found = extract_server_ref(value)
            if found:
                return found
    elif isinstance(payload, list):
        for item in payload:
            found = extract_server_ref(item)
            if found:
                return found
    return {}


def server_sort_key(server: dict[str, Any]) -> tuple[int, str]:
    number = server.get("server_number")
    try:
        parsed = int(str(number))
    except (TypeError, ValueError):
        parsed = 0
    return (parsed, str(server.get("server_ip") or ""))


def list_servers() -> list[dict[str, Any]]:
    return normalize_servers(robot_request("GET", "/server"))


def get_server(server_number: str | None = None, server_ip: str | None = None) -> dict[str, Any]:
    state = load_state()
    if not server_number:
        server_number = state.get("server_number")
    if not server_ip:
        server_ip = state.get("server_ip")

    servers = list_servers()
    for server in servers:
        if server_number and str(server.get("server_number")) == str(server_number):
            return server
        if server_ip and server.get("server_ip") == server_ip:
            return server
    raise SystemExit("Server not found")


def print_servers(servers: list[dict[str, Any]]) -> None:
    for server in servers:
        print(
            f"server_number={server.get('server_number')}  ip={server.get('server_ip')}  "
            f"name={server.get('server_name') or server.get('server_name') or '-'}  "
            f"dc={server.get('dc') or server.get('datacenter') or '-'}  status={server.get('status') or '-'}"
        )
        for key in ("product", "cancelled", "paid_until", "traffic", "subnet", "rescue_active"):
            value = server.get(key)
            if value not in (None, "", []):
                print(f"  - {key}: {value}")
        print()


def cmd_find(args: argparse.Namespace) -> None:
    if args.source == "robot":
        payload = robot_request("GET", "/order/server_market/product")
    elif args.source == "public":
        payload = public_feed()
    else:
        try:
            payload = robot_request("GET", "/order/server_market/product")
        except SystemExit:
            payload = public_feed()
    products = normalize_market_products(payload)
    effective_pattern = apply_preset(args.preset, args.pattern)
    matches = sort_products(filter_products(products, effective_pattern), args.sort)
    if args.json:
        print(json.dumps(matches, indent=2))
    else:
        print(f"Matched {len(matches)} products  (preset={args.preset}, sort={args.sort})")
        print_products(matches[: args.limit])


def cmd_key(args: argparse.Namespace) -> None:
    fp = ensure_robot_key(Path(args.pubkey), args.name, args.execute)
    print(fp)


def cmd_order(args: argparse.Namespace) -> None:
    product_id = args.product_id if args.product_id is not None else resolve_first_match(args.pattern, args.source, args.preset)
    fp = ensure_robot_key(Path(args.pubkey), args.key_name, args.execute)
    payload = {"product_id": str(product_id), "authorized_key[]": [fp]}
    if args.primary_ipv4:
        payload["addon[]"] = ["primary_ipv4"]
    if not args.execute:
        print("Dry-run order payload:")
        print(json.dumps(payload, indent=2))
        print("Endpoint: POST /order/server_market/transaction")
        return

    resp = robot_request("POST", "/order/server_market/transaction", payload)
    print(json.dumps(resp, indent=2))

    state_update: dict[str, Any] = {"product_id": str(product_id)}
    tx_id = extract_transaction_id(resp)
    if tx_id:
        state_update["transaction_id"] = tx_id
    server_ref = extract_server_ref(resp)
    state_update.update(server_ref)
    if state_update:
        save_state(state_update)


def cmd_transaction(args: argparse.Namespace) -> None:
    tx = args.transaction_id or load_state().get("transaction_id")
    if not tx:
        raise SystemExit("Need --transaction-id or existing state file")

    deadline = time.time() + args.timeout
    resp: Any = None
    while True:
        resp = robot_request("GET", f"/order/server_market/transaction/{urllib.parse.quote(str(tx))}")
        server_ref = extract_server_ref(resp)
        if server_ref:
            save_state({"transaction_id": str(tx), **server_ref})
            break
        if not args.wait_for_server:
            break
        if time.time() >= deadline:
            break
        time.sleep(args.interval)

    print(json.dumps(resp, indent=2))

    if args.wait_for_server:
        server_ref = extract_server_ref(resp)
        if not server_ref:
            raise SystemExit("Timed out waiting for server assignment in transaction response")


def cmd_server(args: argparse.Namespace) -> None:
    servers = list_servers()
    if args.server_number or args.server_ip:
        server = get_server(args.server_number, args.server_ip)
        selected = [server]
    elif args.latest:
        if not servers:
            raise SystemExit("No servers found")
        selected = [sorted(servers, key=server_sort_key, reverse=True)[0]]
    else:
        selected = sorted(servers, key=server_sort_key, reverse=True)[: args.limit]

    if args.save and selected:
        save_state(extract_server_ref(selected[0]))

    if args.json:
        if args.latest or args.server_number or args.server_ip:
            print(json.dumps(selected[0], indent=2))
        else:
            print(json.dumps(selected, indent=2))
        return

    print_servers(selected)


def cmd_rescue(args: argparse.Namespace) -> None:
    fp = ensure_robot_key(Path(args.pubkey), args.key_name, args.execute)
    has_creds = bool(env("HETZNER_ROBOT_USER") and env("HETZNER_ROBOT_PASSWORD"))
    state = load_state()
    server_number = args.server_number or state.get("server_number")
    server_ip = args.server_ip or state.get("server_ip")

    if not has_creds and not args.execute:
        num = server_number or "<server-number>"
        payload = {"os": "linux", "authorized_key[]": [fp]}
        print(f"Dry-run: would POST /boot/{num}/rescue with:")
        print(json.dumps(payload, indent=2))
        if args.power_cycle:
            print(f"Dry-run: would POST /reset/{num} with type=hw")
        return

    server = get_server(server_number, server_ip)
    num = server["server_number"]
    payload = {"os": "linux", "authorized_key[]": [fp]}
    if not args.execute:
        print(f"Dry-run: would POST /boot/{num}/rescue with:")
        print(json.dumps(payload, indent=2))
        if args.power_cycle:
            print(f"Dry-run: would POST /reset/{num} with type=hw")
        return

    resp = robot_request("POST", f"/boot/{num}/rescue", payload)
    print(json.dumps(resp, indent=2))
    if args.power_cycle:
        reset = robot_request("POST", f"/reset/{num}", {"type": "hw"})
        print(json.dumps(reset, indent=2))
    save_state({"server_number": str(num), "server_ip": server.get("server_ip")})


def cmd_cancellation(args: argparse.Namespace) -> None:
    state = load_state()
    server_number = args.server_number or state.get("server_number")
    server_ip = args.server_ip or state.get("server_ip")
    server = get_server(server_number, server_ip)
    num = server["server_number"]
    path = f"/server/{num}/cancellation"

    if args.revoke:
        if not args.execute:
            print(f"Dry-run: would DELETE {path}")
            return
        resp = robot_request("DELETE", path)
        print(json.dumps(resp, indent=2))
        cancellation = resp.get("cancellation", {}) if isinstance(resp, dict) else {}
        save_state({
            "server_number": str(num),
            "server_cancellation_date": cancellation.get("cancellation_date"),
            "server_cancelled": cancellation.get("cancelled"),
        })
        return

    if args.execute:
        payload: dict[str, Any] = {"cancellation_date": args.date or "now"}
        if args.reason:
            payload["cancellation_reason"] = args.reason
        if args.reserve_location is not None:
            payload["reserve_location"] = "true" if args.reserve_location else "false"
        resp = robot_request("POST", path, payload)
        print(json.dumps(resp, indent=2))
        cancellation = resp.get("cancellation", {}) if isinstance(resp, dict) else {}
        save_state({
            "server_number": str(num),
            "server_cancellation_date": cancellation.get("cancellation_date"),
            "server_cancelled": cancellation.get("cancelled"),
            "server_cancellation_reason": cancellation.get("cancellation_reason"),
            "server_reserved_after_cancellation": cancellation.get("reserved"),
        })
        return

    if args.date or args.reason or args.reserve_location is not None:
        payload: dict[str, Any] = {"cancellation_date": args.date or "now"}
        if args.reason:
            payload["cancellation_reason"] = args.reason
        if args.reserve_location is not None:
            payload["reserve_location"] = "true" if args.reserve_location else "false"
        print(f"Dry-run: would POST {path} with:")
        print(json.dumps(payload, indent=2))
        return

    resp = robot_request("GET", path)
    print(json.dumps(resp, indent=2))
    cancellation = resp.get("cancellation", {}) if isinstance(resp, dict) else {}
    save_state({
        "server_number": str(num),
        "server_cancellation_date": cancellation.get("cancellation_date"),
        "server_cancelled": cancellation.get("cancelled"),
        "server_cancellation_reason": cancellation.get("cancellation_reason"),
        "server_reserved_after_cancellation": cancellation.get("reserved"),
    })


def format_builder_host(host: str) -> str:
    if ":" in host and not (host.startswith("[") and host.endswith("]")):
        return f"[{host}]"
    return host


def scan_host_key_base64(host: str) -> str:
    scan_cmd = ["ssh-keyscan", "-T", "5", "-t", "ed25519"]
    if ":" in host:
        scan_cmd.append("-6")
    scan_cmd.append(host)
    out = subprocess.check_output(scan_cmd, text=True, stderr=subprocess.DEVNULL)
    for line in out.splitlines():
        if not line or line.startswith("#"):
            continue
        parts = line.split()
        if len(parts) >= 3:
            key_text = f"{parts[1]} {parts[2]}"
            return base64.b64encode(key_text.encode()).decode()
    raise SystemExit(f"Failed to scan SSH host key for {host}")


def cmd_register(args: argparse.Namespace) -> None:
    state = load_state()
    host = args.server_host or args.server_ip or state.get("server_host") or state.get("server_ip")
    if not host:
        raise SystemExit("Need --server-host/--server-ip or state file")
    key = os.path.expanduser(args.ssh_key)
    supported_features = state.get("builder_supported_features") or "benchmark,big-parallel"
    features = (
        args.features
        or state.get("builder_registered_features")
        or supported_features
    )
    builder_user = args.builder_user or state.get("builder_user") or "builder"
    protocol = args.protocol or state.get("builder_protocol") or "ssh-ng"
    host_uri = format_builder_host(host)
    host_key_base64 = args.host_key_base64 or scan_host_key_base64(host)
    builder_spec = f"{protocol}://{builder_user}@{host_uri} x86_64-linux {key} 24 48 {features} - {host_key_base64}"
    config = (
        f"builders = {builder_spec}\n"
        f"builders-use-substitutes = true\n"
    )
    out = STATE_DIR / "builder-remote.conf"
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    out.write_text(config)
    save_state({
        "builder_host_key_base64": host_key_base64,
        "builder_user": builder_user,
        "builder_protocol": protocol,
        "builder_supported_features": supported_features,
        "builder_registered_features": features,
    })
    print(f"Wrote {out}")
    print()
    print("Use with:")
    print(f"export SPY_NIX_BUILD_OPTS=\"--builders '{builder_spec}'\"")


def main() -> None:
    p = argparse.ArgumentParser()
    sub = p.add_subparsers(dest="cmd", required=True)

    pf = sub.add_parser("find")
    pf.add_argument("--preset", choices=["cheapest-any", "cheapest-gpu", "cheapest-ada", "cheapest-big-vram"], default="cheapest-ada")
    pf.add_argument("--pattern")
    pf.add_argument("--source", choices=["auto", "robot", "public"], default="auto")
    pf.add_argument("--sort", choices=["hourly", "monthly", "ram", "none"], default="hourly")
    pf.add_argument("--limit", type=int, default=20)
    pf.add_argument("--json", action="store_true")
    pf.set_defaults(func=cmd_find)

    pk = sub.add_parser("key")
    pk.add_argument("--pubkey", default=str(Path.home() / ".ssh/id_ed25519.pub"))
    pk.add_argument("--name", default="spies-builder-key")
    pk.add_argument("--execute", action="store_true")
    pk.set_defaults(func=cmd_key)

    po = sub.add_parser("order")
    po.add_argument("--product-id", type=int)
    po.add_argument("--preset", choices=["cheapest-any", "cheapest-gpu", "cheapest-ada", "cheapest-big-vram"], default="cheapest-ada")
    po.add_argument("--pattern")
    po.add_argument("--source", choices=["auto", "robot", "public"], default="auto")
    po.add_argument("--pubkey", default=str(Path.home() / ".ssh/id_ed25519.pub"))
    po.add_argument("--key-name", default="spies-builder-key")
    po.add_argument("--primary-ipv4", action="store_true")
    po.add_argument("--execute", action="store_true")
    po.set_defaults(func=cmd_order)

    pt = sub.add_parser("transaction")
    pt.add_argument("--transaction-id")
    pt.add_argument("--wait-for-server", action="store_true")
    pt.add_argument("--timeout", type=int, default=300)
    pt.add_argument("--interval", type=int, default=5)
    pt.set_defaults(func=cmd_transaction)

    ps = sub.add_parser("server")
    ps.add_argument("--server-number")
    ps.add_argument("--server-ip")
    ps.add_argument("--latest", action="store_true")
    ps.add_argument("--limit", type=int, default=20)
    ps.add_argument("--save", action="store_true")
    ps.add_argument("--json", action="store_true")
    ps.set_defaults(func=cmd_server)

    pr = sub.add_parser("rescue")
    pr.add_argument("--server-number")
    pr.add_argument("--server-ip")
    pr.add_argument("--pubkey", default=str(Path.home() / ".ssh/id_ed25519.pub"))
    pr.add_argument("--key-name", default="spies-builder-key")
    pr.add_argument("--power-cycle", action="store_true")
    pr.add_argument("--execute", action="store_true")
    pr.set_defaults(func=cmd_rescue)

    pc = sub.add_parser("cancellation")
    pc.add_argument("--server-number")
    pc.add_argument("--server-ip")
    pc.add_argument("--date")
    pc.add_argument("--reason")
    pc.add_argument("--reserve-location", dest="reserve_location", action="store_true")
    pc.add_argument("--no-reserve-location", dest="reserve_location", action="store_false")
    pc.add_argument("--revoke", action="store_true")
    pc.add_argument("--execute", action="store_true")
    pc.set_defaults(func=cmd_cancellation, reserve_location=None)

    preg = sub.add_parser("register")
    preg.add_argument("--server-ip")
    preg.add_argument("--server-host")
    preg.add_argument("--ssh-key", default=str(Path.home() / ".ssh/id_ed25519"))
    preg.add_argument("--builder-user", default="builder")
    preg.add_argument("--protocol", choices=["ssh", "ssh-ng"], default="ssh-ng")
    preg.add_argument("--features")
    preg.add_argument("--host-key-base64")
    preg.set_defaults(func=cmd_register)

    args = p.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
