package shared

import (
	"fmt"
	"log/slog"
	"net"
	"os"
	"strings"
)

// requireLoopbackOrOverride enforces that the bind address is on the
// loopback interface unless an explicit env override is set. Closes
// the worst case of audit risk R-001 (queryd /sql + DuckDB + non-
// loopback bind = RCE-equivalent for anyone who can reach the port)
// without committing to an auth model.
//
// Override env: LH_<UPPER(serviceName)>_ALLOW_NONLOOPBACK=1.
// When the override fires, we log a structured warn so the choice is
// auditable in production logs.
//
// Cases that pass:
//   - 127.0.0.1, 127.x.y.z (the /8), [::1], localhost
//   - explicit-override env set to "1"
//
// Cases that fail-loud:
//   - 0.0.0.0, [::], any non-loopback IP
//   - empty host ":port" (listens on all interfaces)
//   - unparseable addr
//
// The function is also useful as a unit-testable predicate; callers
// that want to gate something other than Run can call it directly.
func requireLoopbackOrOverride(serviceName, addr string) error {
	if isLoopbackAddr(addr) {
		return nil
	}
	envKey := "LH_" + strings.ToUpper(serviceName) + "_ALLOW_NONLOOPBACK"
	if os.Getenv(envKey) == "1" {
		slog.Warn("non-loopback bind allowed by env override",
			"service", serviceName,
			"addr", addr,
			"env", envKey,
			"hint", "audit risk R-001 — see reports/scrum/risk-register.md")
		return nil
	}
	return fmt.Errorf("refusing non-loopback bind %q for %q "+
		"(set %s=1 to override; see audit R-001)", addr, serviceName, envKey)
}

// requireAuthOnNonLoopback closes the audit's R-001 + R-007 worst
// case: any binary that's deployed off-loopback MUST have an auth
// token configured. An off-loopback bind without auth is the literal
// "queryd /sql is RCE-equivalent" failure mode.
//
// Pairs with requireLoopbackOrOverride: that gate refuses non-loopback
// bind unless an explicit env override fires; this gate refuses the
// same bind unless auth.token is also set. Together they make the
// worst case mechanically impossible.
func requireAuthOnNonLoopback(serviceName, addr string, auth AuthConfig) error {
	if isLoopbackAddr(addr) {
		return nil
	}
	if auth.Token != "" {
		return nil
	}
	return fmt.Errorf("refuse non-loopback bind %q for %q without auth.token configured "+
		"(R-001 + R-007 — see ADR-003)", addr, serviceName)
}

// isLoopbackAddr returns true iff addr's host portion is on the
// loopback interface. Covers IPv4 127.0.0.0/8, IPv6 ::1, and
// "localhost". Empty host (":port"), empty string, and any
// non-parseable addr return false.
func isLoopbackAddr(addr string) bool {
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		// Unparseable shape — could be a bare hostname or wholly
		// malformed. Rejecting protects against future changes that
		// silently accept new shapes.
		return false
	}
	if host == "" {
		// ":port" listens on ALL interfaces — explicitly non-loopback.
		return false
	}
	if host == "localhost" {
		return true
	}
	ip := net.ParseIP(host)
	if ip == nil {
		// Hostname that isn't "localhost". We don't resolve DNS here
		// (slow + misleading); reject so deploys must be explicit.
		return false
	}
	return ip.IsLoopback()
}
