package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

type Config struct {
	ListenAddr    string
	WorkspaceRoot string        // mkdtemp parent. empty = os.TempDir()
	JobTimeout    time.Duration // max wall-clock for a single Claude session
}

func loadConfig() Config {
	cfg := Config{
		ListenAddr:    envOr("KLAUS_LISTEN", ":8020"),
		WorkspaceRoot: os.Getenv("KLAUS_WORKSPACE"),
		JobTimeout:    20 * time.Minute,
	}
	if v := os.Getenv("KLAUS_JOB_TIMEOUT"); v != "" {
		if d, err := time.ParseDuration(v); err == nil {
			cfg.JobTimeout = d
		} else {
			log.Printf("ignoring invalid KLAUS_JOB_TIMEOUT=%q: %v", v, err)
		}
	}
	if cfg.WorkspaceRoot != "" {
		if err := os.MkdirAll(cfg.WorkspaceRoot, 0o755); err != nil {
			log.Fatalf("create workspace root %s: %v", cfg.WorkspaceRoot, err)
		}
	}
	return cfg
}

func envOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

func main() {
	cfg := loadConfig()
	worker := NewWorker(cfg)

	http.HandleFunc("/gitea-webhook", func(w http.ResponseWriter, r *http.Request) {
		eventType := r.Header.Get("X-Gitea-Event")
		if eventType == "" {
			eventType = r.Header.Get("X-Gogs-Event")
		}
		log.Printf("%s %s from %s event=%s", r.Method, r.URL.Path, r.RemoteAddr, eventType)

		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		body, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		defer r.Body.Close()

		if !json.Valid(body) {
			http.Error(w, "invalid JSON", http.StatusBadRequest)
			return
		}

		event, trigger, err := DecideTrigger(eventType, body)
		if err != nil {
			log.Printf("decode webhook: %v", err)
			http.Error(w, "bad payload", http.StatusBadRequest)
			return
		}
		if trigger == nil {
			log.Printf("[skip] %s(action=%s) on %s by @%s — no klaus trigger; %s",
				eventType, event.Action, event.Repository.FullName, event.Sender.Name(),
				skipHint(eventType, event))
			w.WriteHeader(http.StatusOK)
			return
		}
		log.Printf("[queue] %s on %s#%d — reason=%s (by @%s)",
			eventType, event.Repository.FullName, trigger.Number, trigger.Reason, event.Sender.Name())

		worker.Enqueue(Job{
			EventType: eventType,
			Event:     event,
			Trigger:   trigger,
		})
		w.WriteHeader(http.StatusAccepted)
	})

	log.Printf("klaus-bot listening on %s (workspace=%s, timeout=%s)",
		cfg.ListenAddr, displayPath(cfg.WorkspaceRoot), cfg.JobTimeout)
	log.Fatal(http.ListenAndServe(cfg.ListenAddr, nil))
}

func displayPath(p string) string {
	if p == "" {
		return os.TempDir() + " (default)"
	}
	return p
}

// skipHint produces a short diagnostic suffix for the "[skip]" log line so
// operators can tell at a glance why a payload didn't trigger Klaus.
func skipHint(eventType string, ev *GiteaEvent) string {
	snippet := func(s string) string {
		s = strings.TrimSpace(s)
		s = strings.ReplaceAll(s, "\n", " ")
		if len(s) > 120 {
			s = s[:120] + "…"
		}
		return s
	}
	switch eventType {
	case "issue_comment":
		if ev.Comment != nil {
			return fmt.Sprintf("comment=%q", snippet(ev.Comment.Body))
		}
	case "issues":
		if ev.Issue != nil {
			return fmt.Sprintf("issue=%q", snippet(ev.Issue.Body))
		}
	case "pull_request":
		if ev.PullRequest != nil {
			return fmt.Sprintf("pr=%q", snippet(ev.PullRequest.Body))
		}
	case "pull_request_review":
		if ev.Review != nil {
			return fmt.Sprintf("review=%q", snippet(ev.Review.Body))
		}
	}
	return "(no body)"
}
