#!/usr/bin/env python3
"""hermes-exec — exécuteur d'actions Hermes pour l'assistant vocal STT.

Petit service HTTP (tourne en utilisateur `hermes` sur storage-01) qui lance le vrai
agent Hermes en one-shot : `hermes -z "<prompt>" --yolo` (profil par défaut, tous ses
outils). Appelé par le STT-server **après** confirmation vocale de l'utilisateur.

Sécurité :
  • Jeton d'auth OBLIGATOIRE (Bearer) — comparaison à temps constant. Sans lui, refus.
    C'est ce qui empêche n'importe quel client du cluster de déclencher l'agent.
  • `--yolo` (auto-exécution) n'est atteint qu'avec un jeton valide ; la confirmation
    qui protège l'utilisateur des erreurs de transcription est faite EN AMONT (STT-server).
  • Audit : chaque requête (prompt + sortie) est journalisée.

Endpoints :
  GET  /health        → {"ok": true}            (sans auth)
  POST /exec {prompt}  → {"output": …}           (Bearer requis)

Config via variables d'environnement (cf. rôle Ansible hermes_exec) :
  HERMES_EXEC_TOKEN   jeton attendu (obligatoire ; vide → service refuse tout)
  HERMES_EXEC_PORT    port d'écoute (défaut 9096)
  HERMES_BIN          chemin du binaire hermes (défaut "hermes")
  HERMES_EXEC_TIMEOUT timeout d'une action en secondes (défaut 120)
"""

import hmac
import json
import os
import subprocess
import threading
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

TOKEN = os.getenv("HERMES_EXEC_TOKEN", "")
PORT = int(os.getenv("HERMES_EXEC_PORT", "9096"))
HERMES_BIN = os.getenv("HERMES_BIN", "hermes")
TIMEOUT = int(os.getenv("HERMES_EXEC_TIMEOUT", "120"))
LOG_FILE = Path(os.getenv("HERMES_EXEC_LOG", "/var/log/hermes-exec.log"))

# Une action à la fois : l'agent + ses outils ne doivent pas s'entrelacer.
_lock = threading.Lock()


def _audit(prompt: str, status: str, output: str) -> None:
    try:
        LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(LOG_FILE, "a") as f:
            f.write(f"\n=== {datetime.now(timezone.utc).isoformat()} [{status}] ===\n")
            f.write(f"PROMPT: {prompt}\n")
            f.write(f"OUTPUT: {output[:4000]}\n")
    except Exception:  # noqa: BLE001 — l'audit ne doit jamais casser l'exécution
        pass


def run_hermes(prompt: str) -> dict:
    """Lance `hermes -z "<prompt>" --yolo` et renvoie {ok, output}."""
    if not _lock.acquire(blocking=False):
        return {"ok": False, "output": "Une action est déjà en cours, réessaie dans un instant."}
    try:
        proc = subprocess.run(
            [HERMES_BIN, "-z", prompt, "--yolo"],
            capture_output=True, text=True, timeout=TIMEOUT,
        )
        out = (proc.stdout or "").strip() or (proc.stderr or "").strip()
        _audit(prompt, "ok" if proc.returncode == 0 else f"rc={proc.returncode}", out)
        return {"ok": proc.returncode == 0, "output": out or "(aucune sortie)"}
    except subprocess.TimeoutExpired:
        _audit(prompt, "timeout", "")
        return {"ok": False, "output": f"Action interrompue (dépassement de {TIMEOUT}s)."}
    except Exception as e:  # noqa: BLE001
        _audit(prompt, "error", str(e))
        return {"ok": False, "output": f"Erreur d'exécution : {e}"}
    finally:
        _lock.release()


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *a):  # silence le log HTTP par requête
        pass

    def _json(self, code: int, data: dict) -> None:
        body = json.dumps(data, ensure_ascii=False).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _authorized(self) -> bool:
        if not TOKEN:  # pas de jeton configuré → service verrouillé (refuse tout)
            return False
        auth = self.headers.get("Authorization", "")
        prefix = "Bearer "
        if not auth.startswith(prefix):
            return False
        return hmac.compare_digest(auth[len(prefix):], TOKEN)

    def do_GET(self):
        if self.path == "/health":
            self._json(200, {"ok": True})
        else:
            self._json(404, {"error": "not_found"})

    def do_POST(self):
        if self.path != "/exec":
            self._json(404, {"error": "not_found"})
            return
        if not self._authorized():
            self._json(401, {"error": "unauthorized"})
            return
        try:
            length = int(self.headers.get("Content-Length", 0))
            body = json.loads(self.rfile.read(length)) if length > 0 else {}
        except (ValueError, TypeError):
            self._json(400, {"error": "invalid_json"})
            return
        prompt = (body.get("prompt") or "").strip()
        if not prompt:
            self._json(400, {"error": "missing 'prompt'"})
            return
        self._json(200, run_hermes(prompt))


if __name__ == "__main__":
    if not TOKEN:
        print("hermes-exec : ⚠️ HERMES_EXEC_TOKEN vide → toutes les requêtes seront refusées", flush=True)
    print(f"hermes-exec — écoute sur 0.0.0.0:{PORT} (hermes={HERMES_BIN}, timeout={TIMEOUT}s)", flush=True)
    ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
