package runtimeconfig

import (
	"net"
	"net/url"
	"os"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

const mcpLocalNoProxyEnvVar = "AGENTTEAM_MCP_LOCAL_NO_PROXY"

var defaultLoopbackNoProxy = []string{"127.0.0.1", "localhost", "::1"}

type loopbackNoProxyMode uint8

const (
	loopbackNoProxyAuto loopbackNoProxyMode = iota
	loopbackNoProxyOn
	loopbackNoProxyOff
)

// RuntimeEnvOverrides clones the runtime env overrides and appends standard
// loopback NO_PROXY entries when any MCP server is served from localhost. This
// keeps runtimes talking to the local daemon directly even when a system proxy
// is configured.
func RuntimeEnvOverrides(extra map[string]string, mcps []types.NamedMCPConfig) map[string]string {
	env := CloneStringMap(extra)
	env = runtimeEnvOverridesWithShellPath(env)
	mode := parseLoopbackNoProxyMode(firstNonEmpty(env[mcpLocalNoProxyEnvVar], os.Getenv(mcpLocalNoProxyEnvVar)))
	if mode == loopbackNoProxyOff {
		return env
	}

	loopbackHosts := collectLoopbackMCPHosts(mcps)
	if len(loopbackHosts) == 0 && mode != loopbackNoProxyOn {
		return env
	}

	if env == nil {
		env = make(map[string]string, 2)
	}

	additions := append([]string(nil), defaultLoopbackNoProxy...)
	additions = append(additions, loopbackHosts...)

	env["NO_PROXY"] = mergeNoProxyEntries(firstNonEmpty(env["NO_PROXY"], os.Getenv("NO_PROXY")), additions...)
	env["no_proxy"] = mergeNoProxyEntries(firstNonEmpty(env["no_proxy"], os.Getenv("no_proxy")), additions...)
	return env
}

// RuntimeEnvOverridesWithLoopbackURLs appends standard NO_PROXY entries when a
// runtime-owned local endpoint, such as the AgentTeam MaaS LLM bridge, is injected
// outside the MCP config list.
func RuntimeEnvOverridesWithLoopbackURLs(extra map[string]string, rawURLs ...string) map[string]string {
	env := CloneStringMap(extra)
	loopbackHosts := collectLoopbackURLHosts(rawURLs)
	if len(loopbackHosts) == 0 {
		return env
	}
	if env == nil {
		env = make(map[string]string, 2)
	}
	additions := append([]string(nil), defaultLoopbackNoProxy...)
	additions = append(additions, loopbackHosts...)
	env["NO_PROXY"] = mergeNoProxyEntries(firstNonEmpty(env["NO_PROXY"], os.Getenv("NO_PROXY")), additions...)
	env["no_proxy"] = mergeNoProxyEntries(firstNonEmpty(env["no_proxy"], os.Getenv("no_proxy")), additions...)
	return env
}

func collectLoopbackMCPHosts(mcps []types.NamedMCPConfig) []string {
	seen := make(map[string]struct{})
	var hosts []string
	for _, mcp := range mcps {
		host := loopbackURLHost(mcp.URL)
		if host == "" {
			continue
		}
		key := strings.ToLower(host)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		hosts = append(hosts, host)
	}
	return hosts
}

func collectLoopbackURLHosts(rawURLs []string) []string {
	seen := make(map[string]struct{})
	var hosts []string
	for _, rawURL := range rawURLs {
		host := loopbackURLHost(rawURL)
		if host == "" {
			continue
		}
		key := strings.ToLower(host)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		hosts = append(hosts, host)
	}
	return hosts
}

func loopbackMCPHost(rawURL string) string {
	return loopbackURLHost(rawURL)
}

func loopbackURLHost(rawURL string) string {
	parsed, err := url.Parse(strings.TrimSpace(rawURL))
	if err != nil {
		return ""
	}

	host := strings.TrimSpace(parsed.Hostname())
	if host == "" {
		return ""
	}
	if strings.EqualFold(host, "localhost") {
		return "localhost"
	}

	ip := net.ParseIP(host)
	if ip == nil || !ip.IsLoopback() {
		return ""
	}
	if ip.To4() != nil {
		return ip.String()
	}
	return "::1"
}

func mergeNoProxyEntries(existing string, additions ...string) string {
	seen := make(map[string]struct{})
	var entries []string

	add := func(raw string) {
		value := strings.TrimSpace(raw)
		if value == "" {
			return
		}
		key := strings.ToLower(value)
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		entries = append(entries, value)
	}

	for _, part := range strings.Split(existing, ",") {
		add(part)
	}
	for _, part := range additions {
		add(part)
	}
	return strings.Join(entries, ",")
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}

func parseLoopbackNoProxyMode(raw string) loopbackNoProxyMode {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "auto":
		return loopbackNoProxyAuto
	case "on", "true", "1", "yes":
		return loopbackNoProxyOn
	case "off", "false", "0", "no":
		return loopbackNoProxyOff
	default:
		return loopbackNoProxyAuto
	}
}

// CloneStringMap returns a shallow copy of the input map. Returns nil for nil or
// empty input.
func CloneStringMap(input map[string]string) map[string]string {
	if len(input) == 0 {
		return nil
	}
	result := make(map[string]string, len(input))
	for key, value := range input {
		result[key] = value
	}
	return result
}
