package auth

import (
	"net"
	"net/http"
	"strings"
)

// ClientIP returns the caller's address, reading through trustedProxyHops
// reverse proxies (#2453).
//
// X-Forwarded-For grows left-to-right: every proxy APPENDS the peer it
// received the request from. For client -> CDN -> own-proxy -> Minstrel the
// app sees XFF = [client, CDN] and RemoteAddr = own-proxy. Each trusted proxy
// therefore accounts for one entry counting from the right, and the first
// address we were NOT told to trust is the client:
//
//	hops 0  -> RemoteAddr; XFF ignored entirely
//	hops 1  -> XFF[1] = CDN — trusting only our own proxy, the most we can
//	           honestly claim is the address it told us about
//	hops 2  -> XFF[0] = client
//
// This replaces an earlier heuristic that ignored XFF whenever RemoteAddr was
// public. That was safe but useless in the deployment that matters: a proxy
// on a public address (separate host, or a CDN) meant every session recorded
// the proxy, so the active-sessions surface could never show an address
// change (#370).
//
// # What the operator is asserting
//
// hops >= 1 is a DECLARATION that a proxy sits in front. Two ways to get it
// wrong, both worth understanding rather than papering over:
//
//   - Set to 1+ with NO proxy: any client can forge X-Forwarded-For and pick
//     what its own session row shows, defeating the compromise detection.
//   - Set HIGHER than the real chain: the index runs past the proxy-written
//     entries into attacker-supplied ones, same result.
//
// Both are inherent to the trusted-hop model — Rails, Caddy, Traefik and
// nginx all behave this way — which is why 0 is a first-class value and the
// admin card tells the operator to count their proxies.
func ClientIP(r *http.Request, trustedProxyHops int) string {
	remote := hostOf(r.RemoteAddr)
	if trustedProxyHops <= 0 {
		return remote
	}
	chain := forwardedChain(r)
	if len(chain) == 0 {
		// No forwarding header: either there's genuinely no proxy, or one is
		// misconfigured. The socket peer is the only thing we actually know.
		return remote
	}
	// Clamp rather than reject: a chain shorter than the configured depth
	// means the operator over-counted, and the leftmost entry is the closest
	// thing to a client on offer. The caveat above covers the risk.
	idx := len(chain) - trustedProxyHops
	if idx < 0 {
		idx = 0
	}
	if ip := net.ParseIP(chain[idx]); ip != nil {
		return ip.String()
	}
	// A proxy wrote something that isn't an address. Positional meaning is
	// lost, so fall back to what we can verify ourselves.
	return remote
}

// forwardedChain returns the X-Forwarded-For entries in wire order, or the
// single X-Real-IP value when XFF is absent.
//
// Entries are kept verbatim, including unparseable ones: their POSITION is
// what carries meaning here, so silently dropping a malformed hop would
// shift every index and could hand back an attacker-supplied entry.
func forwardedChain(r *http.Request) []string {
	raw := r.Header.Get("X-Forwarded-For")
	if strings.TrimSpace(raw) == "" {
		// Some proxies set only X-Real-IP, which by construction is a single
		// hop — the address that proxy saw.
		if real := strings.TrimSpace(r.Header.Get("X-Real-IP")); real != "" {
			return []string{real}
		}
		return nil
	}
	parts := strings.Split(raw, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if p = strings.TrimSpace(p); p != "" {
			out = append(out, p)
		}
	}
	return out
}

// hopsOf reads a trusted-depth accessor, treating a nil one as "trust
// nothing". Test contexts and any future caller that hasn't wired the
// settings service get the safe reading rather than a panic.
func hopsOf(fn func() int) int {
	if fn == nil {
		return 0
	}
	return fn()
}

// hostOf strips the port from a RemoteAddr, tolerating values that have none.
func hostOf(remoteAddr string) string {
	host, _, err := net.SplitHostPort(remoteAddr)
	if err != nil {
		return strings.TrimSpace(remoteAddr)
	}
	return host
}
