// Command comparesrht is the compare.sr.ht daemon: a stateless HTTP service
// that renders git ref-to-ref diffs and single-commit views for a self-hosted
// SourceHut instance.
//
// It reuses core-go's server.New assembly so it shares the standard SourceHut
// daemon lifecycle (crypto initialization, -b/-m/-p flags, warm shutdown on
// SIGINT) with the rest of the fleet, but it deliberately does NOT call
// WithDefaultMiddleware: compare.sr.ht owns no Postgres or Redis and must serve
// anonymous viewers, so it installs its own lightweight middleware group on the
// anonymous router instead (see the web package documentation for the exact
// chain).
//
// Flags (parsed by core-go's server.New):
//
//	-b addr   bind address (repeatable); default 127.0.0.1:5090
//	-d        debug (verbose request logging in core-go)
//	-m addr   Prometheus metrics bind (default random port)
//	-p addr   pprof bind (default random localhost port)
//
// Configuration is loaded from the shared SourceHut config.ini via core-go's
// fixed search path (./config.ini, ../config.ini, /etc/sr.ht/config.ini,
// /etc/sr.ht/*.ini). All required keys are validated up front with clear
// messages so a misconfiguration fails loudly at startup rather than as a deep
// panic on the first request.
package main

import (
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/sirupsen/logrus"
	"github.com/vaughan0/go-ini"
	"sourcecraft.dev/bigbes/sr-ht-core/config"
	coreserver "sourcecraft.dev/bigbes/sr-ht-core/server"

	"sourcecraft.dev/bigbes/sr-ht-compare/authz"
	"sourcecraft.dev/bigbes/sr-ht-compare/web"
)

const (
	service     = "compare.sr.ht"
	defaultBind = "127.0.0.1:5090"

	// authzTTL is how long the GraphQL authorizer memoizes positive and
	// not-found repository lookups, sparing git.sr.ht a round trip per page.
	authzTTL = 60 * time.Second
)

func main() {
	logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true})

	// LoadConfig never panics on a missing file (it returns a nil ini.File);
	// validateConfig turns any absent required key into a single clear fatal.
	conf := config.LoadConfig()
	apiOrigin := validateConfig(conf)

	// server.New parses -b/-d/-m/-p from the argument vector and runs
	// crypto.InitCrypto(conf) — the network-key and webhook key validated above
	// are exactly what it needs, so this cannot fatal after validateConfig.
	// It expects the full os.Args (core-go's getopt skips argv[0] as the
	// program name, exactly as every upstream SourceHut daemon calls it).
	srv := coreserver.New(service, defaultBind, conf, os.Args)

	authorizer := authz.NewAuthorizer(authzTTL)
	app, err := web.New(conf, authorizer)
	if err != nil {
		logrus.Fatalf("initialize web server: %v", err)
	}

	// Middleware chain per the web package contract (outermost first). This is
	// the hand-rolled substitute for WithDefaultMiddleware: no database, no
	// redis, and authz.Middleware never issues a 401 so anonymous browsing
	// works. config.Middleware must be present because the GraphQL authorizer
	// resolves git.sr.ht's API origin from config.ForContext at request time.
	//
	// server.New already froze the anonymous router for direct middleware
	// registration (it built inline sub-routers during construction), so the
	// group + middleware + routes are installed together inside a Group, which
	// chi permits on a fresh inline mux sharing the same routing tree.
	//
	// Register installs three more of its own inside a nested group: the
	// private cache policy, panic recovery through the service's error page,
	// and the same-origin guard. chi's Recoverer stays here as the outer net
	// for a panic in the two middlewares above, which are outside that group —
	// it re-panics http.ErrAbortHandler, which is what the inner one raises for
	// a panic arriving after the response has already started.
	srv.AnonRouter().Group(func(r chi.Router) {
		r.Use(middleware.RealIP)
		r.Use(middleware.Recoverer)
		r.Use(middleware.Logger)
		r.Use(config.Middleware(conf, service))
		r.Use(authz.Middleware())
		app.Register(r)
	})

	reposRoot, _ := conf.Get("git.sr.ht", "repos")
	logrus.WithFields(logrus.Fields{
		"bind":          resolveBind(os.Args[1:]),
		"repos":         reposRoot,
		"git.sr.ht-api": apiOrigin,
	}).Info("compare.sr.ht starting")

	// Run blocks until SIGINT, then performs a warm shutdown. systemd should
	// stop this unit with KillSignal=SIGINT (see contrib/compare-srht.service).
	srv.Run()
}

// validateConfig verifies every configuration key compare.sr.ht needs before it
// can serve or authorize a request. It reports all missing keys at once via a
// single logrus.Fatal so operators fix the config in one pass instead of
// discovering each gap on a separate restart. It returns the git.sr.ht internal
// API origin that GraphQL authorization will use (also logged at startup).
//
// This runs BEFORE anything can reach config.GetAPI, which panics when no
// origin candidate is configured, and before crypto.InitCrypto (invoked by
// server.New), which would otherwise fatal with a terse message on a missing
// network-key or webhook key.
func validateConfig(conf ini.File) string {
	var missing []string
	require := func(section, key string) {
		if v, ok := conf.Get(section, key); !ok || strings.TrimSpace(v) == "" {
			missing = append(missing, fmt.Sprintf("[%s] %s", section, key))
		}
	}

	require("sr.ht", "network-key")    // crypto: unified-login cookie fernet key
	require("webhooks", "private-key") // crypto: webhook signing key (shared)
	require("git.sr.ht", "repos")      // bare repository root on disk
	require("meta.sr.ht", "origin")    // login/logout links in the nav
	require("compare.sr.ht", "origin") // our own external origin

	// git.sr.ht needs at least one internal API origin candidate; without it
	// config.GetAPI panics on the first authorization request.
	apiOrigin := firstConfigured(conf, "git.sr.ht",
		"api-internal-origin", "internal-origin", "api-origin", "origin")
	if apiOrigin == "" {
		missing = append(missing,
			"[git.sr.ht] one of api-internal-origin, internal-origin, api-origin, origin")
	}

	if len(missing) > 0 {
		logrus.Fatalf("incomplete configuration; missing required keys:\n\t%s",
			strings.Join(missing, "\n\t"))
	}
	return apiOrigin
}

// firstConfigured returns the value of the first present, non-empty key in
// section, or "" if none are set.
func firstConfigured(conf ini.File, section string, keys ...string) string {
	for _, k := range keys {
		if v, ok := conf.Get(section, k); ok && strings.TrimSpace(v) != "" {
			return v
		}
	}
	return ""
}

// resolveBind reconstructs the primary bind address server.New will use, for
// logging only. server.New owns the authoritative parse; this mirrors its -b
// handling (last -b wins here; the real server binds every -b given) and falls
// back to the same default.
func resolveBind(args []string) string {
	bind := defaultBind
	for i := 0; i < len(args); i++ {
		switch a := args[i]; {
		case a == "-b" && i+1 < len(args):
			bind = args[i+1]
			i++
		case strings.HasPrefix(a, "-b") && len(a) > 2:
			bind = a[2:]
		}
	}
	return bind
}
