package webui

import (
	"net"
	"net/http"
	"net/netip"
	"strings"

	"mailgoserver/internal/toolbox"
)

// cloudflareRanges are Cloudflare's published proxy IP ranges (fetched live from
// https://www.cloudflare.com/ips-v4 and /ips-v6 rather than trusted from memory,
// since a stale list here would either wrongly trust an attacker-controlled hop or
// wrongly distrust Cloudflare's own edge) — expanded when "cloudflare" appears in
// the trusted_proxies config value. Cloudflare rotates these occasionally; re-fetch
// and update this list if IP resolution behind Cloudflare ever looks wrong.
var cloudflareRanges = []string{
	"173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22",
	"141.101.64.0/18", "108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20",
	"197.234.240.0/22", "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
	"104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
	"2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32",
	"2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32",
}

// parseTrustedProxies reads the [Server] trusted_proxies config value — a
// comma-separated list of CIDRs and/or the literal word "cloudflare" — into parsed
// prefixes. Unparseable entries are skipped (logged by the caller) rather than
// failing startup over a typo in a security-adjacent but non-fatal setting.
func parseTrustedProxies(raw string, logger *toolbox.Logger) []netip.Prefix {
	var out []netip.Prefix
	for _, entry := range strings.Split(raw, ",") {
		entry = strings.TrimSpace(entry)
		if entry == "" {
			continue
		}
		if strings.EqualFold(entry, "cloudflare") {
			for _, cidr := range cloudflareRanges {
				if p, err := netip.ParsePrefix(cidr); err == nil {
					out = append(out, p)
				}
			}
			continue
		}
		p, err := netip.ParsePrefix(entry)
		if err != nil {
			// A bare IP (no /mask) is a common typo for "trust this one proxy" —
			// accept it as a /32 or /128 host route rather than silently dropping it.
			if addr, addrErr := netip.ParseAddr(entry); addrErr == nil {
				bits := 32
				if addr.Is6() {
					bits = 128
				}
				out = append(out, netip.PrefixFrom(addr, bits))
				continue
			}
			if logger != nil {
				logger.Error("trusted_proxies: skipping unparseable entry %q: %v", entry, err)
			}
			continue
		}
		out = append(out, p)
	}
	return out
}

func isTrustedProxy(trusted []netip.Prefix, addr netip.Addr) bool {
	for _, p := range trusted {
		if p.Contains(addr) {
			return true
		}
	}
	return false
}

// requestIP returns the best-effort real client IP for r. Forwarded headers
// (CF-Connecting-IP, X-Forwarded-For, X-Real-IP) are only honored when the direct
// TCP peer (r.RemoteAddr) is itself a configured trusted proxy — otherwise a client
// with no proxy in front of it could simply set these headers itself and spoof any
// IP for every audit log entry and IP-based check in the app. When trusted,
// X-Forwarded-For is walked from the right (the hop closest to us) skipping any
// entries that are themselves trusted proxies, landing on the first untrusted (i.e.
// real client) address — the standard correct algorithm, since the leftmost entry is
// client-supplied and trivially spoofable even through a legitimate proxy.
func (a *App) requestIP(r *http.Request) string {
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		host = r.RemoteAddr
	}
	peer, parseErr := netip.ParseAddr(host)
	if parseErr != nil || !isTrustedProxy(a.trustedProxies, peer) {
		return host
	}

	if cf := strings.TrimSpace(r.Header.Get("CF-Connecting-IP")); cf != "" {
		return cf
	}
	if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
		hops := strings.Split(fwd, ",")
		for i := len(hops) - 1; i >= 0; i-- {
			hop := strings.TrimSpace(hops[i])
			if hop == "" {
				continue
			}
			if addr, err := netip.ParseAddr(hop); err == nil && isTrustedProxy(a.trustedProxies, addr) {
				continue // another hop we also trust — keep walking left for the real client
			}
			return hop
		}
	}
	if realIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); realIP != "" {
		return realIP
	}
	return host
}
