package resolver

import (
	"context"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"ekdns/platform"
)

// Resolve performs DNS resolution for hostname using the specified mode and config.
// discoverer is used to obtain local network information (DNS servers, gateway).
func Resolve(hostname string, mode ServerMode, config QueryConfig, discoverer platform.NetworkDiscoverer) ([]string, error) {
	var w io.Writer = io.Discard
	if config.Verbose {
		w = os.Stderr
	}

	var ips []string
	var err error

	switch mode.Mode {
	case "default", "":
		ips, err = resolveDefaultEntry(hostname, config, discoverer, w)
	case "local":
		ips, err = resolveLocal(hostname, config, discoverer)
	case "gateway":
		ips, err = resolveGateway(hostname, config, discoverer)
	case "explicit":
		ips, err = resolveExplicit(hostname, mode, config)
	default:
		return nil, fmt.Errorf("unknown server mode %q", mode.Mode)
	}

	if err != nil {
		return nil, err
	}
	if len(ips) > 0 {
		fmt.Fprintf(w, "[dns] Result: %s\n", strings.Join(ips, ", "))
	}
	return ips, nil
}

// resolveDefaultEntry is the public entry point for default mode.
// It discovers network info once, builds the resolver pool, and delegates
// to resolveDefault with depth=0.
func resolveDefaultEntry(hostname string, config QueryConfig, discoverer platform.NetworkDiscoverer, w io.Writer) ([]string, error) {
	info, _ := discoverer.Discover() // FR-009: ignore discovery error, fall back to bootstrap
	pool := BuildResolverPool(ServerMode{Mode: "default"}, info)
	fmt.Fprintf(w, "[dns] Resolver pool: [%s]\n", strings.Join(pool, ", "))
	return resolveDefault(hostname, pool, info.DNSServers, config, 0, w)
}

// resolveDefault runs the full smart-default resolution pipeline:
//  1. Stage 1: Parallel NS fan-out across all resolver × label-level combinations.
//  2. Stage 2: Non-recursive A query to the most-specific authoritative NS.
//     2.5. Split-horizon cross-check (runs concurrently with Stage 2).
//  3. Stage 3: Parallel A fallback if no NS records found.
//
// depth tracks CNAME chain hops; exceeding maxCNAMEDepth returns an error.
func resolveDefault(hostname string, pool []string, localResolvers []string, config QueryConfig, depth int, w io.Writer) ([]string, error) {
	if depth > maxCNAMEDepth {
		return nil, fmt.Errorf(
			"CNAME chain depth exceeded for %s (max %d hops): probable CNAME loop or misconfigured zone",
			hostname, maxCNAMEDepth)
	}

	labels := ExtractLabelLevels(hostname)

	ctx := context.Background()
	timeout := config.Timeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	// Stage 1: Parallel NS fan-out.
	fmt.Fprintf(w, "[dns] Stage 1: NS fan-out for %s (%d levels × %d resolvers = %d queries)\n",
		hostname, len(labels), len(pool), len(labels)*len(pool))
	nsResults := ParallelNSFanOut(ctx, w, pool, labels, timeout)
	authority := SelectAuthoritativeNS(nsResults)

	if authority != nil {
		fmt.Fprintf(w, "[dns] Selected authority: %s → %s\n",
			authority.Zone, strings.Join(authority.Nameservers, ", "))
		ips, err := resolveAuthoritative(ctx, authority, hostname, pool, localResolvers, config, depth, w)
		if err == nil {
			return ips, nil
		}
		// Stage 2 failed (e.g. sub-zone delegation referral not followed) —
		// fall through to Stage 3 parallel A fallback.
		fmt.Fprintf(w, "[dns] Stage 2 failed: %v\n", err)
		fmt.Fprintf(w, "[dns] Falling back to Stage 3 parallel A queries\n")
	} else {
		fmt.Fprintf(w, "[dns] Stage 1: No NS records found at any level\n")
	}

	// Stage 3: Fall back to parallel recursive A queries.
	ips, err := ParallelAFallback(ctx, w, pool, hostname, timeout)
	if err != nil {
		return nil, addPrivateTLDHint(hostname, err)
	}
	return ips, nil
}

// resolveAuthoritative handles Stage 2 and Stage 2.5, plus CNAME restart.
//
// Stage 2.5 (split-horizon cross-check) is fired as a goroutine concurrently
// with Stage 2 so it adds zero wall-clock latency to the happy path.
func resolveAuthoritative(ctx context.Context, authority *AuthoritativeNS, hostname string, pool []string, localResolvers []string, config QueryConfig, depth int, w io.Writer) ([]string, error) {
	timeout := config.Timeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	// Stage 2.5: fire local cross-check concurrently (FR-033, FR-037, FR-038).
	type shOut struct {
		localIPs []string
		localSrc string
	}
	var shCh chan shOut
	if len(localResolvers) > 0 {
		shCh = make(chan shOut, 1)
		go func() {
			localIPs, localSrc := queryLocalResolvers(ctx, localResolvers, hostname, timeout)
			shCh <- shOut{localIPs: localIPs, localSrc: localSrc}
		}()
	}

	// Stage 2: Query authoritative NS with RD=false.
	ips, cnameTarget, nsHostnameUsed, err := QueryAuthoritative(ctx, w, authority, hostname, pool, timeout)
	if err != nil {
		return nil, addPrivateTLDHint(hostname, err)
	}

	if cnameTarget != "" {
		// CNAME: restart full resolution for the target, incrementing depth.
		return resolveDefault(cnameTarget, pool, localResolvers, config, depth+1, w)
	}

	// Stage 2.5: Collect cross-check result and compare IP sets.
	if shCh != nil {
		sh := <-shCh
		if sh.localIPs != nil {
			fmt.Fprintf(w, "[dns] Stage 2.5: Split-horizon cross-check against local resolvers [%s]\n",
				strings.Join(localResolvers, ", "))
			if !ipSetsEqual(ips, sh.localIPs) {
				fmt.Fprintf(w, "[dns]   %s → %s (differs from authoritative %s)\n",
					sh.localSrc, strings.Join(sh.localIPs, ", "), strings.Join(ips, ", "))
				fmt.Fprintf(w, "[dns] CONFLICT: authoritative and local resolvers disagree\n")
				return nil, fmt.Errorf(
					"conflicting DNS answers for %s\n"+
						"  Authoritative (%s): %s\n"+
						"  Local resolver (%s): %s\n"+
						"  The hostname resolves to different IPs depending on the DNS source.\n"+
						"  Use -server local to trust your internal DNS, or -server <ip> to choose explicitly.",
					hostname,
					nsHostnameUsed, strings.Join(ips, ", "),
					sh.localSrc, strings.Join(sh.localIPs, ", "))
			}
			fmt.Fprintf(w, "[dns]   %s → %s (matches authoritative)\n",
				sh.localSrc, strings.Join(sh.localIPs, ", "))
			fmt.Fprintf(w, "[dns] No conflict detected\n")
		}
	}

	return ips, nil
}

// queryLocalResolvers tries each localResolver in order and returns the first
// successful A record result. Returns nil, "" if all fail or return NXDOMAIN.
func queryLocalResolvers(ctx context.Context, localResolvers []string, hostname string, timeout time.Duration) ([]string, string) {
	for _, lr := range localResolvers {
		queryCtx, cancel := context.WithTimeout(ctx, timeout)
		ips, err := queryA(queryCtx, lr, hostname, timeout)
		cancel()
		if err == nil && len(ips) > 0 {
			return ips, lr
		}
	}
	return nil, ""
}

// resolveLocal queries only the locally configured DNS servers in priority order.
// No public resolver fallback is used (FR-026).
func resolveLocal(hostname string, config QueryConfig, discoverer platform.NetworkDiscoverer) ([]string, error) {
	info, err := discoverer.Discover()
	if err != nil || len(info.DNSServers) == 0 {
		return nil, fmt.Errorf("failed to resolve %s using local resolvers: no local DNS servers found", hostname)
	}

	timeout := config.Timeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	ctx := context.Background()
	for _, server := range info.DNSServers {
		ips, queryErr := queryA(ctx, server, hostname, timeout)
		if queryErr == nil && len(ips) > 0 {
			return ips, nil
		}
	}

	return nil, fmt.Errorf(
		"failed to resolve %s using local resolvers: all local DNS servers are unreachable\n  Local resolvers tried: %s",
		hostname, strings.Join(info.DNSServers, ", "))
}

// resolveGateway queries the default gateway as a DNS server.
// No other resolvers are tried (FR-027).
func resolveGateway(hostname string, config QueryConfig, discoverer platform.NetworkDiscoverer) ([]string, error) {
	info, err := discoverer.Discover()
	if err != nil || info.Gateway == "" {
		return nil, fmt.Errorf("failed to resolve %s using gateway: no default gateway found", hostname)
	}

	timeout := config.Timeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	ctx := context.Background()
	ips, queryErr := queryA(ctx, info.Gateway, hostname, timeout)
	if queryErr != nil {
		return nil, fmt.Errorf(
			"failed to resolve %s using gateway: gateway %s did not respond to DNS query",
			hostname, info.Gateway)
	}
	return ips, nil
}

// resolveExplicit queries the server address provided directly by the user.
// Uses the per-query timeout from config to respect the -timeout flag (FR-029).
func resolveExplicit(hostname string, mode ServerMode, config QueryConfig) ([]string, error) {
	timeout := config.Timeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}
	ctx := context.Background()
	ips, err := queryA(ctx, mode.ExplicitAddr, hostname, timeout)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve %s via %s: %w", hostname, mode.ExplicitAddr, err)
	}
	return ips, nil
}

// addPrivateTLDHint wraps err with a user-friendly hint when the hostname uses
// a private TLD (FR-025).
func addPrivateTLDHint(hostname string, origErr error) error {
	parts := strings.Split(strings.TrimSuffix(hostname, "."), ".")
	if len(parts) > 0 {
		tld := strings.ToLower(parts[len(parts)-1])
		if PrivateTLDs[tld] {
			return fmt.Errorf(
				"%w\n  Hint: the hostname uses a private TLD (.%s). Try specifying an internal DNS server:\n        ekdns add -host %s -server <internal-dns-ip>",
				origErr, tld, hostname)
		}
	}
	return origErr
}
