#!/usr/bin/env python3
"""paas-cmk-sync — runs ON the Checkmk server (systemd timer).

Model: ONE Checkmk host per paas project, named after the project's MAIN
domain (uptime + cert via folder-scoped custom_checks rules on $HOSTNAME$).
Every OTHER configured domain of the project gets its own
"Certificate <domain>" service on that same host, via host-scoped
custom_checks rules that this script reconciles (marker in the rule
description). Changing a project's main domain simply renames the host:
old host deleted, new one created, secondary cert rules follow.

Config: /etc/paas-cmk-sync.conf  (KEY=VALUE)
  PANEL_URL=https://deploy.younex.de
  TOKEN=<CMK_AGENT_TOKEN>
  SITE=monitoring
  VIP=188.40.238.163
  EXTRA_DOMAINS=deploy.younex.de
"""
import json
import ssl
import sys
import urllib.request

CONF = "/etc/paas-cmk-sync.conf"
FOLDER = "paas_sites"
RULE_MARK = "paas-cert"   # description: "paas-cert <host> <domain>"


def load_conf():
    cfg = {}
    for line in open(CONF):
        line = line.strip()
        if line and not line.startswith("#") and "=" in line:
            k, v = line.split("=", 1)
            cfg[k] = v
    return cfg


def http(method, url, body=None, headers=None, ctx=None):
    req = urllib.request.Request(url, method=method,
                                 data=json.dumps(body).encode() if body is not None else None)
    req.add_header("Content-Type", "application/json")
    req.add_header("Accept", "application/json")
    for k, v in (headers or {}).items():
        req.add_header(k, v)
    try:
        with urllib.request.urlopen(req, context=ctx, timeout=30) as r:
            return r.status, r.read().decode()
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode()


def main():
    cfg = load_conf()
    secret = open(f"/omd/sites/{cfg['SITE']}/var/check_mk/web/automation/automation.secret").read().strip()
    api = f"http://localhost/{cfg['SITE']}/check_mk/api/1.0"
    auth = {"Authorization": f"Bearer automation {secret}"}

    # 1. desired state from the panel: {main_host: [secondary domains...]}
    st, body = http("GET", f"{cfg['PANEL_URL']}/cmk-sites",
                    headers={"Authorization": f"Bearer {cfg['TOKEN']}"},
                    ctx=ssl.create_default_context())
    if st != 200:
        print(f"panel unreachable: {st}", file=sys.stderr)
        sys.exit(1)
    desired = {}
    for site in json.loads(body).get("sites", []):
        domains = [d.strip().lower() for d in site.get("domains", []) if d.strip()]
        if not domains:
            continue
        main = str(site.get("main") or domains[0]).strip().lower()
        if main not in domains:
            main = domains[0]
        desired[main] = sorted(set(domains) - {main})
    for d in cfg.get("EXTRA_DOMAINS", "").split(","):
        d = d.strip().lower()
        if d:
            desired.setdefault(d, [])

    changed = False

    # 2. reconcile hosts in the folder
    st, body = http("GET", f"{api}/objects/folder_config/~{FOLDER}/collections/hosts", headers=auth)
    if st != 200:
        print(f"cannot list folder hosts ({st}): {body[:200]}", file=sys.stderr)
        sys.exit(1)
    current_hosts = {h["id"] for h in json.loads(body).get("value", [])}

    for host in sorted(set(desired) - current_hosts):
        st, body = http("POST", f"{api}/domain-types/host_config/collections/all", body={
            "folder": f"/{FOLDER}",
            "host_name": host,
            "attributes": {"ipaddress": cfg["VIP"], "tag_agent": "no-agent",
                           "alias": f"paas website {host}"},
        }, headers=auth)
        print(f"add host {host}: {st}")
        changed = changed or st == 200
    for host in sorted(current_hosts - set(desired)):
        st, body = http("DELETE", f"{api}/objects/host_config/{host}", headers=auth)
        print(f"remove host {host}: {st}")
        changed = changed or st == 204

    # 3. reconcile secondary-domain certificate rules (host-scoped)
    st, body = http("GET", f"{api}/domain-types/rule/collections/all?ruleset_name=custom_checks",
                    headers=auth)
    if st != 200:
        print(f"cannot list rules ({st}): {body[:200]}", file=sys.stderr)
        sys.exit(1)
    current_rules = {}   # (host, domain) -> rule_id
    for r in json.loads(body).get("value", []):
        desc = (((r.get("extensions") or {}).get("properties") or {}).get("description") or "")
        parts = desc.split()
        if len(parts) == 3 and parts[0] == RULE_MARK:
            current_rules[(parts[1], parts[2])] = r["id"]

    desired_rules = {(host, dom) for host, secs in desired.items() for dom in secs}

    for host, dom in sorted(desired_rules - set(current_rules)):
        value = ("{'service_description': 'Certificate %s', "
                 "'command_line': 'check_http -H %s -S --sni -C 21,7'}" % (dom, dom))
        st, body = http("POST", f"{api}/domain-types/rule/collections/all", body={
            "ruleset": "custom_checks",
            "folder": f"/{FOLDER}",
            "properties": {"description": f"{RULE_MARK} {host} {dom}"},
            "value_raw": value,
            "conditions": {"host_name": {"match_on": [host], "operator": "one_of"}},
        }, headers=auth)
        print(f"add cert rule {dom} on {host}: {st}")
        changed = changed or st == 200
    for (host, dom) in sorted(set(current_rules) - desired_rules):
        st, body = http("DELETE", f"{api}/objects/rule/{current_rules[(host, dom)]}", headers=auth)
        print(f"remove cert rule {dom} on {host}: {st}")
        changed = changed or st == 204

    if changed:
        st, body = http("POST", f"{api}/domain-types/activation_run/actions/activate-changes/invoke",
                        body={"redirect": False, "sites": [cfg["SITE"]], "force_foreign_changes": True},
                        headers={**auth, "If-Match": "*"})
        print(f"activate: {st}")
    else:
        print("in sync")


if __name__ == "__main__":
    main()
