#!/usr/bin/env python3
"""Deploy webhook — signed, allowlisted factory actions plus audit log."""
import hashlib
import hmac
import http.server
import json
import os
import re
import subprocess
import time
from datetime import datetime, timezone
from pathlib import Path

PORT = int(os.environ.get("DEPLOY_WEBHOOK_PORT", "8098"))
AUTH_TOKEN = os.environ.get("DEPLOY_TOKEN", "factory-deploy-token-2026")
REQUIRE_SIGNATURE = os.environ.get("DEPLOY_REQUIRE_SIGNATURE", "1") != "0"
AUDIT_LOG = Path(os.environ.get("DEPLOY_AUDIT_LOG", "deploy_audit.jsonl"))
MAX_SKEW_SECONDS = int(os.environ.get("DEPLOY_MAX_SKEW_SECONDS", "300"))
SAFE_NAME = re.compile(r"^[a-zA-Z0-9_.-]+$")
ALLOWED_CONTAINERS = {"hermes", "fullyhermes", "hermes-voice", "openclaw", "scanner", "deploy-webhook"}
ALLOWED_ACTIONS = {
    "run_skill_tests",
    "patch_skill_from_pr",
    "restart_container",
    "rollback_container",
    "disable_skill",
    "enable_skill",
}


def audit(event):
    event = {"ts": datetime.now(timezone.utc).isoformat(), **event}
    AUDIT_LOG.parent.mkdir(parents=True, exist_ok=True)
    with AUDIT_LOG.open("a", encoding="utf-8") as f:
        f.write(json.dumps(event, ensure_ascii=False) + "\n")


def _signed_payload(data):
    keep = {k: data.get(k) for k in ("timestamp", "nonce", "actor", "action", "target", "payload_hash")}
    return json.dumps(keep, sort_keys=True, separators=(",", ":")).encode()


def expected_signature(data):
    return hmac.new(AUTH_TOKEN.encode(), _signed_payload(data), hashlib.sha256).hexdigest()


def verify_signature(data):
    if not REQUIRE_SIGNATURE:
        return True, "signature disabled"
    try:
        ts = int(data.get("timestamp", 0))
    except (TypeError, ValueError):
        return False, "bad timestamp"
    if abs(int(time.time()) - ts) > MAX_SKEW_SECONDS:
        return False, "stale timestamp"
    got = str(data.get("signature", ""))
    if not hmac.compare_digest(got, expected_signature(data)):
        return False, "bad signature"
    return True, "ok"


def safe_name(value):
    value = str(value or "")
    return bool(SAFE_NAME.fullmatch(value) and value not in {".", ".."} and ".." not in value.split("."))


def command_for(data):
    action = data.get("action")
    target = str(data.get("target", ""))
    if action not in ALLOWED_ACTIONS:
        raise ValueError("action not allowed")
    if not safe_name(target):
        raise ValueError("unsafe target")

    if action == "run_skill_tests":
        return f"cd /opt/data/skills/{target} && bash test.sh"
    if action == "restart_container":
        if target not in ALLOWED_CONTAINERS:
            raise ValueError("container not allowed")
        return f"docker restart {target}"
    if action == "rollback_container":
        if target not in ALLOWED_CONTAINERS:
            raise ValueError("container not allowed")
        return f"echo rollback requested for {target}"
    if action in {"disable_skill", "enable_skill"}:
        return f"echo {action} {target}"
    if action == "patch_skill_from_pr":
        payload_hash = str(data.get("payload_hash", ""))
        if not safe_name(payload_hash):
            raise ValueError("unsafe payload hash")
        return f"echo patch_skill_from_pr {target} {payload_hash}"
    raise ValueError("unhandled action")


class DeployHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/dynamic-status":
            self.send_json(200, self.get_dynamic_status())
        else:
            self.send_json(200, {"status": "ok", "service": "deploy-webhook", "phase": 4, "actions": sorted(ALLOWED_ACTIONS)})

    def do_POST(self):
        auth = self.headers.get("Authorization", "")
        if auth != f"Bearer {AUTH_TOKEN}":
            audit({"event": "reject", "reason": "unauthorized", "client": self.client_address[0]})
            self.send_json(401, {"error": "unauthorized"})
            return

        try:
            data = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
        except Exception:
            self.send_json(400, {"error": "invalid json"})
            return

        ok, reason = verify_signature(data)
        action = data.get("action")
        target = data.get("target")
        if not ok:
            audit({"event": "reject", "reason": reason, "action": action, "target": target})
            self.send_json(403, {"error": reason})
            return

        try:
            cmd = command_for(data)
        except ValueError as e:
            audit({"event": "reject", "reason": str(e), "action": action, "target": target})
            self.send_json(403, {"error": str(e), "allowed_actions": sorted(ALLOWED_ACTIONS)})
            return

        cmd_hash = hashlib.sha256(cmd.encode()).hexdigest()[:16]
        audit({"event": "execute", "actor": data.get("actor"), "action": action, "target": target, "cmd_hash": cmd_hash})
        try:
            result = subprocess.run(["ssh", "engine", cmd], capture_output=True, text=True, timeout=60)
            audit({"event": "result", "action": action, "target": target, "cmd_hash": cmd_hash, "exit_code": result.returncode})
            self.send_json(200, {"exit_code": result.returncode, "stdout": result.stdout[-1000:], "stderr": result.stderr[-500:]})
        except subprocess.TimeoutExpired:
            audit({"event": "timeout", "action": action, "target": target, "cmd_hash": cmd_hash})
            self.send_json(504, {"error": "command timed out"})
        except Exception as e:
            audit({"event": "error", "action": action, "target": target, "cmd_hash": cmd_hash, "error": str(e)})
            self.send_json(500, {"error": str(e)})

    def get_dynamic_status(self):
        try:
            r = subprocess.run(["ssh", "engine", "docker ps -q | wc -l"], capture_output=True, text=True, timeout=10)
            count = int(r.stdout.strip()) if r.stdout.strip() else 25
        except Exception:
            count = 25
        return {"status": "ok", "phase": 4, "containers": count, "agents": 6, "infra": 14, "monitoring": 5, "skills": 63}

    def send_json(self, code, data):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(data).encode())

    def log_message(self, *_args):
        return


if __name__ == "__main__":
    http.server.HTTPServer(("0.0.0.0", PORT), DeployHandler).serve_forever()
