#!/usr/bin/env python3
"""Provision the services used by Daneshyar online classrooms.

Run this *on the web/media server* as root after the application is deployed:

    sudo python3 scripts/classroom_server.py install \
      --app-dir /var/www/daneshyar --domain mydaneshyar.ir

It is idempotent. Re-running it repairs the Reverb/queue services, Apache
WebSocket proxy, coturn relay, environment values, health timer, and database
media-server registration without changing room URLs or application data.
"""

from __future__ import annotations

import argparse
import os
import secrets
import shutil
import subprocess
import sys
from pathlib import Path


def run(command: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
    print("  ->", " ".join(command))
    result = subprocess.run(command, text=True, capture_output=True, check=False)
    if result.stdout.strip():
        print(result.stdout.strip())
    if result.stderr.strip() and result.returncode != 0:
        print(result.stderr.strip(), file=sys.stderr)
    if check and result.returncode != 0:
        raise SystemExit(result.returncode)
    return result


def require_root() -> None:
    if os.name != "nt" and os.geteuid() != 0:
        raise SystemExit("Run this installer as root (sudo).")


def read_env(path: Path) -> tuple[list[str], dict[str, str]]:
    if not path.exists():
        raise SystemExit(f"Missing {path}; deploy the app and create .env first.")
    lines = path.read_text(encoding="utf-8").splitlines()
    values: dict[str, str] = {}
    for raw in lines:
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        values[key] = value.strip().strip('"').strip("'")
    return lines, values


def update_env(path: Path, updates: dict[str, str]) -> None:
    lines, _ = read_env(path)
    pending = dict(updates)
    output: list[str] = []
    for line in lines:
        stripped = line.strip()
        if stripped and not stripped.startswith("#") and "=" in stripped:
            key = stripped.split("=", 1)[0]
            if key in pending:
                output.append(f"{key}={pending.pop(key)}")
                continue
        output.append(line)
    if pending:
        output.extend(["", "# Online classroom realtime/media services"])
        output.extend(f"{key}={value}" for key, value in pending.items())
    path.write_text("\n".join(output).rstrip() + "\n", encoding="utf-8")


def write(path: Path, content: str, mode: int = 0o644) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content.strip() + "\n", encoding="utf-8")
    path.chmod(mode)
    print(f"  wrote {path}")


def install_packages() -> None:
    run(["apt-get", "update", "-qq"])
    run(["apt-get", "install", "-y", "-qq", "coturn", "redis-server", "python3-pymysql"])
    run(["systemctl", "enable", "--now", "redis-server"])


def configure_coturn(domain: str, port: int, secret: str, public_ip: str | None) -> None:
    external = f"external-ip={public_ip}\n" if public_ip else ""
    config_path = Path("/etc/turnserver.conf")
    write(config_path, f"""
listening-port={port}
fingerprint
use-auth-secret
static-auth-secret={secret}
realm={domain}
total-quota=300
stale-nonce=600
min-port=49152
max-port=65535
no-multicast-peers
no-cli
{external}denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
""", mode=0o640)
    try:
        shutil.chown(config_path, user="root", group="turnserver")
    except LookupError:
        # Some distributions run coturn as root and do not create this group.
        pass
    write(Path("/etc/systemd/system/coturn.service.d/daneshyar.conf"), """
[Service]
Restart=always
RestartSec=3
LimitNOFILE=65535
""")
    default = Path("/etc/default/coturn")
    if default.exists():
        text = default.read_text(encoding="utf-8")
        text = text.replace("#TURNSERVER_ENABLED=1", "TURNSERVER_ENABLED=1")
        if not any(line.strip() == "TURNSERVER_ENABLED=1" for line in text.splitlines()):
            text += "\nTURNSERVER_ENABLED=1\n"
        default.write_text(text, encoding="utf-8")
    run(["systemctl", "daemon-reload"])
    run(["systemctl", "enable", "--now", "coturn"])
    run(["systemctl", "restart", "coturn"])
    if shutil.which("ufw"):
        for rule in (f"{port}/udp", f"{port}/tcp", "49152:65535/udp"):
            run(["ufw", "allow", rule], check=False)


def configure_apache(reverb_port: int) -> None:
    if not shutil.which("a2enmod"):
        print("  Apache not installed; skipping reverse proxy configuration.")
        return
    run(["a2enmod", "proxy", "proxy_http", "proxy_wstunnel", "rewrite"])
    enabled_sites = Path("/etc/apache2/sites-enabled")
    if enabled_sites.exists() and any("ProxyPass /app " in path.read_text(encoding="utf-8", errors="ignore") for path in enabled_sites.glob("*.conf")):
        print("  active virtual host already proxies /app and /apps; keeping it")
        # Avoid duplicate proxy directives if an older installer run enabled
        # the global fallback before the virtual host gained its own rules.
        run(["a2disconf", "daneshyar-realtime"], check=False)
        run(["apache2ctl", "configtest"])
        run(["systemctl", "reload", "apache2"])
        return
    write(Path("/etc/apache2/conf-available/daneshyar-realtime.conf"), f"""
# Generated by scripts/classroom_server.py
ProxyPreserveHost On
ProxyTimeout 3600
ProxyPass /apps http://127.0.0.1:{reverb_port}/apps retry=0 timeout=60
ProxyPassReverse /apps http://127.0.0.1:{reverb_port}/apps
ProxyPass /app ws://127.0.0.1:{reverb_port}/app retry=0 timeout=3600
ProxyPassReverse /app ws://127.0.0.1:{reverb_port}/app
""")
    run(["a2enconf", "daneshyar-realtime"])
    run(["apache2ctl", "configtest"])
    run(["systemctl", "reload", "apache2"])


def configure_services(app_dir: Path, php: str, reverb_port: int) -> None:
    write(Path("/etc/systemd/system/laravel-reverb.service"), f"""
[Unit]
Description=Daneshyar Laravel Reverb
After=network.target redis-server.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory={app_dir}
ExecStart=/usr/bin/{php} artisan reverb:start --host=127.0.0.1 --port={reverb_port} --no-interaction
ExecReload=/usr/bin/{php} artisan reverb:restart
Restart=always
RestartSec=3
KillSignal=SIGTERM
TimeoutStopSec=20
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
""")
    write(Path("/etc/systemd/system/daneshyar-queue.service"), f"""
[Unit]
Description=Daneshyar Queue Worker
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory={app_dir}
ExecStart=/usr/bin/{php} artisan queue:work --sleep=1 --tries=3 --timeout=1000 --max-time=3600
Restart=always
RestartSec=3
TimeoutStopSec=1050

[Install]
WantedBy=multi-user.target
""")
    write(Path("/etc/systemd/system/daneshyar-media-health.service"), f"""
[Unit]
Description=Daneshyar media server health and load refresh
After=network.target

[Service]
Type=oneshot
WorkingDirectory={app_dir}
ExecStart=/usr/bin/python3 {app_dir}/scripts/media_servers.py health
""")
    write(Path("/etc/systemd/system/daneshyar-media-health.timer"), """
[Unit]
Description=Check Daneshyar media servers every five seconds

[Timer]
OnBootSec=5
OnUnitActiveSec=5
AccuracySec=1
Persistent=true

[Install]
WantedBy=timers.target
""")
    run(["systemctl", "daemon-reload"])
    run(["systemctl", "enable", "--now", "laravel-reverb", "daneshyar-queue", "daneshyar-media-health.timer"])
    run(["systemctl", "restart", "laravel-reverb", "daneshyar-queue"])


def install(args: argparse.Namespace) -> None:
    require_root()
    app_dir = Path(os.path.abspath(args.app_dir))
    env_path = app_dir / ".env"
    _, current = read_env(env_path)
    # The PC deployer invokes this script by absolute path from the SSH user's
    # home directory. Artisan and Composer commands must still run in the app.
    os.chdir(app_dir)

    reverb_key = current.get("REVERB_APP_KEY") or current.get("VITE_REVERB_APP_KEY") or secrets.token_hex(16)
    reverb_secret = current.get("REVERB_APP_SECRET") or secrets.token_hex(32)
    reverb_id = current.get("REVERB_APP_ID") or secrets.token_hex(8)
    turn_secret = current.get("TURN_SECRET") or secrets.token_hex(32)
    queue_connection = current.get("QUEUE_CONNECTION", "database")
    if queue_connection not in {"database", "redis"}:
        queue_connection = "database"

    print("[1/7] Installing coturn, Redis, and health-check dependencies")
    install_packages()
    print("[2/7] Writing runtime environment")
    update_env(env_path, {
        "BROADCAST_CONNECTION": "reverb",
        "QUEUE_CONNECTION": queue_connection,
        "DB_QUEUE_RETRY_AFTER": "1200",
        "REDIS_QUEUE_RETRY_AFTER": "1200",
        "REVERB_APP_ID": reverb_id,
        "REVERB_APP_KEY": reverb_key,
        "REVERB_APP_SECRET": reverb_secret,
        "REVERB_HOST": "127.0.0.1",
        "REVERB_PORT": str(args.reverb_port),
        "REVERB_SCHEME": "http",
        "REVERB_SERVER_HOST": "127.0.0.1",
        "REVERB_SERVER_PORT": str(args.reverb_port),
        "REVERB_ALLOWED_ORIGINS": f"{args.domain},www.{args.domain}",
        "VITE_REVERB_APP_KEY": reverb_key,
        "TURN_HOST": args.domain,
        "TURN_PORT": str(args.turn_port),
        "TURN_SECRET": turn_secret,
        "TURN_CREDENTIAL_TTL": "3600",
    })
    env_path.chmod(0o640)

    print("[3/7] Configuring coturn")
    configure_coturn(args.domain, args.turn_port, turn_secret, args.public_ip)
    print("[4/7] Configuring Apache WebSocket proxy")
    configure_apache(args.reverb_port)
    print("[5/7] Running classroom migrations and registering this relay")
    artisan = [f"/usr/bin/{args.php}", "artisan"]
    run([*artisan, "migrate", "--force"], check=True)
    run([*artisan, "config:clear"])
    run([sys.executable, str(app_dir / "scripts/media_servers.py"), "migrate"])
    print("[6/7] Installing long-running services")
    configure_services(app_dir, args.php, args.reverb_port)
    print("[7/7] Caching config and checking status")
    run([*artisan, "config:cache"])
    run([sys.executable, str(app_dir / "scripts/media_servers.py"), "health"], check=False)
    status(args)
    print("\nOnline classroom services are ready. Re-run this command after changing the domain or ports.")


def status(args: argparse.Namespace) -> None:
    units = ["coturn", "redis-server", "laravel-reverb", "daneshyar-queue", "daneshyar-media-health.timer"]
    print("\nService status:")
    for unit in units:
        result = run(["systemctl", "is-active", unit], check=False)
        mark = "OK" if result.returncode == 0 else "DOWN"
        print(f"  [{mark:4}] {unit}")
    app_dir = Path(os.path.abspath(args.app_dir))
    manager = app_dir / "scripts/media_servers.py"
    if manager.exists():
        run([sys.executable, str(manager), "list"], check=False)


def main() -> None:
    parser = argparse.ArgumentParser(description="Provision Daneshyar classroom realtime/media services")
    sub = parser.add_subparsers(dest="command", required=True)
    for name in ("install", "status"):
        command = sub.add_parser(name)
        command.add_argument("--app-dir", default="/var/www/daneshyar")
        command.add_argument("--domain", default="mydaneshyar.ir")
        command.add_argument("--php", default="php8.3")
        command.add_argument("--reverb-port", type=int, default=8081)
        command.add_argument("--turn-port", type=int, default=3478)
        command.add_argument("--public-ip", default=None, help="Required only when coturn is behind one-to-one NAT")
        command.set_defaults(fn=install if name == "install" else status)
    args = parser.parse_args()
    args.fn(args)


if __name__ == "__main__":
    main()
