#!/usr/bin/env python3
"""
bewpro-webhook.py — Servidor HTTP local para ejecutar delete-project.sh como root.

Escucha en 127.0.0.1:9876 (solo localhost, nunca expuesto a internet).
Laravel le hace POST /delete con JSON y un Bearer token compartido.

Instalación:
  cp bewpro-webhook.py /root/scripts/bewpro-webhook.py
  chmod 700 /root/scripts/bewpro-webhook.py
  cp bewpro-webhook.service /etc/systemd/system/bewpro-webhook.service
  systemctl daemon-reload
  systemctl enable --now bewpro-webhook

Variables de entorno (poner en /etc/systemd/system/bewpro-webhook.service):
  BEWPRO_WEBHOOK_SECRET=<token-largo-aleatorio>
  BEWPRO_SCRIPT=/root/scripts/delete-project.sh   (opcional, default abajo)

Endpoints:
  POST /delete   — ejecuta delete-project.sh
  GET  /health   — liveness check

Body JSON de /delete:
  {
    "cpanel_user": "siteabc",
    "backup":      false,
    "dry_run":     false,
    "reason":      "Eliminado desde panel BewPro",
    "keep_airtable": false
  }

Respuesta JSON:
  { "ok": true/false, "exit": 0, "output": "...(últimas 200 líneas)..." }
"""

import http.server
import json
import logging
import os
import subprocess
import threading
import time

# ── Config ────────────────────────────────────────────────────────────────────
HOST   = os.getenv("BEWPRO_WEBHOOK_HOST", "127.0.0.1")
PORT   = int(os.getenv("BEWPRO_WEBHOOK_PORT", "9876"))
SECRET = os.getenv("BEWPRO_WEBHOOK_SECRET", "")
SCRIPT = os.getenv("BEWPRO_SCRIPT", "/root/scripts/delete-project.sh")
LOG_FILE = os.getenv("BEWPRO_LOG", "/var/log/bewpro-webhook.log")
TIMEOUT  = int(os.getenv("BEWPRO_TIMEOUT", "900"))  # 15 min máximo por job

# ── Logging ───────────────────────────────────────────────────────────────────
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE),
        logging.StreamHandler(),
    ],
)
log = logging.getLogger("bewpro-webhook")

# Semáforo: solo un delete a la vez (evitar condiciones de carrera en VPS)
_delete_lock = threading.Semaphore(1)


def verify_token(auth_header: str) -> bool:
    if not SECRET:
        log.warning("BEWPRO_WEBHOOK_SECRET no configurado — rechazando todas las peticiones")
        return False
    if not auth_header or not auth_header.startswith("Bearer "):
        return False
    return auth_header[7:].strip() == SECRET


def build_cmd(body: dict) -> list[str]:
    cpanel_user = str(body.get("cpanel_user", "")).strip()
    if not cpanel_user:
        raise ValueError("cpanel_user requerido")

    cmd = [SCRIPT, cpanel_user, "--yes"]

    if body.get("keep_airtable"):
        cmd.append("--keep-airtable")

    if body.get("backup"):
        cmd.append("--backup")
    else:
        cmd.append("--skip-backup")

    if body.get("dry_run"):
        cmd.append("--dry-run")

    reason = str(body.get("reason", "Eliminado desde panel BewPro")).strip()
    if reason:
        cmd.append(f"--reason={reason}")

    if body.get("server"):
        cmd.append(f"--server={body['server']}")

    if body.get("cloudinary_folder"):
        cmd.append(f"--cloudinary-folder={body['cloudinary_folder']}")

    if body.get("airtable_project_id"):
        cmd.append(f"--airtable-project-id={body['airtable_project_id']}")

    if body.get("domain"):
        cmd.append(f"--domain={body['domain']}")

    return cmd


class WebhookHandler(http.server.BaseHTTPRequestHandler):

    def log_message(self, fmt, *args):  # silencia el log default de BaseHTTP
        pass

    # ── GET /health | /status/<cpanel_user> ───────────────────────────────────
    def do_GET(self):
        if self.path == "/health":
            self._json(200, {"ok": True, "pid": os.getpid(), "ts": int(time.time())})
        elif self.path.startswith("/status/"):
            cpanel_user = self.path[len("/status/"):].strip("/")
            if not cpanel_user or "/" in cpanel_user:
                self._json(400, {"ok": False, "error": "invalid cpanel_user"})
                return
            auth = self.headers.get("Authorization", "")
            if not verify_token(auth):
                self._json(401, {"ok": False, "error": "unauthorized"})
                return
            progress_file = f"/tmp/bewpro-delete-{cpanel_user}.json"
            if not os.path.isfile(progress_file):
                self._json(404, {"ok": False, "error": "no progress file"})
                return
            try:
                with open(progress_file) as f:
                    data = json.load(f)
                self._json(200, {"ok": True, "progress": data})
            except Exception as exc:
                self._json(500, {"ok": False, "error": str(exc)})
        else:
            self._json(404, {"ok": False, "error": "not found"})

    # ── POST /delete ──────────────────────────────────────────────────────────
    def do_POST(self):
        if self.path != "/delete":
            self._json(404, {"ok": False, "error": "not found"})
            return

        # Auth
        auth = self.headers.get("Authorization", "")
        if not verify_token(auth):
            log.warning("POST /delete — token inválido desde %s", self.client_address[0])
            self._json(401, {"ok": False, "error": "unauthorized"})
            return

        # Body
        try:
            length = int(self.headers.get("Content-Length", 0))
            raw = self.rfile.read(length)
            body = json.loads(raw)
        except Exception as exc:
            self._json(400, {"ok": False, "error": f"bad body: {exc}"})
            return

        # Construir comando
        try:
            cmd = build_cmd(body)
        except ValueError as exc:
            self._json(400, {"ok": False, "error": str(exc)})
            return

        cpanel_user = body.get("cpanel_user", "?")
        log.info("DELETE solicitado: user=%s cmd=%s", cpanel_user, " ".join(cmd))

        # Solo un delete a la vez
        if not _delete_lock.acquire(blocking=False):
            self._json(409, {"ok": False, "error": "Otro delete en progreso, reintentá en 1 minuto"})
            return

        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=TIMEOUT,
            )
        except subprocess.TimeoutExpired:
            _delete_lock.release()
            log.error("DELETE timeout (%ds) para user=%s", TIMEOUT, cpanel_user)
            self._json(504, {"ok": False, "error": f"script timeout after {TIMEOUT}s"})
            return
        except Exception as exc:
            _delete_lock.release()
            log.error("DELETE error para user=%s: %s", cpanel_user, exc)
            self._json(500, {"ok": False, "error": str(exc)})
            return

        _delete_lock.release()

        combined = (result.stdout or "") + (result.stderr or "")
        # Devolver últimas 200 líneas para no saturar el response
        lines = combined.splitlines()
        output_tail = "\n".join(lines[-200:]) if len(lines) > 200 else combined

        ok = result.returncode == 0
        status = 200 if ok else 500

        log.info("DELETE completado: user=%s exit=%d ok=%s", cpanel_user, result.returncode, ok)
        if not ok:
            log.error("Output (tail):\n%s", output_tail[-2000:])

        self._json(status, {
            "ok":     ok,
            "exit":   result.returncode,
            "output": output_tail,
        })

    # ── Helper ────────────────────────────────────────────────────────────────
    def _json(self, code: int, data: dict):
        body = json.dumps(data).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)


class ThreadedHTTPServer(http.server.ThreadingHTTPServer):
    daemon_threads = True
    allow_reuse_address = True


def main():
    if not SECRET:
        log.error("BEWPRO_WEBHOOK_SECRET no configurado — saliendo por seguridad.")
        raise SystemExit(1)

    if not os.path.isfile(SCRIPT):
        log.error("Script no encontrado: %s", SCRIPT)
        raise SystemExit(1)

    server = ThreadedHTTPServer((HOST, PORT), WebhookHandler)
    log.info("bewpro-webhook escuchando en %s:%d (timeout=%ds)", HOST, PORT, TIMEOUT)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        log.info("Detenido.")


if __name__ == "__main__":
    main()
