#!/usr/bin/env python3
# ================================================================
# Scenario 1a: Webhook HTTP Listener (Linux 版)
# Role: HTTP server 监听 GitLink webhook → 解析 Issue 编号 → 调用分类脚本
#
# 部署: systemd 管理，端口 8080，无需 root（用 systemd socket activation 或 sudo）
# 依赖: Python 3.6+ (无需额外 pip 包)
# ================================================================

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

# ── 配置 ────────────────────────────────────────────────────────
PORT = int(os.environ.get("WEBHOOK_PORT", "8080"))
SECRET = os.environ.get("WEBHOOK_SECRET", "")
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
TRIAGE_SCRIPT = os.path.join(SCRIPT_DIR, "01a-issue-triage.sh")
GATEKEEPER_SCRIPT = os.path.join(SCRIPT_DIR, "02a-pr-gatekeeper.sh")
LOG_DIR = os.path.join(SCRIPT_DIR, "webhook-logs")

os.makedirs(LOG_DIR, exist_ok=True)


def log(msg, level="INFO"):
    ts = datetime.now().strftime("%H:%M:%S")
    color = {"INFO": "\033[36m", "OK": "\033[32m", "WARN": "\033[33m", "ERR": "\033[31m"}.get(level, "")
    reset = "\033[0m"
    print(f"[{ts}] {color}[{level:>4}]{reset} {msg}", flush=True)
    # Also append to log file
    logfile = os.path.join(LOG_DIR, datetime.now().strftime("webhook-%Y%m%d.log"))
    with open(logfile, "a", encoding="utf-8") as f:
        f.write(f"[{ts}] [{level:>4}] {msg}\n")


def verify_signature(body: bytes, signature_header: str) -> bool:
    """HMAC-SHA256 签名验证"""
    if not SECRET:
        return True  # 未配置密钥，跳过验证
    if not signature_header:
        log("No signature header in request", "WARN")
        return False

    try:
        expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
        # Support both "sha256=abc..." and plain "abc..." formats
        received = signature_header
        if received.startswith("sha256="):
            received = received[7:]
        return hmac.compare_digest(expected, received)
    except Exception as e:
        log(f"HMAC error: {e}", "ERR")
        return False


def extract_issue_number(payload: dict) -> str:
    """从 webhook payload 提取 Issue 编号"""
    issue = payload.get("issue", {})
    if issue:
        num = issue.get("project_issues_index") or issue.get("number") or issue.get("id")
        if num:
            log(f"Extracted issue number: #{num}", "OK")
            return str(num)
    if "number" in payload:
        return str(payload["number"])
    if "id" in payload:
        return str(payload["id"])
    log("Could not extract issue number from payload", "WARN")
    return None


def extract_pr_number(payload: dict) -> str:
    """从 webhook payload 提取 PR 编号"""
    pr = payload.get("pull_request", {}) or payload.get("pullrequest", {})
    if pr:
        num = pr.get("number") or pr.get("pull_request_number") or pr.get("id")
        if num:
            log(f"Extracted PR number: #{num}", "OK")
            return str(num)
    if "pull_request_number" in payload:
        return str(payload["pull_request_number"])
    log("Could not extract PR number from payload", "WARN")
    return None


def extract_repo_info(payload: dict):
    """从 payload 提取 owner/repo"""
    repo = payload.get("repository", {})
    owner = repo.get("owner", {})
    owner_name = owner.get("login") or owner.get("username") or str(owner) if isinstance(owner, dict) else str(owner)
    repo_name = repo.get("name", "")
    return owner_name, repo_name


def run_triage_async(issue_number: str, owner: str = "", repo: str = ""):
    """异步调用分类脚本，另起线程避免阻塞 webhook 响应"""
    def _run():
        cmd = ["bash", TRIAGE_SCRIPT, "--issue-number", issue_number]
        if owner:
            cmd += ["--owner", owner]
        if repo:
            cmd += ["--repo", repo]

        log(f"Dispatching triage: {' '.join(cmd)}", "INFO")
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, cwd=SCRIPT_DIR)
            # Save output to log
            outfile = os.path.join(LOG_DIR, f"triage-{issue_number}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.log")
            with open(outfile, "w", encoding="utf-8") as f:
                f.write(f"=== STDOUT ===\n{result.stdout}\n=== STDERR ===\n{result.stderr}\n")
            if result.returncode == 0:
                log(f"Triage #{issue_number} completed successfully → {outfile}", "OK")
            else:
                log(f"Triage #{issue_number} failed (exit={result.returncode}) → {outfile}", "ERR")
        except subprocess.TimeoutExpired:
            log(f"Triage #{issue_number} TIMEOUT after 120s", "ERR")
        except Exception as e:
            log(f"Triage #{issue_number} error: {e}", "ERR")

    t = threading.Thread(target=_run, daemon=True)
    t.start()


def run_gatekeeper_async(pr_number: str, owner: str = "", repo: str = ""):
    """异步调用 PR gatekeeper 脚本"""
    def _run():
        cmd = ["bash", GATEKEEPER_SCRIPT, "--pr-id", pr_number]
        if owner:
            cmd += ["--owner", owner]
        if repo:
            cmd += ["--repo", repo]

        log(f"Dispatching gatekeeper: {' '.join(cmd)}", "INFO")
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, cwd=SCRIPT_DIR)
            outfile = os.path.join(LOG_DIR, f"gatekeeper-{pr_number}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.log")
            with open(outfile, "w", encoding="utf-8") as f:
                f.write(f"=== STDOUT ===\n{result.stdout}\n=== STDERR ===\n{result.stderr}\n")
            if result.returncode == 0:
                log(f"Gatekeeper PR #{pr_number} completed successfully -> {outfile}", "OK")
            else:
                log(f"Gatekeeper PR #{pr_number} failed (exit={result.returncode}) -> {outfile}", "ERR")
        except subprocess.TimeoutExpired:
            log(f"Gatekeeper PR #{pr_number} TIMEOUT after 300s", "ERR")
        except Exception as e:
            log(f"Gatekeeper PR #{pr_number} error: {e}", "ERR")

    t = threading.Thread(target=_run, daemon=True)
    t.start()


class WebhookHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        log(f"{self.client_address[0]} - {format % args}", "INFO")

    def do_GET(self):
        """健康检查 / 根页面"""
        html = f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>GitLink Webhook Listener</title>
<style>body{{font-family:sans-serif;max-width:800px;margin:40px auto;padding:20px}}
h1{{color:#333}}.ok{{color:green;font-weight:bold}}code{{background:#f0f0f0;padding:2px 6px;border-radius:3px}}</style>
</head><body>
<h1>GitLink Community Ops — Webhook Listener</h1>
<p class="ok">✓ Running</p>
<p>Listening for <code>issue</code> and <code>pull_request</code> events at <code>/webhook</code></p>
<p><small>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | Port: {PORT} | HMAC: {'enabled' if SECRET else 'disabled'}</small></p>
</body></html>"""
        self._respond(200, html, "text/html")

    def do_POST(self):
        """接收 webhook"""
        content_length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(content_length) if content_length > 0 else b""

        event_type = self.headers.get("X-GitLink-Event") or self.headers.get("X-GitHub-Event") or ""
        signature = self.headers.get("X-GitLink-Signature") or self.headers.get("X-Hub-Signature-256") or ""

        log(f"POST {self.path} | Event: {event_type} | Size: {content_length}B | From: {self.client_address[0]}")

        # HMAC 验证
        if SECRET and not verify_signature(body, signature):
            self._respond(403, '{"error":"Invalid signature"}')
            return

        # 解析 payload
        try:
            payload = json.loads(body)
        except json.JSONDecodeError:
            log("Failed to parse JSON payload", "ERR")
            self._respond(400, '{"error":"Invalid JSON"}')
            return

        action = payload.get("action", "")
        is_issue = "issue" in event_type.lower()
        is_pr = "pull_request" in event_type.lower() or "pullrequest" in event_type.lower()

        if not is_issue and not is_pr:
            log(f"Ignoring unsupported event: {event_type}", "INFO")
            self._respond(200, '{"status":"ignored","reason":"unsupported event type"}')
            return

        # 只处理 "opened" 动作
        if action and action != "opened":
            log(f"Ignoring {event_type} event with action: {action}", "INFO")
            self._respond(200, f'{{"status":"ignored","reason":"action={action}"}}')
            return

        # 提取 owner/repo
        owner, repo = extract_repo_info(payload)

        # ── Issue 事件 → 调起分类 ──
        if is_issue:
            issue_number = extract_issue_number(payload)
            if not issue_number:
                self._respond(400, '{"error":"Cannot extract issue number"}')
                return
            log(f"=== New Issue #{issue_number} — dispatching to triage ===", "OK")
            run_triage_async(issue_number, owner or "", repo or "")
            self._respond(200, f'{{"status":"accepted","issue_number":{issue_number}}}')
            return

        # ── PR 事件 → 调起 gatekeeper ──
        if is_pr:
            pr_number = extract_pr_number(payload)
            if not pr_number:
                self._respond(400, '{"error":"Cannot extract PR number"}')
                return
            log(f"=== New PR #{pr_number} — dispatching to gatekeeper ===", "OK")
            run_gatekeeper_async(pr_number, owner or "", repo or "")
            self._respond(200, f'{{"status":"accepted","pr_number":{pr_number}}}')
            return

    def _respond(self, code, body, content_type="application/json"):
        self.send_response(code)
        self.send_header("Content-Type", f"{content_type}; charset=utf-8")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(body.encode("utf-8"))


def main():
    print()
    print("\033[36m╔══════════════════════════════════════════════════════════════╗\033[0m")
    print("\033[36m║   GitLink Community Ops — Webhook Listener (Linux)         ║\033[0m")
    print("\033[36m╚══════════════════════════════════════════════════════════════╝\033[0m")
    print()

    log(f"Starting on port {PORT}")
    log(f"  Issue -> {os.path.basename(TRIAGE_SCRIPT)}")
    log(f"  PR    -> {os.path.basename(GATEKEEPER_SCRIPT)}")
    log(f"HMAC verification: {'ENABLED' if SECRET else 'DISABLED (set WEBHOOK_SECRET env var)'}")
    print()

    server = HTTPServer(("0.0.0.0", PORT), WebhookHandler)
    log(f"Listening on http://0.0.0.0:{PORT}/webhook", "OK")
    log(f"Health check: http://0.0.0.0:{PORT}/", "OK")
    print()
    log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "OK")
    log("  Press Ctrl+C to stop", "OK")
    log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "OK")
    print()

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        log("Shutting down...", "INFO")
        server.shutdown()
        log("Stopped", "OK")


if __name__ == "__main__":
    main()
