package cardigann

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"strings"
	"time"
)

// ValidateURL validates that a URL is safe to make requests to.
// It blocks requests to private/internal IPs and non-HTTP schemes.
// Threat model T-10-05: SSRF protection.
func ValidateURL(rawURL string) error {
	// Check for config override (testing only)
	if os.Getenv("CARDIGANN_ALLOW_PRIVATE") == "true" {
		return nil
	}

	// Basic scheme check before full URL parsing
	lower := strings.ToLower(rawURL)
	if !strings.HasPrefix(lower, "http://") && !strings.HasPrefix(lower, "https://") {
		return fmt.Errorf("URL scheme must be http or https, got: %q", rawURL)
	}

	// Extract hostname
	host := rawURL
	// Remove scheme
	if idx := strings.Index(host, "://"); idx >= 0 {
		host = host[idx+3:]
	}
	// Remove path and everything after
	if idx := strings.Index(host, "/"); idx >= 0 {
		host = host[:idx]
	}
	// Remove port
	if idx := strings.LastIndex(host, ":"); idx >= 0 {
		host = host[:idx]
	}
	// Remove user info
	if idx := strings.LastIndex(host, "@"); idx >= 0 {
		host = host[idx+1:]
	}

	host = strings.ToLower(strings.TrimSpace(host))

	// Block well-known local hostnames
	if host == "localhost" || strings.HasSuffix(host, ".local") || strings.HasSuffix(host, ".internal") {
		return fmt.Errorf("hostname %q is blocked (private/local)", host)
	}

	// Resolve hostname and check IPs
	resolveCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	resolver := net.Resolver{}
	ips, err := resolver.LookupIPAddr(resolveCtx, host)
	if err != nil {
		return fmt.Errorf("failed to resolve hostname %q: %w", host, err)
	}

	for _, ipAddr := range ips {
		ip := ipAddr.IP
		if isPrivateIP(ip) {
			return fmt.Errorf("hostname %q resolves to private IP %s", host, ip)
		}
	}

	return nil
}

// isPrivateIP checks if an IP address is in a private/reserved range.
func isPrivateIP(ip net.IP) bool {
	// IPv4 private ranges
	if ip.To4() != nil {
		// 127.0.0.0/8 (loopback)
		if ip.IsLoopback() {
			return true
		}
		// 10.0.0.0/8
		if ip[0] == 10 {
			return true
		}
		// 172.16.0.0/12
		if ip[0] == 172 && ip[1] >= 16 && ip[1] <= 31 {
			return true
		}
		// 192.168.0.0/16
		if ip[0] == 192 && ip[1] == 168 {
			return true
		}
		// 169.254.0.0/16 (link-local)
		if ip[0] == 169 && ip[1] == 254 {
			return true
		}
		// 0.0.0.0
		if ip.IsUnspecified() {
			return true
		}
	}

	// IPv6 checks
	if ip.To4() == nil {
		// ::1 (loopback)
		if ip.IsLoopback() {
			return true
		}
		// fc00::/7 (unique local / private)
		if (ip[0] & 0xfe) == 0xfc {
			return true
		}
		// fe80::/10 (link-local)
		if ip[0] == 0xfe && (ip[1]&0xc0) == 0x80 {
			return true
		}
		// :: (unspecified)
		if ip.IsUnspecified() {
			return true
		}
	}

	return false
}

// SafeHTTPClient returns an http.Client with timeouts and DNS checking.
func SafeHTTPClient() *http.Client {
	return &http.Client{
		Timeout: 15 * time.Second,
		Transport: &http.Transport{
			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
				// Extract host from addr (may include port)
				host, _, err := net.SplitHostPort(addr)
				if err != nil {
					host = addr
				}

				// Resolve and check the IP
				resolver := net.Resolver{}
				ips, err := resolver.LookupIPAddr(ctx, host)
				if err != nil {
					return nil, fmt.Errorf("DNS resolution failed for %q: %w", host, err)
				}

				for _, ipAddr := range ips {
					if isPrivateIP(ipAddr.IP) {
						return nil, fmt.Errorf("blocked private IP %s for host %q", ipAddr.IP, host)
					}
				}

				// Use the first resolved IP
				if len(ips) == 0 {
					return nil, fmt.Errorf("no IP addresses found for %q", host)
				}

				dialer := net.Dialer{Timeout: 10 * time.Second}
				return dialer.DialContext(ctx, network, net.JoinHostPort(ips[0].IP.String(), func() string {
					_, port, _ := net.SplitHostPort(addr)
					return port
				}()))
			},
		},
	}
}
