package clientip

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

	"go.uber.org/zap"
)

// Extractor provides secure client IP address extraction
// CWE-348: Prevents X-Forwarded-For header spoofing by validating trusted proxies
type Extractor struct {
	trustedProxies []*net.IPNet
	logger         *zap.Logger
}

// NewExtractor creates a new IP extractor with trusted proxy configuration
// trustedProxyCIDRs should contain CIDR blocks of trusted reverse proxies
// Example: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
func NewExtractor(trustedProxyCIDRs []string, logger *zap.Logger) (*Extractor, error) {
	var trustedProxies []*net.IPNet

	for _, cidr := range trustedProxyCIDRs {
		_, ipNet, err := net.ParseCIDR(cidr)
		if err != nil {
			logger.Error("failed to parse trusted proxy CIDR",
				zap.String("cidr", cidr),
				zap.Error(err))
			return nil, err
		}
		trustedProxies = append(trustedProxies, ipNet)
	}

	logger.Info("client IP extractor initialized",
		zap.Int("trusted_proxy_ranges", len(trustedProxies)))

	return &Extractor{
		trustedProxies: trustedProxies,
		logger:         logger.Named("client-ip-extractor"),
	}, nil
}

// NewDefaultExtractor creates an extractor with no trusted proxies
// This is safe for direct connections but will ignore X-Forwarded-For headers
func NewDefaultExtractor(logger *zap.Logger) *Extractor {
	logger.Warn("client IP extractor initialized with NO trusted proxies - X-Forwarded-For will be ignored")
	return &Extractor{
		trustedProxies: []*net.IPNet{},
		logger:         logger.Named("client-ip-extractor"),
	}
}

// Extract extracts the real client IP address from the HTTP request
// CWE-348: Secure implementation that prevents header spoofing
func (e *Extractor) Extract(r *http.Request) string {
	// Step 1: Get the immediate connection's remote address
	remoteAddr := r.RemoteAddr

	// Remove port from RemoteAddr (format: "IP:port" or "[IPv6]:port")
	remoteIP := e.stripPort(remoteAddr)

	// Step 2: Parse the remote IP
	parsedRemoteIP := net.ParseIP(remoteIP)
	if parsedRemoteIP == nil {
		e.logger.Warn("failed to parse remote IP address",
			zap.String("remote_addr", remoteAddr))
		return remoteIP // Return as-is if we can't parse it
	}

	// Step 3: Check if the immediate connection is from a trusted proxy
	if !e.isTrustedProxy(parsedRemoteIP) {
		// NOT from a trusted proxy - do NOT trust X-Forwarded-For header
		// This prevents clients from spoofing their IP by setting the header
		e.logger.Debug("remote IP is not a trusted proxy, using RemoteAddr",
			zap.String("remote_ip", remoteIP))
		return remoteIP
	}

	// Step 4: Remote IP is trusted, check X-Forwarded-For header
	// Format: "client, proxy1, proxy2" (leftmost is original client)
	xff := r.Header.Get("X-Forwarded-For")
	if xff == "" {
		// No X-Forwarded-For header, use RemoteAddr
		e.logger.Debug("no X-Forwarded-For header from trusted proxy",
			zap.String("remote_ip", remoteIP))
		return remoteIP
	}

	// Step 5: Parse X-Forwarded-For header
	// Take the FIRST IP (leftmost) which should be the original client
	ips := strings.Split(xff, ",")
	if len(ips) == 0 {
		e.logger.Debug("empty X-Forwarded-For header",
			zap.String("remote_ip", remoteIP))
		return remoteIP
	}

	// Get the first IP and trim whitespace
	clientIP := strings.TrimSpace(ips[0])

	// Step 6: Validate the client IP
	parsedClientIP := net.ParseIP(clientIP)
	if parsedClientIP == nil {
		e.logger.Warn("invalid IP in X-Forwarded-For header",
			zap.String("xff", xff),
			zap.String("client_ip", clientIP))
		return remoteIP // Fall back to RemoteAddr
	}

	e.logger.Debug("extracted client IP from X-Forwarded-For",
		zap.String("client_ip", clientIP),
		zap.String("remote_proxy", remoteIP),
		zap.String("xff_chain", xff))

	return clientIP
}

// ExtractOrDefault extracts the client IP or returns a default value
func (e *Extractor) ExtractOrDefault(r *http.Request, defaultIP string) string {
	ip := e.Extract(r)
	if ip == "" {
		return defaultIP
	}
	return ip
}

// isTrustedProxy checks if an IP is in the trusted proxy list
func (e *Extractor) isTrustedProxy(ip net.IP) bool {
	for _, ipNet := range e.trustedProxies {
		if ipNet.Contains(ip) {
			return true
		}
	}
	return false
}

// stripPort removes the port from an address string
// Handles both IPv4 (192.168.1.1:8080) and IPv6 ([::1]:8080) formats
func (e *Extractor) stripPort(addr string) string {
	// For IPv6, check for bracket format [IP]:port
	if strings.HasPrefix(addr, "[") {
		// IPv6 format: [::1]:8080
		if idx := strings.LastIndex(addr, "]:"); idx != -1 {
			return addr[1:idx] // Extract IP between [ and ]
		}
		// Malformed IPv6 address
		return addr
	}

	// For IPv4, split on last colon
	if idx := strings.LastIndex(addr, ":"); idx != -1 {
		return addr[:idx]
	}

	// No port found
	return addr
}

// GetTrustedProxyCount returns the number of configured trusted proxy ranges
func (e *Extractor) GetTrustedProxyCount() int {
	return len(e.trustedProxies)
}

// HasTrustedProxies returns true if any trusted proxies are configured
func (e *Extractor) HasTrustedProxies() bool {
	return len(e.trustedProxies) > 0
}
