#!/usr/bin/python3
# SPDX-License-Identifier: AGPL-3.0-only
# SPDX-FileCopyrightText: 2025 Univention GmbH

import grp
import json
import os
import pwd
import secrets
import stat
import string
import subprocess
import sys

from univention.config_registry import ConfigRegistry
from univention.config_registry.frontend import ucr_update

APPCENTER_PATH = "/var/lib/univention-appcenter/apps/provisioning-service/"

NATS_CONF = """{{
  server_name: nats
  pid_file: "/nats.pid"
  port: 4222
  http_port: 8222
  lame_duck_duration: 30s
  lame_duck_grace_period: 10s
  jetstream {{
    max_file_store: 1Gi
    max_memory_store: 256Mi
    store_dir: "/data"
  }}

  authorization {{
    users: [
      {{
        user: api
        password: {NATS_PASSWORD}
        permissions: {{
          publish: '>'
          subscribe: '>'
        }}
      }}
      {{
        user: dispatcher
        password: {NATS_PASSWORD}
        permissions: {{
          publish: '>'
          subscribe: '>'
        }}
      }}
      {{
        user: udm-transformer
        password: {NATS_PASSWORD}
        permissions: {{
          publish: '>'
          subscribe: '>'
        }}
      }}
      {{
        user: listener-module
        password: {NATS_PASSWORD}
        permissions: {{
          publish: '>'
          subscribe: '>'
        }}
      }}
      {{
        user: prefill
        password: {NATS_PASSWORD}
        permissions: {{
          publish: '>'
          subscribe: '>'
        }}
      }}
    ]
  }}
}}
"""

SECRETS_FILE = "/etc/provisioning-secrets.json"
SECRETS_KEYS = ["NATS_PASSWORD", "EVENTS_PASSWORD_UDM", "PROVISIONING_API_ADMIN_PASSWORD"]
LISTENER_SECRETS_FILE = "/etc/nats/provisioning-listener.secret"

ucr = ConfigRegistry()
ucr.load()


def _alphanum_token(length=32):
    return "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(length))


def _load_secrets(filepath):
    if os.path.isfile(filepath):
        try:
            with open(filepath, "r") as f:
                return json.load(f)
        except json.JSONDecodeError:
            print(f"Warning: {filepath} is not valid JSON.", file=sys.stderr)
    return {}


def _generate_missing_secrets(existing_secrets, required_keys):
    """Generate random secrets for missing keys"""
    updated = False
    for key in required_keys:
        if key not in existing_secrets:
            existing_secrets[key] = _alphanum_token(32)
            updated = True
            print(f"Generated new secret for: {key}")
    return updated, existing_secrets


def setup_listener():
    ucr_update(ucr, {"nats/user": "listener-module"})

    os.makedirs(os.path.dirname(LISTENER_SECRETS_FILE), exist_ok=True)
    with open(SECRETS_FILE) as f:
        listener_password = json.load(f)["NATS_PASSWORD"]

        with open(LISTENER_SECRETS_FILE, "w") as fl:
            fl.write(listener_password)

        user_info = pwd.getpwnam("listener")
        uid = user_info.pw_uid
        os.chown(LISTENER_SECRETS_FILE, uid, 0)
        os.chmod(LISTENER_SECRETS_FILE, stat.S_IRUSR | stat.S_IWUSR)


def create_nats_conf():
    os.makedirs(os.path.join(APPCENTER_PATH, "conf/"), exist_ok=True)
    secrets = _load_secrets(SECRETS_FILE)
    nats_config_file = os.path.join(APPCENTER_PATH, "conf/", "nats.conf")

    with open(nats_config_file, "w") as f:
        f.write(NATS_CONF.format(NATS_PASSWORD=secrets["NATS_PASSWORD"]))

    os.chown(nats_config_file, 0, 0)
    os.chmod(nats_config_file, stat.S_IRUSR | stat.S_IWUSR)
    print(f"nats.conf file written to {nats_config_file}")


def create_secrets():
    secrets_data = _load_secrets(SECRETS_FILE)
    updated, secrets_data = _generate_missing_secrets(secrets_data, SECRETS_KEYS)

    if updated:
        with open(SECRETS_FILE, "w") as f:
            json.dump(secrets_data, f, indent=2)
    else:
        print(f"All secrets already present in {SECRETS_FILE}")

    print(f"Setting permissions for {SECRETS_FILE}")

    group_info = grp.getgrnam("DC Backup Hosts")
    gid = group_info.gr_gid
    os.chown(SECRETS_FILE, 0, gid)
    os.chmod(SECRETS_FILE, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)


def get_secrets_from_primary():
    """
    For now, we decided to simply sync the secrets file from the Primary to the Backup.
    """
    machine_secret = "/etc/machine.secret"
    hostname = ucr.get("hostname")
    primary = ucr.get("ldap/master")

    username = f"{hostname}$"
    cmd = ["univention-scp", machine_secret, f"{username}@{primary}:{SECRETS_FILE} {SECRETS_FILE}"]
    try:
        subprocess.check_output(cmd, universal_newlines=True)
        print("Successfully copied secrets from primary")
    except subprocess.CalledProcessError as e:
        print(f"Failed to retrieve secrets from primary: {e}")
        sys.exit(1)


def check_app_installed_on_primary():
    """
    The app must be installed on the Primary Directory Node first.
    """
    try:
        p = subprocess.run(["univention-app", "domain"], capture_output=True, check=True, text=True, timeout=300)
    except subprocess.CalledProcessError as e:
        print(f"ERROR: `univention-app domain` command failed (exit {e.returncode}).\n{e.stderr}")
        sys.exit(1)
    except subprocess.TimeoutExpired as e:
        print(
            "ERROR: `univention-app domain` did not return within 20 seconds. Could not verify that "
            f"Provisioning Service app is installed on Primary Directory Node. Aborting..\n{e}"
        )
        sys.exit(1)

    for line in p.stdout.splitlines():
        if "provisioning-service=" in line and "provisioning-service-backend=" in line:
            return

    print("ERROR: Provisioning Service app not installed on Primary Directory Node. Aborting..")
    sys.exit(1)


if __name__ == "__main__":
    if ucr.get("server/role") == "domaincontroller_backup":
        check_app_installed_on_primary()
        get_secrets_from_primary()
    create_secrets()
    create_nats_conf()
    setup_listener()
