// Filter a GitHub webhook so only your review requests start an agent.
//
// A GitHub repository webhook delivers every pull-request event: opened, edited,
// comments, team review requests, and direct review requests. Only a direct
// request naming your reviewer should start the review agent — every other
// delivery should just be acknowledged.
//
// Documented at https://appcd-dev.github.io/stackgen-sdk/code-samples/webhook-filter-cel/
//
//	go run ./examples/go/automation/webhook-filter-cel.go --agent my-pr-review-agent
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	stackgen "github.com/appcd-dev/stackgen-sdk/clients/go"
)

// envOr returns the first non-empty environment variable among keys.
func envOr(keys ...string) string {
	for _, key := range keys {
		if v := strings.TrimSpace(os.Getenv(key)); v != "" {
			return v
		}
	}
	return ""
}

// buildFilterCel matches a direct review request naming reviewerLogin (human or bot), nothing else.
func buildFilterCel(reviewerLogin string) string {
	return `("x-github-event" in headers) && ` +
		`headers["x-github-event"] == "pull_request" && ` +
		`has(body.action) && body.action == "review_requested" && ` +
		`has(body.requested_reviewer) && body.requested_reviewer != null && ` +
		`has(body.requested_reviewer.login) && ` +
		fmt.Sprintf(`body.requested_reviewer.login.matches("(?i)^%s$")`, reviewerLogin)
}

func postGithubEvent(baseURL, orgID, token, event string, body map[string]any) (int, error) {
	raw, err := json.Marshal(body)
	if err != nil {
		return 0, err
	}
	endpoint := strings.TrimRight(baseURL, "/") + "/guild/api/v1/webhooks/trigger?" +
		url.Values{"orgId": {orgID}}.Encode()
	req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(raw))
	if err != nil {
		return 0, err
	}
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-GitHub-Event", event)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()
	return resp.StatusCode, nil
}

func main() {
	agent := flag.String("agent", "", "Existing agent name")
	workflow := flag.String("workflow", "", "Existing workflow name")
	reviewer := flag.String("reviewer", "release-bot", "GitHub login the filter accepts review requests for")
	message := flag.String("message", "sdk: review this pull request and reply Done", "Webhook action prompt")
	flag.Parse()
	if (*agent == "") == (*workflow == "") {
		fmt.Fprintln(os.Stderr, "pass exactly one of --agent or --workflow")
		os.Exit(2)
	}
	base, token, org := envOr("STACKGEN_URL"), envOr("STACKGEN_TOKEN"), envOr("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT")
	if base == "" || token == "" || org == "" {
		fmt.Fprintln(os.Stderr, "set STACKGEN_URL, STACKGEN_TOKEN, and STACKGEN_WORKSPACE_ID")
		os.Exit(2)
	}
	targetType, targetName := "agent", *agent
	if *workflow != "" {
		targetType, targetName = "workflow", *workflow
	}
	client, err := stackgen.NewClient(stackgen.Config{BaseURL: base, APIToken: token, WorkspaceID: org})
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	ctx := context.Background()
	filterCel := buildFilterCel(*reviewer)

	prEdited := map[string]any{"action": "edited", "pull_request": map[string]any{"number": 42}}
	reviewRequestedMatch := map[string]any{
		"action":             "review_requested",
		"requested_reviewer": map[string]any{"login": *reviewer, "type": "User"},
	}

	// 1. Preview the condition before saving anything.
	fmt.Println("--- previewing filter_cel (no webhook saved yet) ---")
	matched, err := client.Aiden.PreviewWebhookFilter(ctx, filterCel, mustJSON(reviewRequestedMatch),
		[]map[string]string{{"name": "X-GitHub-Event", "value": "pull_request"}})
	if err != nil {
		fmt.Fprintln(os.Stderr, "preview error:", err)
		os.Exit(1)
	}
	fmt.Printf("  matching human reviewer: matched=%v\n", matched)
	matched, err = client.Aiden.PreviewWebhookFilter(ctx, filterCel, mustJSON(prEdited),
		[]map[string]string{{"name": "X-GitHub-Event", "value": "pull_request"}})
	if err != nil {
		fmt.Fprintln(os.Stderr, "preview error:", err)
		os.Exit(1)
	}
	fmt.Printf("  PR edited (no reviewer): matched=%v\n", matched)

	// 2. Create the webhook with the condition already attached.
	wh, err := client.Aiden.CreateWebhook(ctx, map[string]any{
		"name":        fmt.Sprintf("sdk-pr-review-filter-%d", time.Now().UnixNano()%1_000_000),
		"target_type": targetType,
		"target_name": targetName,
		"action":      *message,
		"enabled":     true,
		"filter_cel":  filterCel,
	})
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
	webhookID, _ := wh["id"].(string)
	webhookToken, _ := wh["token"].(string)
	fmt.Printf("\ncreated webhook id=%s filter_cel=%v\n", webhookID, wh["filter_cel"])
	defer func() {
		if webhookID != "" {
			_, _ = client.Aiden.DeleteWebhook(context.Background(), webhookID)
			fmt.Printf("deleted webhook id=%s\n", webhookID)
		}
	}()
	if webhookToken == "" {
		fmt.Fprintln(os.Stderr, "create webhook did not return a token")
		os.Exit(1)
	}

	runsBefore := len(listRuns(ctx, client, webhookID))

	// 3. A PR edit does not match — acknowledged, but no run recorded.
	status, err := postGithubEvent(base, org, webhookToken, "pull_request", prEdited)
	if err != nil {
		fmt.Fprintln(os.Stderr, "trigger error:", err)
		os.Exit(1)
	}
	fmt.Printf("PR edited delivery: ack status=%d\n", status)
	time.Sleep(2 * time.Second)
	runsAfterEdit := len(listRuns(ctx, client, webhookID))
	if runsAfterEdit != runsBefore {
		fmt.Fprintf(os.Stderr, "expected no new run for a nonmatching delivery (before=%d, after=%d)\n", runsBefore, runsAfterEdit)
		os.Exit(1)
	}
	fmt.Printf("webhook runs after PR edit: %d (unchanged)\n", runsAfterEdit)

	// 4. A direct review request for the reviewer matches — a run starts.
	status, err = postGithubEvent(base, org, webhookToken, "pull_request", reviewRequestedMatch)
	if err != nil {
		fmt.Fprintln(os.Stderr, "trigger error:", err)
		os.Exit(1)
	}
	fmt.Printf("review_requested delivery: ack status=%d\n", status)
	time.Sleep(2 * time.Second)
	runsAfterReview := len(listRuns(ctx, client, webhookID))
	if runsAfterReview <= runsAfterEdit {
		fmt.Fprintf(os.Stderr, "expected a new run for the matching delivery (afterEdit=%d, afterReview=%d)\n", runsAfterEdit, runsAfterReview)
		os.Exit(1)
	}
	fmt.Printf("webhook runs after review request: %d (started one run)\n", runsAfterReview)

	// 5. Clear the condition — the webhook reverts to accepting every event.
	cleared, err := client.Aiden.UpdateWebhook(ctx, webhookID, map[string]any{"filter_cel": ""})
	if err != nil {
		fmt.Fprintln(os.Stderr, "update error:", err)
		os.Exit(1)
	}
	fmt.Printf("cleared filter_cel=%v\n", cleared["filter_cel"])
}

func mustJSON(v map[string]any) string {
	raw, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}
	return string(raw)
}

func listRuns(ctx context.Context, client *stackgen.Client, webhookID string) []map[string]any {
	runs, err := client.Aiden.ListWebhookRuns(ctx, webhookID)
	if err != nil {
		fmt.Fprintln(os.Stderr, "list runs error:", err)
		os.Exit(1)
	}
	return runs
}
