#!/usr/bin/env python3
"""
SYLink HoneyBot — License watcher.

Polle /api/honeypot/license/verify toutes les 5 min. Si licence revoked/suspended
côté UniSOC → stop tous les services leurres (la VM devient inerte).
Quand la licence redevient valide → redémarre les services.

Mode "résilient" : si UniSOC injoignable, on garde la dernière décision connue
(cache /opt/sylink-honeypot/etc/license_status.json TTL 30 min) — pas de flap.
"""
from __future__ import annotations

import json
import os
import subprocess
import sys
import time
import urllib.request
from datetime import datetime, timezone, timedelta
from pathlib import Path

CONFIG_DIR = Path("/opt/sylink-honeypot/etc")
LOG_DIR = Path("/opt/sylink-honeypot/log")
SUSPENDED_FLAG = Path("/var/lib/unisoc-honeypot.suspended")
LICENSE_FILE = CONFIG_DIR / "license.json"
STATUS_CACHE = CONFIG_DIR / "license_status.json"
PT_API_BASE = os.environ.get("HP_API_BASE", "https://api.unisoc.fr")
CACHE_TTL_S = 30 * 60  # 30 min de tolérance UniSOC down

# Services à toggle on/off selon licence
LEURRE_SERVICES = [
    "cowrie", "opencanary", "veeam-fake", "proftpd-fake",
    "ssh-tarpit", "file-watcher", "rdp-recorder", "http-honeytrap",
    "smbd", "nmbd", "xrdp", "xrdp-sesman",
]


def log(msg: str):
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    line = f"[{datetime.now(timezone.utc).isoformat()}] {msg}"
    print(line, flush=True)
    with (LOG_DIR / "license_check.log").open("a") as f:
        f.write(line + "\n")


def read_license_token() -> str | None:
    if not LICENSE_FILE.exists():
        return None
    try:
        return json.loads(LICENSE_FILE.read_text()).get("license_token")
    except Exception:
        return None


def verify_remote(license_token: str) -> dict | None:
    url = f"{PT_API_BASE}/api/honeypot/license/verify"
    req = urllib.request.Request(
        url, method="POST",
        headers={"Authorization": f"Bearer {license_token}", "Content-Type": "application/json"},
        data=b"{}",
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            return json.loads(resp.read())
    except Exception as e:
        log(f"verify HTTP erreur : {e}")
        return None


def read_cache() -> dict | None:
    if not STATUS_CACHE.exists():
        return None
    try:
        c = json.loads(STATUS_CACHE.read_text())
        ts = datetime.fromisoformat(c.get("cached_at", "").replace("Z", "+00:00"))
        if (datetime.now(timezone.utc) - ts).total_seconds() > CACHE_TTL_S:
            return None
        return c
    except Exception:
        return None


def write_cache(result: dict):
    result["cached_at"] = datetime.now(timezone.utc).isoformat()
    STATUS_CACHE.write_text(json.dumps(result, indent=2))


def stop_services(reason: str):
    log(f"=> SUSPENSION ({reason}) — stop des {len(LEURRE_SERVICES)} services leurres")
    for svc in LEURRE_SERVICES:
        subprocess.run(["systemctl", "stop", svc], timeout=20, capture_output=True)
    SUSPENDED_FLAG.parent.mkdir(parents=True, exist_ok=True)
    SUSPENDED_FLAG.write_text(f"{datetime.now(timezone.utc).isoformat()} {reason}\n")
    # Banner explicite côté console
    try:
        Path("/etc/issue").write_text(
            "\n"
            "===================================================================\n"
            "  Corp Backup Infrastructure - MAINTENANCE MODE\n"
            "===================================================================\n"
            f"\n  System temporarily unavailable.\n"
            f"  Reason: license suspended ({reason})\n"
            f"  Contact your SOC administrator.\n\n"
        )
    except Exception:
        pass


def start_services():
    log(f"=> REPRISE — démarrage des {len(LEURRE_SERVICES)} services leurres")
    for svc in LEURRE_SERVICES:
        subprocess.run(["systemctl", "start", svc], timeout=20, capture_output=True)
    if SUSPENDED_FLAG.exists():
        SUSPENDED_FLAG.unlink()
    # Re-pose le banner couverture (via init.py)
    subprocess.run(["/opt/sylink-honeypot/bin/init.py"], timeout=15, capture_output=True)


def main() -> int:
    log("== license_check start ==")
    token = read_license_token()
    if not token:
        log("license.json absent — VM pas activée, rien à faire")
        return 0

    result = verify_remote(token)
    if result is None:
        cached = read_cache()
        if cached:
            log(f"UniSOC injoignable → utilise cache (valid={cached.get('valid')}, age cache)")
            result = cached
        else:
            log("UniSOC injoignable + cache expiré — décision : maintenir l'état actuel (fail-open)")
            return 0

    write_cache(result)
    suspended = SUSPENDED_FLAG.exists()

    if result.get("valid"):
        if suspended:
            log("licence redevenue valide → reprise")
            start_services()
        else:
            log("licence valide, services nominaux — rien à faire")
    else:
        reason = result.get("reason", "unknown")
        if not suspended:
            stop_services(reason)
        else:
            log(f"licence toujours invalide ({reason}) — services restent stoppés")

    log("== license_check done ==")
    return 0


if __name__ == "__main__":
    sys.exit(main())
