# ----------------------------------------------------------------
# Scenario 1a: Webhook HTTP Listener (实时链路接收器)
# Role: HTTP server listening for GitLink webhook events (Issue created)
#       → Parse payload → Call 01a-issue-triage.ps1 to classify & assign
#
# 这是社区运营自动化"实时链路"的入口，负责接收 GitLink 平台推送的
# Webhook 事件，解析 Issue 编号，然后调用分类脚本。
#
# 架构:
#   GitLink 平台 (Issue 创建)
#       → POST https://<YOUR_URL>:<PORT>/webhook
#       → 01a-webhook-listener.ps1 (本脚本，HTTP 服务器)
#       → 01a-issue-triage.ps1 (分类+打标签+分配)
#
# 部署方式:
#   A. 本地 + ngrok 内网穿透:
#      1. 启动本脚本: powershell 01a-webhook-listener.ps1
#      2. 启动 ngrok:  ngrok http 8080
#      3. 运行注册:   powershell 01a-webhook-setup.ps1 -WebhookUrl "https://xxx.ngrok.io/webhook"
#
#   B. 部署到公网服务器:
#      1. 上传脚本到服务器
#      2. 启动本脚本: powershell 01a-webhook-listener.ps1 -Port 443 -Ssl
#      3. 运行注册:   powershell 01a-webhook-setup.ps1 -WebhookUrl "https://your-server.com/webhook"
#
#   C. 仅手动触发 (无需 webhook):
#      powershell 01a-issue-triage.ps1 -IssueNumber 42
# ----------------------------------------------------------------
#Requires -Version 5.1
#Requires -RunAsAdministrator

param(
    [int]$Port = 8080,
    [string]$Secret = "",
    [string]$HostPrefix = "+",
    [switch]$Ssl,
    [switch]$Help
)

$ErrorActionPreference = "Stop"
$ScriptDir = $PSScriptRoot

if ($Help) {
    Write-Host "Usage: powershell 01a-webhook-listener.ps1 [-Port PORT] [-Secret SECRET] [-HostPrefix +] [-Ssl]"
    Write-Host ""
    Write-Host "  Webhook HTTP 接收器 — 监听 GitLink 平台的 Issue 事件，自动触发分类。"
    Write-Host ""
    Write-Host "  -Port PORT        监听端口 (默认: 8080)"
    Write-Host "  -Secret SECRET    HMAC 密钥，用于验证 GitLink 请求来源（需与注册时一致）"
    Write-Host "  -HostPrefix PREFIX 监听主机前缀 (默认: + 表示所有IP，也可用 localhost)"
    Write-Host "  -Ssl              启用 HTTPS (需要已导入的 SSL 证书)"
    Write-Host ""
    Write-Host "  部署前准备:"
    Write-Host "    如需公网访问，请使用 ngrok 或部署到有公网IP的服务器:"
    Write-Host "      ngrok http $Port"
    Write-Host "    然后运行 01a-webhook-setup.ps1 在 GitLink 平台注册 webhook"
    exit 0
}

# ================================================================
# Color Helpers (no dependency on common.psm1 since this is a server)
# ================================================================
function Log-Step  { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [STEP] $Msg" -ForegroundColor Blue }
function Log-Ok    { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [  OK] $Msg" -ForegroundColor Green }
function Log-Warn  { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [WARN] $Msg" -ForegroundColor Yellow }
function Log-Err   { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [ ERR] $Msg" -ForegroundColor Red }
function Log-Info  { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [INFO] $Msg" -ForegroundColor Cyan }

# ================================================================
# HMAC Signature Verification
# ================================================================
function Test-WebhookSignature {
    param(
        [string]$RequestBody,
        [string]$SignatureHeader,
        [string]$Secret
    )
    if (-not $Secret) { return $true }  # No secret configured, skip verification

    if (-not $SignatureHeader) {
        Log-Warn "No signature header in request (expected X-GitLink-Signature or X-Hub-Signature-256)"
        return $false
    }

    try {
        $hmac = New-Object System.Security.Cryptography.HMACSHA256
        $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Secret)
        $hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($RequestBody))
        $computed = "sha256=" + [System.BitConverter]::ToString($hash).Replace("-", "").ToLower()

        # Support both X-GitLink-Signature and X-Hub-Signature-256 (GitHub-compatible)
        # Strip prefix if present (e.g., "sha256=abc123..." → "abc123...")
        $received = $SignatureHeader
        if ($received -match '^sha256=') {
            $received = $received
        } else {
            $received = "sha256=$received"
        }

        return $computed -eq $received
    } catch {
        Log-Err "HMAC verification error: $_"
        return $false
    }
}

# ================================================================
# Extract Issue Number from Webhook Payload
# ================================================================
function Get-IssueNumberFromPayload {
    param([string]$Body, [string]$EventType)

    try {
        $payload = $Body | ConvertFrom-Json

        # Try multiple known payload structures
        # GitLink format: { action: "opened", issue: { id: ..., number: ..., project_issues_index: ... } }
        if ($payload.issue) {
            $num = $payload.issue.project_issues_index
            if (-not $num) { $num = $payload.issue.number }
            if (-not $num) { $num = $payload.issue.id }
            if ($num) {
                Log-Info "Extracted issue number: #$num (event: $EventType)"
                return $num.ToString()
            }
        }

        # GitHub-compatible format: { action: "opened", issue: { number: ... } }
        if ($payload.issue -and $payload.issue.number) {
            Log-Info "Extracted issue number (GitHub format): #$($payload.issue.number)"
            return $payload.issue.number.ToString()
        }

        # Direct format: { number: ..., id: ... }
        if ($payload.number) { return $payload.number.ToString() }
        if ($payload.id) {
            Log-Info "Extracted issue id: $($payload.id)"
            return $payload.id.ToString()
        }

        Log-Warn "Could not extract issue number from payload"
        Log-Info "Payload keys: $($payload.PSObject.Properties.Name -join ', ')"
        if ($payload.issue) {
            Log-Info "Issue keys: $($payload.issue.PSObject.Properties.Name -join ', ')"
        }
        return $null
    } catch {
        Log-Err "Failed to parse webhook payload: $_"
        Log-Info "Raw body (first 500 chars): $($Body.Substring(0, [Math]::Min(500, $Body.Length)))"
        return $null
    }
}

# ================================================================
# Extract Owner/Repo from payload or git remote
# ================================================================
function Get-RepoInfoFromPayload {
    param([string]$Body)

    try {
        $payload = $Body | ConvertFrom-Json
        $owner = $null
        $repo = $null

        # GitLink format
        if ($payload.repository) {
            if ($payload.repository.owner) {
                $owner = if ($payload.repository.owner.login) { $payload.repository.owner.login }
                         elseif ($payload.repository.owner.username) { $payload.repository.owner.username }
                         else { $payload.repository.owner }
            }
            if ($payload.repository.name) { $repo = $payload.repository.name }
        }

        # GitHub-compatible format
        if ((-not $owner) -and $payload.repository -and $payload.repository.full_name) {
            $parts = $payload.repository.full_name -split '/'
            $owner = $parts[0]
            $repo = $parts[1]
        }

        return @{ Owner = $owner; Repo = $repo }
    } catch {
        return @{ Owner = $null; Repo = $null }
    }
}

# ================================================================
# Process Incoming Webhook
# ================================================================
function Invoke-WebhookHandler {
    param(
        [string]$Body,
        [string]$EventType,
        [string]$EventHeader,
        [string]$SignatureHeader
    )

    # Validate secret if configured
    if ($Secret -and -not (Test-WebhookSignature -RequestBody $Body -SignatureHeader $SignatureHeader -Secret $Secret)) {
        Log-Err "HMAC signature verification FAILED — request rejected"
        return @{ StatusCode = 403; Body = '{"error":"Invalid signature"}' }
    }

    # Only process issue events
    if ($EventType -notmatch '^issue' -and $EventHeader -notmatch 'issue') {
        Log-Info "Ignoring non-issue event: $EventType"
        return @{ StatusCode = 200; Body = '{"status":"ignored","reason":"non-issue event"}' }
    }

    # Only process "opened" action (new issue created)
    try {
        $payload = $Body | ConvertFrom-Json
        if ($payload.action -and $payload.action -ne 'opened') {
            Log-Info "Ignoring issue event with action: $($payload.action)"
            return @{ StatusCode = 200; Body = '{"status":"ignored","reason":"action is not opened"}' }
        }
    } catch { }

    # Extract issue number
    $issueNumber = Get-IssueNumberFromPayload -Body $Body -EventType $EventType
    if (-not $issueNumber) {
        Log-Err "Cannot extract issue number — skipping triage"
        return @{ StatusCode = 400; Body = '{"error":"Cannot extract issue number from payload"}' }
    }

    Log-Ok "=== New Issue #$issueNumber — dispatching to triage ==="

    # Extract owner/repo to pass to triage script
    $repoInfo = Get-RepoInfoFromPayload -Body $Body

    # Dispatch triage script asynchronously so we can respond to webhook quickly
    $jobScript = {
        param($ScriptDir, $IssueNum, $Owner, $Repo, $Body)
        $argList = @("-File", "$ScriptDir\01a-issue-triage.ps1", "-IssueNumber", $IssueNum)
        if ($Owner) { $argList += @("-Owner", $Owner) }
        if ($Repo)  { $argList += @("-Repo", $Repo) }
        $result = & powershell.exe -NoProfile -ExecutionPolicy Bypass @argList 2>&1
        $result | Out-File "$ScriptDir\webhook-triage-$IssueNum-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
    }

    Start-Job -ScriptBlock $jobScript -ArgumentList $ScriptDir, $issueNumber, $repoInfo.Owner, $repoInfo.Repo, $Body | Out-Null
    Log-Ok "Triage job started for #$issueNumber (running in background)"

    return @{ StatusCode = 200; Body = '{"status":"accepted","issue_number":' + $issueNumber + '}' }
}

# ================================================================
# Main: Start HTTP Listener
# ================================================================
Clear-Host
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║   GitLink Community Ops — Webhook Listener                 ║" -ForegroundColor Cyan
Write-Host "║   实时链路接收器: Issue 创建 → 自动分类 → 分配责任人          ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""

$protocol = if ($Ssl) { "https" } else { "http" }
$listenUrl = "$($protocol)://$($HostPrefix):$Port/"

Log-Info "Starting listener on: $listenUrl"
Log-Info "Triage script: $ScriptDir\01a-issue-triage.ps1"
if ($Secret) {
    Log-Info "HMAC verification: ENABLED"
} else {
    Log-Warn "HMAC verification: DISABLED (set -Secret to enable)"
}

# Try to register URL ACL if not running as admin for non-localhost
if ($HostPrefix -ne "localhost" -and $HostPrefix -ne "127.0.0.1") {
    Write-Host ""
    Log-Warn "Listening on $HostPrefix requires URL ACL registration."
    Log-Info "If you get 'Access Denied', run as Administrator OR use -HostPrefix localhost"
}

# Create HttpListener
$listener = $null
try {
    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add($listenUrl + "webhook/")
    $listener.Prefixes.Add($listenUrl)  # Also listen on root path
    $listener.Start()
    Log-Ok "HTTP listener started successfully"
} catch {
    Log-Err "Failed to start HTTP listener: $_"
    Write-Host ""
    Write-Host "Troubleshooting:" -ForegroundColor Yellow
    Write-Host "  1. Run as Administrator"
    Write-Host "  2. Or register URL ACL manually:"
    Write-Host "     netsh http add urlacl url=$listenUrl user=Everyone"
    Write-Host "  3. Or use localhost only: -HostPrefix localhost"
    Write-Host "  4. Check if port $Port is already in use: netstat -ano | findstr $Port"
    exit 1
}

Write-Host ""
Log-Ok "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Log-Ok "  Listening on: $listenUrl"
Log-Ok "  Webhook URL:  ${listenUrl}webhook"
Log-Ok "  Health check: ${listenUrl}"
Log-Ok ""
Log-Ok "  按 Ctrl+C 停止服务"
Log-Ok "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Write-Host ""

# Handle Ctrl+C gracefully
$keepRunning = $true
$null = Register-EngineEvent -SourceIdentifier "WebhookListenerStop" -Forward -SupportEvent
try {
    [Console]::TreatControlCAsInput = $false
} catch { }

# Main event loop
while ($keepRunning) {
    try {
        $context = $listener.GetContext()
        $request = $context.Request
        $response = $context.Response

        $requestMethod = $request.HttpMethod
        $requestUrl = $request.Url.ToString()
        $remoteIp = $request.RemoteEndPoint.Address.ToString()

        Log-Step "$requestMethod $requestUrl (from $remoteIp)"

        if ($requestMethod -eq "GET" -and ($requestUrl -notmatch '/webhook$')) {
            # Health check / root page
            $html = @"
<!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}.status{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="status">✓ Running</p>
<p>Listening for <code>issue</code> events at <code>${listenUrl}webhook</code></p>
<p>When a new Issue is created, this server will:</p>
<ol>
  <li>Receive the webhook payload from GitLink</li>
  <li>Validate HMAC signature (if secret is configured)</li>
  <li>Extract the issue number</li>
  <li>Call <code>01a-issue-triage.ps1</code> to AI-classify and assign</li>
</ol>
<p><small>Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Port: $Port | HMAC: $(if($Secret){'enabled'}else{'disabled'})</small></p>
</body></html>
"@
            $buffer = [System.Text.Encoding]::UTF8.GetBytes($html)
            $response.ContentType = "text/html; charset=utf-8"
            $response.ContentLength64 = $buffer.Length
            $response.OutputStream.Write($buffer, 0, $buffer.Length)
            $response.OutputStream.Close()
            Log-Ok "Health check OK"
            continue
        }

        # Read request body
        $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
        $body = $reader.ReadToEnd()
        $reader.Close()

        # Get event headers
        $eventType = $request.Headers.Get("X-GitLink-Event")
        if (-not $eventType) {
            $eventType = $request.Headers.Get("X-GitHub-Event")  # GitHub-compatible
        }
        if (-not $eventType) {
            $eventType = $request.Headers.Get("X-Event-Type")
        }

        $signatureHeader = $request.Headers.Get("X-GitLink-Signature")
        if (-not $signatureHeader) {
            $signatureHeader = $request.Headers.Get("X-Hub-Signature-256")  # GitHub-compatible
        }

        Log-Info "Event: $eventType | Body length: $($body.Length) bytes"

        # Process the webhook
        $result = Invoke-WebhookHandler -Body $body -EventType $eventType -EventHeader $eventType -SignatureHeader $signatureHeader

        # Send response
        $response.StatusCode = $result.StatusCode
        $responseBuffer = [System.Text.Encoding]::UTF8.GetBytes($result.Body)
        $response.ContentType = "application/json; charset=utf-8"
        $response.ContentLength64 = $responseBuffer.Length
        $response.OutputStream.Write($responseBuffer, 0, $responseBuffer.Length)
        $response.OutputStream.Close()

    } catch [System.Net.HttpListenerException] {
        if ($_.Exception.ErrorCode -eq 995) {
            # Operation aborted — likely shutting down
            Log-Info "Listener shutting down..."
            $keepRunning = $false
        } else {
            Log-Err "HTTP error: $_"
        }
    } catch {
        Log-Err "Unexpected error: $_"
        Start-Sleep -Milliseconds 100
    }
}

# Cleanup
if ($listener -and $listener.IsListening) {
    $listener.Stop()
    $listener.Close()
    Log-Ok "HTTP listener stopped"
}

Log-Ok "Webhook listener exited"
