package proxy

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"time"
)

// Validation step names.
const (
	StepSyntax = "syntax"
	StepDNS    = "dns"
	StepTCP    = "tcp"
	StepHTTP   = "http"
)

// ValidationStep holds the result of a single validation check.
type ValidationStep struct {
	Name    string `json:"name"`
	Passed  bool   `json:"passed"`
	Message string `json:"message,omitempty"`
	Hint    string `json:"hint,omitempty"`
}

// ValidationResult holds the aggregate result of the validation pipeline.
type ValidationResult struct {
	Valid bool             `json:"valid"`
	Steps []ValidationStep `json:"steps"`
}

// ValidateDestination runs the multi-step validation pipeline against the given
// destination host and port. It checks syntax, DNS, TCP reachability, and HTTP health.
// The pipeline short-circuits on failure: later steps are skipped if an earlier one fails.
func ValidateDestination(ctx context.Context, host string, port int) ValidationResult {
	result := ValidationResult{Valid: true}

	// Step 1: Syntax validation.
	if step, ok := validateSyntax(host, port); !ok {
		result.Valid = false
		result.Steps = append(result.Steps, step)
		return result
	} else {
		result.Steps = append(result.Steps, step)
	}

	// Step 2: DNS resolution (skip for IP addresses).
	ip := net.ParseIP(host)
	if ip == nil {
		if step, ok := validateDNS(ctx, host); !ok {
			result.Valid = false
			result.Steps = append(result.Steps, step)
			return result
		} else {
			result.Steps = append(result.Steps, step)
		}
	} else {
		result.Steps = append(result.Steps, ValidationStep{
			Name:    StepDNS,
			Passed:  true,
			Message: "Skipped (IP address provided)",
		})
	}

	// Step 3: TCP port reachability.
	if step, ok := validateTCP(ctx, host, port); !ok {
		result.Valid = false
		result.Steps = append(result.Steps, step)
		return result
	} else {
		result.Steps = append(result.Steps, step)
	}

	// Step 4: HTTP health probe.
	step := validateHTTP(ctx, host, port)
	result.Steps = append(result.Steps, step)
	if !step.Passed {
		result.Valid = false
	}

	return result
}

// validateSyntax checks that the host and port values are syntactically valid.
func validateSyntax(host string, port int) (ValidationStep, bool) {
	if host == "" {
		return ValidationStep{
			Name:    StepSyntax,
			Passed:  false,
			Message: "Host is empty",
			Hint:    "Provide a valid hostname or IP address.",
		}, false
	}

	if port < 1 || port > 65535 {
		return ValidationStep{
			Name:    StepSyntax,
			Passed:  false,
			Message: fmt.Sprintf("Port %d is out of range (1-65535)", port),
			Hint:    "Provide a valid port number between 1 and 65535.",
		}, false
	}

	// Reject obviously invalid hostnames (but allow IPs).
	if net.ParseIP(host) == nil {
		// Basic hostname validation: must not contain spaces or schemes.
		if _, err := url.Parse("http://" + host); err != nil {
			return ValidationStep{
				Name:    StepSyntax,
				Passed:  false,
				Message: "Invalid hostname: " + err.Error(),
				Hint:    "Provide a valid hostname without scheme (e.g., 'example.com' not 'http://example.com').",
			}, false
		}
	}

	return ValidationStep{
		Name:    StepSyntax,
		Passed:  true,
		Message: fmt.Sprintf("Host %q port %d syntax OK", host, port),
	}, true
}

// validateDNS performs a DNS lookup on the given host.
func validateDNS(ctx context.Context, host string) (ValidationStep, bool) {
	resolver := net.DefaultResolver
	addrs, err := resolver.LookupHost(ctx, host)
	if err != nil {
		return ValidationStep{
			Name:    StepDNS,
			Passed:  false,
			Message: fmt.Sprintf("DNS resolution failed for %q: %s", host, err.Error()),
			Hint:    diagnosticHint(StepDNS, err),
		}, false
	}

	return ValidationStep{
		Name:    StepDNS,
		Passed:  true,
		Message: fmt.Sprintf("Resolved to %v", addrs),
	}, true
}

// validateTCP attempts a TCP connection to host:port with a 5-second timeout.
func validateTCP(ctx context.Context, host string, port int) (ValidationStep, bool) {
	addr := net.JoinHostPort(host, strconv.Itoa(port))

	dialCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	var d net.Dialer
	conn, err := d.DialContext(dialCtx, "tcp", addr)
	if err != nil {
		return ValidationStep{
			Name:    StepTCP,
			Passed:  false,
			Message: fmt.Sprintf("TCP connect to %s failed: %s", addr, err.Error()),
			Hint:    diagnosticHint(StepTCP, err),
		}, false
	}
	conn.Close()

	return ValidationStep{
		Name:    StepTCP,
		Passed:  true,
		Message: fmt.Sprintf("TCP connect to %s succeeded", addr),
	}, true
}

// validateHTTP performs a GET request to the destination and checks for a response.
// Non-5xx responses are considered passing (the service is responding).
func validateHTTP(ctx context.Context, host string, port int) ValidationStep {
	target := fmt.Sprintf("http://%s:%d/", host, port)

	httpCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, target, nil)
	if err != nil {
		return ValidationStep{
			Name:    StepHTTP,
			Passed:  false,
			Message: fmt.Sprintf("Failed to build HTTP request: %s", err.Error()),
			Hint:    diagnosticHint(StepHTTP, err),
		}
	}

	client := &http.Client{
		Timeout: 10 * time.Second,
		// Do not follow redirects — we just want to see if the port responds to HTTP.
		CheckRedirect: func(*http.Request, []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	resp, err := client.Do(req)
	if err != nil {
		return ValidationStep{
			Name:    StepHTTP,
			Passed:  false,
			Message: fmt.Sprintf("HTTP probe to %s failed: %s", target, err.Error()),
			Hint:    diagnosticHint(StepHTTP, err),
		}
	}
	resp.Body.Close()

	if resp.StatusCode >= 500 {
		return ValidationStep{
			Name:    StepHTTP,
			Passed:  false,
			Message: fmt.Sprintf("Service responded with HTTP %d. The service may not be healthy.", resp.StatusCode),
			Hint:    fmt.Sprintf("Service responded with HTTP %d. The service may not be healthy.", resp.StatusCode),
		}
	}

	return ValidationStep{
		Name:    StepHTTP,
		Passed:  true,
		Message: fmt.Sprintf("HTTP probe returned %d", resp.StatusCode),
	}
}
