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

Polle /api/pentest/vm/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-pentest/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-pentest/etc")
LOG_DIR = Path("/opt/sylink-pentest/log")
SUSPENDED_FLAG = Path("/var/lib/unisoc-pentest.suspended")
LICENSE_FILE = CONFIG_DIR / "license.json"
STATUS_CACHE = CONFIG_DIR / "license_status.json"
PT_API_BASE = os.environ.get("PT_API_BASE", "https://api.unisoc.fr")
CACHE_TTL_S = 30 * 60  # 30 min de tolérance UniSOC down

# Services à toggle on/off selon licence
PENTEST_SERVICES = [
    "sylink-pentest-agent", "sylink-pentest-network",
]


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/pentest/vm/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(PENTEST_SERVICES)} services leurres")
    for svc in PENTEST_SERVICES:
        subprocess.run(["systemctl", "--no-block", "stop", svc], timeout=10, capture_output=True)
    SUSPENDED_FLAG.parent.mkdir(parents=True, exist_ok=True)
    SUSPENDED_FLAG.write_text(f"{datetime.now(timezone.utc).isoformat()} {reason}\n")
    if reason != "no_license":
        # Banner maintenance explicite (sauf pour 'no_license' = VM pas activée, on garde le banner activation)
        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(PENTEST_SERVICES)} services leurres")
    for svc in PENTEST_SERVICES:
        subprocess.run(["systemctl", "--no-block", "start", svc], timeout=10, capture_output=True)
    if SUSPENDED_FLAG.exists():
        SUSPENDED_FLAG.unlink()
    subprocess.run(["/opt/sylink-pentest/bin/init.py"], timeout=15, capture_output=True)


# ──────────────────────────────────────────────────────────────────────────
# Pending actions admin (reboot / update / network) — déclenchées via portail
# La VM persiste le timestamp du dernier traitement dans action_state.json
# pour éviter de re-exécuter une action déjà appliquée.
# ──────────────────────────────────────────────────────────────────────────

ACTION_STATE = CONFIG_DIR / "action_state.json"


def load_action_state() -> dict:
    if ACTION_STATE.exists():
        try:
            return json.loads(ACTION_STATE.read_text())
        except Exception:
            pass
    return {}


def save_action_state(s: dict):
    ACTION_STATE.write_text(json.dumps(s, indent=2))


# ─── system events log (boot, reboot, network change, update applied) ───
SYS_EVENTS = Path("/opt/sylink-pentest/log/system_events.json")


def emit_system_event(kind: str, **details):
    """Émet un event système (action admin / changement état VM) dans system_events.json
    → log_forwarder.py le push vers honeypot_events backend → visible dans la page détail."""
    try:
        SYS_EVENTS.parent.mkdir(parents=True, exist_ok=True)
        e = {
            "ts": datetime.now(timezone.utc).isoformat(),
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "eventid": f"vm.{kind}",
            "source": "vm-system",
            **details,
        }
        with SYS_EVENTS.open("a") as f:
            f.write(json.dumps(e) + "\n")
    except Exception:
        pass


def process_pending_actions(actions: dict):
    """Exécute les actions admin pending (cf. license/verify response)."""
    if not actions:
        return
    state = load_action_state()

    # 1. Network config — si requested_at différent du last_processed
    nc_at = actions.get("network_config_requested_at")
    if nc_at and nc_at != state.get("network_processed_at"):
        nc = actions.get("network_config")
        if nc:
            log(f"=> ACTION network_config : {nc}")
            try:
                import importlib.util as _u
                spec = _u.spec_from_file_location("poll_mod", "/opt/sylink-pentest/bin/poll.py")
                mod = _u.module_from_spec(spec); spec.loader.exec_module(mod)
                mod.apply_network_config(nc)
                state["network_processed_at"] = nc_at
                save_action_state(state)
                emit_system_event("network_change", mode=nc.get("mode"),
                                  ip_cidr=nc.get("ip_cidr"), gateway=nc.get("gateway"))
            except Exception as e:
                log(f"   network apply échec : {e}")
                emit_system_event("network_change_failed", error=str(e))

    # 2. Update — via systemd-run pour survivre à la fin de ce service oneshot
    up_at = actions.get("update_requested_at")
    if up_at and up_at != state.get("update_processed_at"):
        log(f"=> ACTION update demandée (via systemd-run)")
        try:
            subprocess.run([
                "systemd-run", "--on-active=3s", "--unit=unisoc-pentest-update.service",
                "/opt/sylink-pentest/bin/self_update.py", "--now",
            ], capture_output=True, timeout=8)
            state["update_processed_at"] = up_at
            save_action_state(state)
            emit_system_event("update_requested",
                              target_version=actions.get("update_target_version"))
        except Exception as e:
            log(f"   update échec : {e}")

    # 3. Reboot — via systemd-run
    rb_at = actions.get("reboot_requested_at")
    if rb_at and rb_at != state.get("reboot_processed_at"):
        log(f"=> ACTION reboot demandée — exécution dans 5 s (via systemd-run)")
        state["reboot_processed_at"] = rb_at
        save_action_state(state)
        emit_system_event("reboot_requested")
        subprocess.run([
            "systemd-run", "--on-active=5s", "--unit=unisoc-pentest-reboot.service",
            "/bin/systemctl", "reboot"
        ], capture_output=True, timeout=8)


def emit_boot_event_once():
    """Émet 1 event vm.boot par boot — uses /proc/uptime pour détecter le boot récent
    et un marqueur /run pour éviter le double-fire (re-tick license_check même boot)."""
    marker = Path("/run/unisoc-pentest.boot_event_emitted")
    if marker.exists():
        return
    try:
        with open("/proc/uptime") as f:
            uptime_s = float(f.read().split()[0])
        emit_system_event("boot", uptime_at_emit_s=int(uptime_s))
        marker.touch()
    except Exception:
        pass


def main() -> int:
    log("== license_check start ==")
    emit_boot_event_once()
    token = read_license_token()
    if not token:
        # VM pas activée → s'assurer que les services leurres sont OFF
        # ET que setup-ui (port 8080 pour activation) est UP
        if not SUSPENDED_FLAG.exists():
            stop_services("no_license")
        else:
            log("license.json absent et déjà suspended — rien à faire")
        subprocess.run(["systemctl", "--no-block", "enable", "--now", "sylink-pentest-setup-ui"],
                       capture_output=True, timeout=10)
        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")
        # Pending actions admin déclenchées via portail
        process_pending_actions(result.get("pending_actions") or {})
    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())
