// auth.go — inter-service auth middleware per ADR-003.
//
// Two layers, each independently configurable:
//   - Bearer token (constant-time compare via crypto/subtle)
//   - IP allowlist (CIDR set; bare IPs treated as /32)
//
// /health is exempt from both layers (load balancers + monitors need
// it open; the route doesn't expose anything sensitive).
//
// When both Token and AllowedIPs are empty, RequireAuth returns a
// pass-through that does no work — preserves G0 dev-mode behavior
// where every binary binds 127.0.0.1 and the network is the auth
// layer.
//
// The non-loopback-bind + empty-token coupling is enforced at
// startup in shared.Run, not in the middleware — the middleware
// only sees per-request auth, not the bind config. Together they
// make the audit's worst case (R-001 + R-007: queryd /sql RCE-eq
// off-loopback with no auth) mechanically impossible.
package shared

import (
	"crypto/subtle"
	"log/slog"
	"net"
	"net/http"
	"strings"
)

// RequireAuth returns a chi-compatible middleware that enforces
// the configured AuthConfig. Empty config returns a pass-through.
func RequireAuth(cfg AuthConfig) func(http.Handler) http.Handler {
	tokenSet := cfg.Token != "" || len(cfg.SecondaryTokens) > 0
	if !tokenSet && len(cfg.AllowedIPs) == 0 {
		// G0 dev mode — no auth wired.
		return passthrough
	}

	// Pre-parse CIDRs once. Invalid entries log a warning and are
	// dropped — fail-loud-but-not-fatal so a typo in one CIDR
	// doesn't kill the binary; operator sees the warning at startup.
	var allowedNets []*net.IPNet
	for _, raw := range cfg.AllowedIPs {
		cidr := raw
		if !strings.Contains(cidr, "/") {
			// Bare IP — single-host CIDR.
			if strings.Contains(cidr, ":") {
				cidr += "/128"
			} else {
				cidr += "/32"
			}
		}
		_, n, err := net.ParseCIDR(cidr)
		if err != nil {
			slog.Warn("auth: invalid CIDR in allowed_ips, skipping",
				"raw", raw, "err", err)
			continue
		}
		allowedNets = append(allowedNets, n)
	}

	// Pre-encode wire-format Bearer headers for primary + every
	// secondary token. Per-request comparison walks the slice with
	// constant-time compare on each — fast path is the primary
	// (first), so the typical case is one compare.
	var expectedHeaders [][]byte
	if cfg.Token != "" {
		expectedHeaders = append(expectedHeaders, []byte("Bearer "+cfg.Token))
	}
	for _, sec := range cfg.SecondaryTokens {
		if sec == "" {
			continue
		}
		expectedHeaders = append(expectedHeaders, []byte("Bearer "+sec))
	}

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// /health bypasses both layers. Operators rely on it
			// being public for liveness probes.
			if r.URL.Path == "/health" {
				next.ServeHTTP(w, r)
				return
			}

			if len(allowedNets) > 0 && !ipAllowed(r, allowedNets) {
				http.Error(w, "forbidden", http.StatusForbidden)
				return
			}

			if tokenSet {
				got := []byte(r.Header.Get("Authorization"))
				matched := false
				for _, want := range expectedHeaders {
					if subtle.ConstantTimeCompare(got, want) == 1 {
						matched = true
						break
					}
				}
				if !matched {
					http.Error(w, "unauthorized", http.StatusUnauthorized)
					return
				}
			}

			next.ServeHTTP(w, r)
		})
	}
}

// passthrough is the no-op middleware returned when no auth is
// configured. Used by RequireAuth in G0 dev mode.
func passthrough(next http.Handler) http.Handler { return next }

// ipAllowed checks whether the request's source IP is in any of
// the allowed networks. Falls back to false for unparseable
// RemoteAddr — a deploy with broken peer-IP logging would otherwise
// silently bypass the allowlist.
func ipAllowed(r *http.Request, nets []*net.IPNet) bool {
	ip := remoteIP(r)
	if ip == nil {
		return false
	}
	for _, n := range nets {
		if n.Contains(ip) {
			return true
		}
	}
	return false
}

// remoteIP extracts the request's source IP. Today: r.RemoteAddr.
// Future: when a trusted proxy fronts the gateway and adds
// X-Forwarded-For, we'd add a config knob to honor the first hop.
// G0 deploys are direct-to-binary so RemoteAddr suffices.
func remoteIP(r *http.Request) net.IP {
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		// SplitHostPort failure could mean the test's httptest.Server
		// passed a bare IP; try parsing as-is.
		host = r.RemoteAddr
	}
	return net.ParseIP(host)
}
