package variable

import (
	"net"
	"regexp"
	"slices"
	"strconv"
	"strings"

	"github.com/cockroachdb/errors"
	"k8s.io/apimachinery/pkg/util/rand"
	"k8s.io/klog/v2"

	_const "github.com/kubesphere/kubekey/v4/pkg/const"
	"github.com/kubesphere/kubekey/v4/pkg/converter/tmpl"
)

// ***************************** GetFunc ***************************** //

// GetHostnames retrieves all hostnames from specified groups or hosts.
// It supports various hostname patterns including direct hostnames, group names,
// indexed group access (e.g., "group[0]"), and random selection (e.g., "group|random").
// The function also supports template parsing for hostnames using configuration variables.
var GetHostnames = func(name []string) GetFunc {
	if len(name) == 0 {
		return emptyGetFunc
	}

	return func(v Variable) (any, error) {
		vv, ok := v.(*variable)
		if !ok {
			return nil, errors.New("variable type error")
		}
		var hs []string
		for _, n := range name {
			// Try to parse hostname using configuration variables as template context
			if pn, err := tmpl.ParseFunc(Extension2Variables(vv.value.Config.Spec), n, func(b []byte) string { return string(b) }); err == nil {
				n = pn
			}
			// Add direct hostname if it exists in the hosts map
			if _, exists := vv.value.Hosts[n]; exists {
				hs = append(hs, n)
			}
			// Add all hosts from matching groups
			for gn, gv := range ConvertGroup(vv.value.Inventory) {
				if gn == n {
					hs = CombineSlice(hs, gv)
					break
				}
			}

			// Handle indexed group access (e.g., "group[0]")
			regexForIndex := regexp.MustCompile(`^(.*?)\[(\d+)\]$`)
			if match := regexForIndex.FindStringSubmatch(strings.TrimSpace(n)); match != nil {
				index, err := strconv.Atoi(match[2])
				if err != nil {
					return nil, errors.Wrapf(err, "failed to convert %q to int", match[2])
				}
				if group, ok := ConvertGroup(vv.value.Inventory)[match[1]]; ok {
					if index >= len(group) {
						return nil, errors.Errorf("index %v out of range for group %s", index, group)
					}
					hs = append(hs, group[index])
				}
			}

			// Handle random host selection from group (e.g., "group|random")
			regexForRandom := regexp.MustCompile(`^(.+?)\s*\|\s*random$`)
			if match := regexForRandom.FindStringSubmatch(strings.TrimSpace(n)); match != nil {
				if group, ok := ConvertGroup(vv.value.Inventory)[match[1]]; ok {
					hs = append(hs, group[rand.Intn(len(group))])
				}
			}
		}

		return hs, nil
	}
}

// GetAllVariable retrieves all variables for a given host, including group variables,
// remote variables, runtime variables, inventory variables, and configuration variables.
// It also sets default variables for localhost and provides access to global host and group information.
var GetAllVariable = func(hostname string) GetFunc {
	// getLocalIP retrieves the IPv4 or IPv6 address for the localhost machine
	getLocalIP := func(ipType string) string {
		addrs, err := net.InterfaceAddrs()
		if err != nil {
			klog.ErrorS(err, "get network address error")
		}

		for _, addr := range addrs {
			if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
				if ipType == _const.VariableIPv4 && ipNet.IP.To4() != nil {
					return ipNet.IP.String()
				}

				if ipType == _const.VariableIPv6 && ipNet.IP.To16() != nil && ipNet.IP.To4() == nil {
					return ipNet.IP.String()
				}
			}
		}

		klog.V(4).Infof("cannot get local %s address", ipType)

		return ""
	}

	// defaultHostVariable sets default variables when hostname is "localhost"
	// It automatically detects and sets IPv4/IPv6 addresses and hostname information
	defaultHostVariable := func(hostname string, hostVars map[string]any) {
		if hostname == _const.VariableLocalHost {
			if _, ok := hostVars[_const.VariableIPv4]; !ok {
				hostVars[_const.VariableIPv4] = getLocalIP(_const.VariableIPv4)
			}
			if _, ok := hostVars[_const.VariableIPv6]; !ok {
				hostVars[_const.VariableIPv6] = getLocalIP(_const.VariableIPv6)
			}
		}
		if os, ok := hostVars[_const.VariableOS]; ok {
			// Try to set hostname by current actual hostname from OS information
			if osd, ok := os.(map[string]any); ok {
				hostVars[_const.VariableHostName] = osd[_const.VariableOSHostName]
			}
		}
		if _, ok := hostVars[_const.VariableInventoryName]; !ok {
			hostVars[_const.VariableInventoryName] = hostname
		}
		if _, ok := hostVars[_const.VariableHostName]; !ok {
			hostVars[_const.VariableHostName] = hostname
		}
	}

	// getHostsVariable builds a complete variable map for all hosts
	// by combining variables from multiple sources in a specific order
	getHostsVariable := func(v *variable) map[string]any {
		globalHosts := make(map[string]any)
		for hostname := range v.value.Hosts {
			hostVars := make(map[string]any)
			// Set group variables for hosts that belong to groups
			for _, gv := range v.value.Inventory.Spec.Groups {
				if slices.Contains(gv.Hosts, hostname) {
					hostVars = CombineVariables(hostVars, Extension2Variables(gv.Vars))
				}
			}
			// Merge remote variables (variables collected from the actual host)
			hostVars = CombineVariables(hostVars, v.value.Hosts[hostname].RemoteVars)
			// Merge runtime variables (variables set during playbook execution)
			hostVars = CombineVariables(hostVars, v.value.Hosts[hostname].RuntimeVars)

			// Merge inventory-level variables
			hostVars = CombineVariables(hostVars, Extension2Variables(v.value.Inventory.Spec.Vars))
			// Merge host-specific variables from inventory
			hostVars = CombineVariables(hostVars, Extension2Variables(v.value.Inventory.Spec.Hosts[hostname]))
			// Merge configuration variables
			hostVars = CombineVariables(hostVars, Extension2Variables(v.value.Config.Spec))
			// Set default variables for localhost
			defaultHostVariable(hostname, hostVars)
			globalHosts[hostname] = hostVars
		}

		return globalHosts
	}

	return func(v Variable) (any, error) {
		vv, ok := v.(*variable)
		if !ok {
			return nil, errors.New("variable type error")
		}
		hosts := getHostsVariable(vv)
		hostVars, ok := hosts[hostname].(map[string]any)
		if !ok {
			// Return empty map if host variables cannot be found
			return make(map[string]any), nil
		}
		// Add global hosts information to the host variables
		hostVars = CombineVariables(hostVars, map[string]any{
			_const.VariableGlobalHosts: hosts,
		})
		// Add group information to the host variables
		hostVars = CombineVariables(hostVars, map[string]any{
			_const.VariableGroups: ConvertGroup(vv.value.Inventory),
		})

		return hostVars, nil
	}
}

// GetHostMaxLength calculates the maximum length of all hostnames.
// This is useful for formatting output or determining display widths.
var GetHostMaxLength = func() GetFunc {
	return func(v Variable) (any, error) {
		vv, ok := v.(*variable)
		if !ok {
			return nil, errors.New("variable type error")
		}
		var hostnameMaxLen int
		for k := range vv.value.Hosts {
			hostnameMaxLen = max(len(k), hostnameMaxLen)
		}

		return hostnameMaxLen, nil
	}
}

// GetWorkDir returns the working directory from the configuration.
var GetWorkDir = func() GetFunc {
	return func(v Variable) (any, error) {
		vv, ok := v.(*variable)
		if !ok {
			return nil, errors.New("variable type error")
		}

		return _const.GetWorkdirFromConfig(vv.value.Config), nil
	}
}

// GetResultVariable returns the global result variables.
// This function retrieves the result variables that are set globally and accessible across all hosts.
var GetResultVariable = func() GetFunc {
	return func(v Variable) (any, error) {
		vv, ok := v.(*variable)
		if !ok {
			return nil, errors.New("variable type error")
		}

		return vv.value.Result[resultKey], nil
	}
}
