package utils

import (
	"context"
	"fmt"
	"maps"
	"os"
	"path/filepath"
	"regexp"
	"strconv"

	"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
	"k8s.io/client-go/tools/clientcmd"

	"github.com/stackitcloud/stackit-sdk-go/services/ske"
	"golang.org/x/mod/semver"
)

const (
	defaultNodepoolCRI              = ske.CRINAME_CONTAINERD
	defaultNodepoolMachineImageName = "flatcar"
	defaultNodepoolMaxUnavailable   = 0
	defaultNodepoolMinimum          = 1
	defaultNodepoolName             = "pool-default"
	defaultNodepoolVolumeType       = "storage_premium_perf2"
	defaultNodepoolVolumeSize       = 50

	supportedState = "supported"
)

type SKEClient interface {
	ListClustersExecute(ctx context.Context, projectId, region string) (*ske.ListClustersResponse, error)
	ListProviderOptionsExecute(ctx context.Context, region string) (*ske.ProviderOptions, error)
}

func ClusterExists(ctx context.Context, apiClient SKEClient, projectId, region, clusterName string) (bool, error) {
	clusters, err := apiClient.ListClustersExecute(ctx, projectId, region)
	if err != nil {
		return false, fmt.Errorf("list SKE clusters: %w", err)
	}
	for _, cl := range *clusters.Items {
		if cl.Name != nil && *cl.Name == clusterName {
			return true, nil
		}
	}
	return false, nil
}

func GetDefaultPayload(ctx context.Context, apiClient SKEClient, region string) (*ske.CreateOrUpdateClusterPayload, error) {
	resp, err := apiClient.ListProviderOptionsExecute(ctx, region)
	if err != nil {
		return nil, fmt.Errorf("get SKE provider options: %w", err)
	}

	payloadKubernetes, err := getDefaultPayloadKubernetes(resp)
	if err != nil {
		return nil, err
	}
	payloadNodepool, err := getDefaultPayloadNodepool(resp)
	if err != nil {
		return nil, err
	}

	payload := &ske.CreateOrUpdateClusterPayload{
		Extensions: &ske.Extension{
			Acl: &ske.ACL{
				AllowedCidrs: &[]string{},
				Enabled:      utils.Ptr(false),
			},
		},
		Kubernetes: payloadKubernetes,
		Nodepools: &[]ske.Nodepool{
			*payloadNodepool,
		},
	}
	return payload, nil
}

func getDefaultPayloadKubernetes(resp *ske.ProviderOptions) (*ske.Kubernetes, error) {
	output := &ske.Kubernetes{}

	if resp.KubernetesVersions == nil {
		return nil, fmt.Errorf("no supported Kubernetes version found")
	}
	foundKubernetesVersion := false
	versions := *resp.KubernetesVersions
	for i := range versions {
		version := versions[i]
		if *version.State != supportedState {
			continue
		}
		if output.Version != nil {
			oldSemVer := fmt.Sprintf("v%s", *output.Version)
			newSemVer := fmt.Sprintf("v%s", *version.Version)
			if semver.Compare(newSemVer, oldSemVer) != 1 {
				continue
			}
		}

		foundKubernetesVersion = true
		output.Version = version.Version
	}
	if !foundKubernetesVersion {
		return nil, fmt.Errorf("no supported Kubernetes version found")
	}
	return output, nil
}

func getDefaultPayloadNodepool(resp *ske.ProviderOptions) (*ske.Nodepool, error) {
	if resp.AvailabilityZones == nil || len(*resp.AvailabilityZones) == 0 {
		return nil, fmt.Errorf("no availability zones found")
	}
	var availabilityZones []string
	for i := range *resp.AvailabilityZones {
		azName := (*resp.AvailabilityZones)[i].GetName()
		// don't include availability zones like eu01-m, eu02-m, not all flavors are available there
		if !regexp.MustCompile(`\w{2}\d{2}-m`).MatchString(azName) {
			availabilityZones = append(availabilityZones, azName)
		}
	}

	if resp.MachineTypes == nil || len(*resp.MachineTypes) == 0 {
		return nil, fmt.Errorf("no machine types found")
	}
	machineType := (*resp.MachineTypes)[0].GetName()

	output := &ske.Nodepool{
		AvailabilityZones: &availabilityZones,
		Cri: &ske.CRI{
			Name: utils.Ptr(defaultNodepoolCRI),
		},
		Machine: &ske.Machine{
			Type: &machineType,
			Image: &ske.Image{
				Name: utils.Ptr(defaultNodepoolMachineImageName),
			},
		},
		// there must be as many nodes as availability zones are given
		MaxSurge:       utils.Ptr(int64(len(availabilityZones))),
		MaxUnavailable: utils.Ptr(int64(defaultNodepoolMaxUnavailable)),
		Maximum:        utils.Ptr(int64(len(availabilityZones))),
		Minimum:        utils.Ptr(int64(defaultNodepoolMinimum)),
		Name:           utils.Ptr(defaultNodepoolName),
		Volume: &ske.Volume{
			Type: utils.Ptr(defaultNodepoolVolumeType),
			Size: utils.Ptr(int64(defaultNodepoolVolumeSize)),
		},
	}

	// Fill in Cri and Machine.Image
	if resp.MachineImages == nil {
		return nil, fmt.Errorf("no supported image versions found")
	}
	foundImageVersion := false
	images := *resp.MachineImages
	for i := range images {
		image := images[i]
		if *image.Name != defaultNodepoolMachineImageName {
			continue
		}
		if image.Versions == nil {
			continue
		}
		versions := *image.Versions
		for j := range versions {
			version := versions[j]
			if *version.State != supportedState {
				continue
			}

			// Check if default CRI is supported
			if version.Cri == nil || len(*version.Cri) == 0 {
				continue
			}
			criSupported := false
			for k := range *version.Cri {
				cri := (*version.Cri)[k]
				if *cri.Name == defaultNodepoolCRI {
					criSupported = true
					break
				}
			}
			if !criSupported {
				continue
			}

			if output.Machine.Image.Version != nil {
				oldSemVer := fmt.Sprintf("v%s", *output.Machine.Image.Version)
				newSemVer := fmt.Sprintf("v%s", *version.Version)
				if semver.Compare(newSemVer, oldSemVer) != 1 {
					continue
				}
			}

			foundImageVersion = true
			output.Machine.Image.Version = version.Version
		}
	}
	if !foundImageVersion {
		return nil, fmt.Errorf("no supported images found")
	}

	return output, nil
}

// ConvertToSeconds	converts a time string to seconds.
// The time string must be in the format of <value><unit>, where unit is one of s, m, h, d, M.
func ConvertToSeconds(timeStr string) (*string, error) {
	if len(timeStr) < 2 {
		return nil, fmt.Errorf("invalid time: %s", timeStr)
	}

	unit := timeStr[len(timeStr)-1:]

	valueStr := timeStr[:len(timeStr)-1]
	value, err := strconv.ParseUint(valueStr, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid time value: %s", valueStr)
	}

	var multiplier uint64
	switch unit {
	// second
	case "s":
		multiplier = 1
	// minute
	case "m":
		multiplier = 60
	// hour
	case "h":
		multiplier = 60 * 60
	// day
	case "d":
		multiplier = 60 * 60 * 24
	// month, assume 30 days
	case "M":
		multiplier = 60 * 60 * 24 * 30
	default:
		return nil, fmt.Errorf("invalid time unit: %s", unit)
	}

	result := uint64(value) * multiplier
	return utils.Ptr(strconv.FormatUint(result, 10)), nil
}

// Merge new Kubeconfig into existing Kubeconfig. If it doesn´t exits, creates a new one
func MergeKubeConfig(pathDestionationKubeConfig, contentNewKubeConfig string) error {
	if contentNewKubeConfig == "" {
		return fmt.Errorf("no data to merge. the new kubeconfig is empty")
	}

	newConfig, err := clientcmd.Load([]byte(contentNewKubeConfig))
	if err != nil {
		return fmt.Errorf("error loading new kubeconfig: %w", err)
	}

	// if the destionation kubeconfig does not exist, create a new one
	if _, err := os.Stat(pathDestionationKubeConfig); os.IsNotExist(err) {
		return WriteConfigFile(pathDestionationKubeConfig, contentNewKubeConfig)
	}

	existingConfig, err := clientcmd.LoadFromFile(pathDestionationKubeConfig)
	if err != nil {
		return fmt.Errorf("error loading existing kubeconfig: %w", err)
	}

	maps.Copy(existingConfig.AuthInfos, newConfig.AuthInfos)
	maps.Copy(existingConfig.Contexts, newConfig.Contexts)
	maps.Copy(existingConfig.Clusters, newConfig.Clusters)

	err = clientcmd.WriteToFile(*existingConfig, pathDestionationKubeConfig)
	if err != nil {
		return fmt.Errorf("error writing merged kubeconfig: %w", err)
	}

	return nil
}

// WriteConfigFile writes the given data to the given path.
// The directory is created if it does not exist.
func WriteConfigFile(configPath, data string) error {
	if data == "" {
		return fmt.Errorf("no data to write")
	}

	dir := filepath.Dir(configPath)

	err := os.MkdirAll(dir, 0o700)
	if err != nil {
		return fmt.Errorf("create config directory: %w", err)
	}

	err = os.WriteFile(configPath, []byte(data), 0o600)
	if err != nil {
		return fmt.Errorf("write file: %w", err)
	}
	return nil
}

// GetDefaultKubeconfigPath returns the default location for the kubeconfig file or the value of KUBECONFIG if set.
func GetDefaultKubeconfigPath() (string, error) {
	if kubeconfigEnv := os.Getenv("KUBECONFIG"); kubeconfigEnv != "" {
		return kubeconfigEnv, nil
	}

	userHome, err := os.UserHomeDir()
	if err != nil {
		return "", fmt.Errorf("get user home directory: %w", err)
	}

	return filepath.Join(userHome, ".kube", "config"), nil
}
