package admin

import (
	"net"
	"net/http"
)

// IPAllow is middleware that rejects any request whose source IP is not within
// the configured allowlist. It is the first line of defence in front of the
// admin backend (doc/06 §2: 管理后台不得暴露公网).
//
// The source address is taken from the TCP peer (RemoteAddr) ONLY. Proxy
// headers like X-Forwarded-For are intentionally ignored: the admin port is
// reached directly over an SSH tunnel / intranet, so trusting client-supplied
// headers would let an attacker spoof the allowlist.
type IPAllow struct {
	allow []*net.IPNet
	sec   *SecurityLog
	next  http.Handler
}

// NewIPAllow wraps next with the allowlist check.
func NewIPAllow(allow []*net.IPNet, sec *SecurityLog, next http.Handler) *IPAllow {
	return &IPAllow{allow: allow, sec: sec, next: next}
}

func (m *IPAllow) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ip := clientIP(r.RemoteAddr)
	if ip == nil || !m.allowed(ip) {
		if m.sec != nil {
			m.sec.IPBlocked(r.Context(), hostOnly(r.RemoteAddr), r.URL.Path)
		}
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}
	m.next.ServeHTTP(w, r)
}

func (m *IPAllow) allowed(ip net.IP) bool {
	for _, n := range m.allow {
		if n.Contains(ip) {
			return true
		}
	}
	return false
}

// clientIP extracts the net.IP from a "host:port" RemoteAddr.
func clientIP(remoteAddr string) net.IP {
	return net.ParseIP(hostOnly(remoteAddr))
}

func hostOnly(remoteAddr string) string {
	host, _, err := net.SplitHostPort(remoteAddr)
	if err != nil {
		// RemoteAddr may already be a bare host in some test setups.
		return remoteAddr
	}
	return host
}
