#!/usr/bin/env python3
"""Tiny webhook listener that runs deploy/pull.sh when Gitea pushes main.

Listens on 127.0.0.1:9528 — exposed publicly only via openresty at
https://web.facere.cc/_hook/deploy. Validates the Gitea HMAC-SHA256
signature against FACERE_DEPLOY_SECRET before running anything.
"""
from __future__ import annotations

import hmac
import hashlib
import json
import os
import subprocess
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SECRET = os.environ.get("FACERE_DEPLOY_SECRET", "").encode()
PULL = os.environ.get(
    "FACERE_DEPLOY_SCRIPT",
    "/home/ubuntu/repo/facere-website/deploy/pull.sh",
)
LOG = os.environ.get("FACERE_DEPLOY_LOG", "/tmp/facere-deploy.log")
BIND_HOST = os.environ.get("FACERE_DEPLOY_BIND", "127.0.0.1")
BIND_PORT = int(os.environ.get("FACERE_DEPLOY_PORT", "9528"))

_run_lock = threading.Lock()


class Handler(BaseHTTPRequestHandler):
    def _reply(self, status: int, msg: str) -> None:
        body = (msg + "\n").encode()
        self.send_response(status)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self) -> None:
        self._reply(200, "facere-deploy-hook ready")

    def do_POST(self) -> None:
        length = int(self.headers.get("Content-Length", "0") or "0")
        body = self.rfile.read(length) if length > 0 else b""

        sig = self.headers.get("X-Gitea-Signature", "") or self.headers.get(
            "X-Hub-Signature-256", ""
        ).removeprefix("sha256=")
        expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
        if not sig or not hmac.compare_digest(sig, expected):
            self._reply(401, "bad signature")
            return

        ref = ""
        try:
            ref = json.loads(body).get("ref", "")
        except Exception:
            pass
        if ref and ref != "refs/heads/main":
            self._reply(202, f"ignored ref {ref}")
            return

        if not _run_lock.acquire(blocking=False):
            self._reply(202, "deploy already running")
            return
        try:
            with open(LOG, "a") as f:
                f.write(f"[{self.log_date_time_string()}] webhook trigger\n")
                f.flush()
                proc = subprocess.run(
                    [PULL], stdout=f, stderr=subprocess.STDOUT, timeout=180
                )
            if proc.returncode == 0:
                self._reply(200, "ok")
            else:
                self._reply(500, f"pull.sh exit {proc.returncode}")
        except subprocess.TimeoutExpired:
            self._reply(504, "pull.sh timeout")
        finally:
            _run_lock.release()

    def log_message(self, fmt: str, *args) -> None:
        sys.stderr.write(
            "[%s] %s - %s\n"
            % (self.log_date_time_string(), self.address_string(), fmt % args)
        )


def main() -> int:
    if not SECRET:
        print("FACERE_DEPLOY_SECRET is not set", file=sys.stderr)
        return 1
    srv = ThreadingHTTPServer((BIND_HOST, BIND_PORT), Handler)
    print(f"facere-deploy-hook listening on {BIND_HOST}:{BIND_PORT}", flush=True)
    try:
        srv.serve_forever()
    except KeyboardInterrupt:
        srv.shutdown()
    return 0


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