package kas

import (
	"fmt"
	"log/slog"
	"net"
	"net/netip"
	"os"
	"strconv"
	"strings"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/nettool"
)

const (
	envVarOwnPrivateAPIURL     = "OWN_PRIVATE_API_URL"
	envVarOwnPrivateAPICIDRs   = "OWN_PRIVATE_API_CIDR"
	envVarOwnPrivateAPIScheme  = "OWN_PRIVATE_API_SCHEME"
	envVarOwnPrivateAPIPort    = "OWN_PRIVATE_API_PORT"
	envVarOwnPrivateAPITLSHost = "OWN_PRIVATE_API_HOST"
)

type privateAPIServerURL struct {
	log            *slog.Logger
	interfaceAddrs func() ([]net.Addr, error)
}

func (ou *privateAPIServerURL) constructFromEnvironment(listenNetwork, listenAddress string, isTLS bool) (nettool.MultiURL, error) {
	return ou.construct(
		os.Getenv(envVarOwnPrivateAPIURL),
		os.Getenv(envVarOwnPrivateAPICIDRs),
		os.Getenv(envVarOwnPrivateAPIScheme),
		os.Getenv(envVarOwnPrivateAPITLSHost),
		os.Getenv(envVarOwnPrivateAPIPort),
		listenNetwork,
		listenAddress,
		isTLS,
	)
}

func (ou *privateAPIServerURL) construct(ownURL, ownCIDRs, ownScheme, ownTLSHost, ownPort, listenNetwork, listenAddress string, isTLS bool) (nettool.MultiURL, error) {
	u, err := ou.constructOwnURLInternal(ownURL, ownCIDRs, ownScheme, ownTLSHost, ownPort, listenNetwork, listenAddress, isTLS)
	if err != nil {
		return nettool.MultiURL{}, err
	}
	for _, addr := range u.Addresses() {
		if !isAcceptableAddr(addr) {
			ou.log.Warn("Own private API multi URL contains a loopback or a link-local IP address(es). "+
				"It's fine to use it if you are running a single kas replica. "+
				"However, if you are running multiple kas replicas, they will likely not work correctly. "+
				"See https://docs.gitlab.com/ee/administration/clusters/kas.html#enable-on-multiple-nodes for more information",
				logz.URL(ownURL),
			)
			break
		}
	}
	if ownTLSHost == "" && isTLS {
		ou.log.Info(fmt.Sprintf("%s environment variable is not set. Please set it if you want to "+
			"override the server name used for KAS->KAS TLS communication", envVarOwnPrivateAPITLSHost))
	}
	return u, nil
}

func (ou *privateAPIServerURL) constructOwnURLInternal(ownURL, ownCIDRs, ownScheme, ownTLSHost, ownPort, listenNetwork,
	listenAddress string, isTLS bool) (nettool.MultiURL, error) {

	if ownURL != "" {
		if ownCIDRs != "" {
			return nettool.MultiURL{}, fmt.Errorf("either %s or %s should be specified, not both", envVarOwnPrivateAPIURL, envVarOwnPrivateAPICIDRs)
		}
		return ou.detectURLFromEnvVar(ownURL, ownTLSHost)
	}

	var (
		addrs []netip.Addr
		host  string
		port  uint16
		err   error
	)

	if ownCIDRs != "" {
		addrs, err = ou.detectIPsByCIDRs(ownCIDRs)
	} else {
		addrs, host, err = ou.detectIPsFromListenAddress(listenNetwork, listenAddress)
	}
	if err != nil {
		return nettool.MultiURL{}, err
	}

	// Determine port
	if ownPort == "" {
		port, err = portFromListenAddress(listenNetwork, listenAddress)
		if err != nil {
			return nettool.MultiURL{}, err
		}
	} else {
		port, err = parsePort(ownPort)
		if err != nil {
			return nettool.MultiURL{}, fmt.Errorf("error parsing %s environment variable: %w", envVarOwnPrivateAPIPort, err)
		}
	}

	// Determine scheme
	scheme, err := detectOwnScheme(ownScheme, isTLS)
	if err != nil {
		return nettool.MultiURL{}, err
	}

	switch {
	case host != "":
		return nettool.NewMultiURLForHost(scheme, host, ownTLSHost, port), nil
	case len(addrs) > 0:
		return nettool.NewMultiURLForAddresses(scheme, ownTLSHost, port, addrs), nil
	default:
		// this shouldn't happen because detectIPsByCIDRs() and detectIPsFromListenAddress() return a error if they cannot determine a host or IPs
		return nettool.MultiURL{}, fmt.Errorf("unexpected error: could determine own private API URL")
	}
}

func (ou *privateAPIServerURL) detectURLFromEnvVar(ownURL, ownTLSHost string) (nettool.MultiURL, error) {
	u, err := nettool.ParseURL(ownURL, ownTLSHost)
	if err != nil {
		return nettool.MultiURL{}, fmt.Errorf("failed to parse %s %s: %w", envVarOwnPrivateAPIURL, ownURL, err)
	}
	return u, err
}

func (ou *privateAPIServerURL) detectIPsByCIDRs(ownCIDRs string) ([]netip.Addr, error) {
	addrs, err := ou.interfaceAddrs()
	if err != nil {
		return nil, fmt.Errorf("net.InterfaceAddrs(): %w", err)
	}

	var foundIPs []netip.Addr

	for ownCIDR := range strings.SplitSeq(ownCIDRs, ",") {
		_, ipNet, err := net.ParseCIDR(ownCIDR)
		if err != nil {
			return nil, fmt.Errorf("failed to parse %s environment variable: %w", envVarOwnPrivateAPICIDRs, err)
		}
		for _, addr := range addrs {
			addrIP, ok := addr.(*net.IPNet)
			if !ok {
				continue
			}
			if !ipNet.Contains(addrIP.IP) {
				continue
			}
			ip, valid := nettool.IP2Addr(addrIP.IP)
			if !valid {
				// this shouldn't ever happen
				return nil, fmt.Errorf("error parsing IP address %s", addrIP.IP)
			}
			foundIPs = append(foundIPs, ip)
		}
	}
	if len(foundIPs) == 0 {
		return nil, fmt.Errorf("no IPs matched CIDR(s) specified in the %s environment variable", envVarOwnPrivateAPICIDRs)
	}
	return foundIPs, nil
}

func (ou *privateAPIServerURL) detectIPsFromListenAddress(listenNetwork, listenAddress string) ([]netip.Addr, string, error) {
	switch listenNetwork {
	case "tcp", "tcp4", "tcp6": // assume listenAddress is a ip:port or name:port
		listenHost, _, err := net.SplitHostPort(listenAddress)
		if err != nil {
			return nil, "", fmt.Errorf("listener address: %w", err)
		}
		if listenHost == "" { // all IPs
			lTCP := listenNetwork == "tcp"
			addrs, err := ou.interfaceIPs(lTCP || listenNetwork == "tcp4", lTCP || listenNetwork == "tcp6") //nolint:govet
			return addrs, "", err
		}
		ip, err := netip.ParseAddr(listenHost)
		if err != nil { // not an IP. Assume it's a hostname
			return nil, listenHost, nil //nolint:nilerr
		}
		if ip.Zone() != "" {
			return nil, "", fmt.Errorf(
				"cannot determine URL. Please set %s or %s environment variable or change private API listen address to listen on all interfaces (currently set to %q)",
				envVarOwnPrivateAPIURL, envVarOwnPrivateAPICIDRs, listenAddress)
		}
		if ip.IsUnspecified() { // all v4 or v6 IPs - '0.0.0.0' or '::'
			lTCP := listenNetwork == "tcp"
			addrs, err := ou.interfaceIPs(lTCP || listenNetwork == "tcp4", lTCP || listenNetwork == "tcp6")
			return addrs, "", err
		}
		return []netip.Addr{ip}, "", nil
	default:
		return nil, "", fmt.Errorf("unsupported network type specified in the listener config: %s", listenNetwork)
	}
}

func (ou *privateAPIServerURL) interfaceIPs(addV4, addV6 bool) ([]netip.Addr, error) {
	addrs, err := ou.interfaceAddrs()
	if err != nil {
		return nil, fmt.Errorf("net.InterfaceAddrs(): %w", err)
	}
	var preferredIPs []netip.Addr
	for _, addr := range addrs {
		addrIP, ok := addr.(*net.IPNet)
		if !ok {
			continue
		}
		ip, valid := nettool.IP2Addr(addrIP.IP)
		if !valid {
			// this shouldn't ever happen
			return nil, fmt.Errorf("error parsing IP address %s", addrIP.IP)
		}
		if !isAcceptableAddr(ip) {
			// We assume link-local and loopback addresses are never used in production in multi-node kas setup.
			// If you want to use them (e.g. in a development setup), explicitly set OWN_PRIVATE_API_URL.
			// We could allow such addresses to simplify development setup, but that may lead to
			// kas not detecting misconfiguration when run in a bad production environment
			// (without a proper IP address).
			continue
		}
		if (addV4 && ip.Is4()) || (addV6 && ip.Is6()) {
			preferredIPs = append(preferredIPs, ip)
		}
	}
	if len(preferredIPs) == 0 {
		return nil, fmt.Errorf("cannot determine URL - no matching network interfaces found. Please set %s environment variable", envVarOwnPrivateAPIURL)
	}
	return preferredIPs, nil
}

func detectOwnScheme(ownScheme string, isTLS bool) (string, error) {
	switch ownScheme {
	case "grpc", "grpcs":
		return ownScheme, nil
	case "":
		if isTLS {
			return "grpcs", nil
		}
		return "grpc", nil
	default:
		return "", fmt.Errorf("%s environment variable should be either grpc or grpcs, got: %s", envVarOwnPrivateAPIScheme, ownScheme)
	}
}

func portFromListenAddress(listenNetwork, listenAddress string) (uint16, error) {
	switch listenNetwork {
	case "tcp", "tcp4", "tcp6": // assume listenAddress is a ip:port or name:port
		_, listenPort, err := net.SplitHostPort(listenAddress)
		if err != nil {
			return 0, fmt.Errorf("listener address: %w", err)
		}
		// parse listenPort
		port, err := parsePort(listenPort)
		if err != nil {
			return 0, fmt.Errorf("listener address port: %w", err)
		}
		return port, nil
	default:
		return 0, fmt.Errorf("unsupported network type specified in the listener config: %s", listenNetwork)
	}
}

func isAcceptableAddr(addr netip.Addr) bool {
	return !addr.IsLoopback() && !addr.IsLinkLocalUnicast()
}

func parsePort(portStr string) (uint16, error) {
	port, err := strconv.ParseUint(portStr, 10, 16)
	if err != nil {
		return 0, fmt.Errorf("parsing port: %w", err)
	}
	if port == 0 {
		return 0, fmt.Errorf("invalid port: %d", port)
	}
	return uint16(port), nil
}
