// Package web is the HTTP layer of compare.sr.ht. It ports the SourceHut chrome
// (nav/service-switcher, login block, environment banner) to Go html/templates,
// renders the repository landing, compare (base...head) and single-commit pages
// server-side, and embeds a compact JSON payload plus the vendored esbuild
// bundle so the browser renders the diff with @pierre/diffs and @pierre/trees.
//
// The package owns no state of its own: identity comes from the authz cookie
// middleware, authorization from an authz.Authorizer (git.sr.ht GraphQL), and
// git data from gitx over bare repositories on disk. Every request that touches
// a repository authorizes first (a not-found or forbidden repo is a 404, never
// a 403, so private-repo existence never leaks) and only then reads the disk.
//
// # What the cmd layer must wire
//
// Register only installs routes; it assumes the following middleware is already
// applied to the router it is handed, in this order (outermost first):
//
//	chi middleware.RealIP
//	chi middleware.Recoverer
//	chi middleware.Logger        (optional, but recommended)
//	config.Middleware(conf, "compare.sr.ht")   // required: authz + gitx read it
//	authz.Middleware()           // required: never 401s; sets the viewer
//
// config.Middleware must run before authz.Middleware is irrelevant to authz
// itself (it only reads the cookie), but the GraphQL authorizer invoked inside
// handlers needs config.ForContext(ctx) to resolve git.sr.ht's API origin, so
// config.Middleware is mandatory on every request that reaches a handler.
package web

import (
	"fmt"
	"io/fs"
	"net/http"
	"path"
	"regexp"

	"github.com/vaughan0/go-ini"
	"sourcecraft.dev/bigbes/sr-ht-core/config"

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

// hashedCSSRe matches the content-addressed stylesheet name so it can be served
// with an immutable cache lifetime (the hash changes whenever the bytes do).
var hashedCSSRe = regexp.MustCompile(`^main\.min\.[0-9a-f]{6,}\.css$`)

// hashedBundleRe matches the content-addressed frontend bundle. Like the
// stylesheet it carries a content hash in its name so a deploy busts the browser
// cache (a stale bundle.js is why the file tree can render blank after an
// upgrade); a matching name is served immutable.
var hashedBundleRe = regexp.MustCompile(`^bundle\.[0-9a-f]{6,}\.js$`)

// Server holds the immutable configuration a request handler needs. It is built
// once at startup and is safe for concurrent use.
type Server struct {
	authorizer authz.Authorizer
	reposRoot  string
	conf       ini.File

	siteName      string
	environment   string
	metaOrigin    string
	compareOrigin string
	hubOrigin     string
	cssHref       string
	bundleHref    string

	nav              []navItem
	staticFileServer http.Handler
}

// New assembles a Server from the shared SourceHut config. It reads
// [git.sr.ht] repos, [meta.sr.ht] origin and [compare.sr.ht] origin (all
// required), the [sr.ht] site-name/environment display values, and resolves the
// hashed stylesheet name by globbing the embedded static FS. A missing required
// key is a clear error, not a panic, so the cmd layer can fail startup loudly.
func New(conf ini.File, authorizer authz.Authorizer) (*Server, error) {
	reposRoot, ok := conf.Get("git.sr.ht", "repos")
	if !ok || reposRoot == "" {
		return nil, fmt.Errorf("web: [git.sr.ht] repos is required")
	}
	metaOrigin := config.GetOrigin(conf, "meta.sr.ht", true)
	if metaOrigin == "" {
		return nil, fmt.Errorf("web: [meta.sr.ht] origin is required")
	}
	compareOrigin := config.GetOrigin(conf, "compare.sr.ht", true)
	if compareOrigin == "" {
		return nil, fmt.Errorf("web: [compare.sr.ht] origin is required")
	}

	cssHref, err := resolveCSSHref()
	if err != nil {
		return nil, err
	}

	bundleHref, err := resolveBundleHref()
	if err != nil {
		return nil, err
	}

	staticSub, err := fs.Sub(staticFS, "static")
	if err != nil {
		return nil, fmt.Errorf("web: sub static FS: %w", err)
	}

	return &Server{
		authorizer:       authorizer,
		reposRoot:        reposRoot,
		conf:             conf,
		siteName:         config.GetString(conf, "sr.ht", "site-name", "sourcehut"),
		environment:      config.GetString(conf, "sr.ht", "environment", "production"),
		metaOrigin:       metaOrigin,
		compareOrigin:    compareOrigin,
		hubOrigin:        config.GetOrigin(conf, "hub.sr.ht", true),
		cssHref:          cssHref,
		bundleHref:       bundleHref,
		nav:              buildNav(conf),
		staticFileServer: http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))),
	}, nil
}

// resolveCSSHref globs the embedded static FS for the content-addressed
// stylesheet and returns its site-absolute URL.
func resolveCSSHref() (string, error) {
	matches, err := fs.Glob(staticFS, "static/main.min.*.css")
	if err != nil {
		return "", fmt.Errorf("web: glob stylesheet: %w", err)
	}
	if len(matches) == 0 {
		return "", fmt.Errorf("web: no main.min.*.css in embedded static assets")
	}
	return "/static/" + path.Base(matches[0]), nil
}

// resolveBundleHref globs the embedded static FS for the content-addressed
// frontend bundle and returns its site-absolute URL.
func resolveBundleHref() (string, error) {
	matches, err := fs.Glob(staticFS, "static/bundle.*.js")
	if err != nil {
		return "", fmt.Errorf("web: glob bundle: %w", err)
	}
	if len(matches) == 0 {
		return "", fmt.Errorf("web: no bundle.*.js in embedded static assets")
	}
	return "/static/" + path.Base(matches[0]), nil
}
