package api

import (
	"fmt"
	"os"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/cluster"
)

type CloudStackConfig struct {
	datacenterConfig *anywherev1.CloudStackDatacenterConfig
	machineConfigs   map[string]*anywherev1.CloudStackMachineConfig
}

type CloudStackFiller func(config CloudStackConfig)

// CloudStackToConfigFiller transforms a set of CloudStackFiller's in a single ClusterConfigFiller.
func CloudStackToConfigFiller(fillers ...CloudStackFiller) ClusterConfigFiller {
	return func(c *cluster.Config) {
		updateCloudStack(c, fillers...)
	}
}

func updateCloudStack(config *cluster.Config, fillers ...CloudStackFiller) {
	cc := CloudStackConfig{
		datacenterConfig: config.CloudStackDatacenter,
		machineConfigs:   config.CloudStackMachineConfigs,
	}

	for _, f := range fillers {
		f(cc)
	}
}

func WithCloudStackComputeOfferingForAllMachines(value string) CloudStackFiller {
	return func(config CloudStackConfig) {
		for _, m := range config.machineConfigs {
			m.Spec.ComputeOffering.Name = value
		}
	}
}

func WithCloudStackAz(az anywherev1.CloudStackAvailabilityZone) CloudStackFiller {
	return func(config CloudStackConfig) {
		config.datacenterConfig.Spec.AvailabilityZones = append(config.datacenterConfig.Spec.AvailabilityZones, az)
	}
}

func RemoveCloudStackAzs() CloudStackFiller {
	return func(config CloudStackConfig) {
		config.datacenterConfig.Spec.AvailabilityZones = make([]anywherev1.CloudStackAvailabilityZone, 0)
	}
}

func WithCloudStackAffinityGroupIds(value []string) CloudStackFiller {
	return func(config CloudStackConfig) {
		for _, m := range config.machineConfigs {
			m.Spec.AffinityGroupIds = value
		}
	}
}

func WithUserCustomDetails(value map[string]string) CloudStackFiller {
	return func(config CloudStackConfig) {
		for _, m := range config.machineConfigs {
			m.Spec.UserCustomDetails = value
		}
	}
}

func WithSymlinks(value map[string]string) CloudStackFiller {
	return func(config CloudStackConfig) {
		for _, m := range config.machineConfigs {
			m.Spec.Symlinks = value
		}
	}
}

func WithCloudStackTemplateForAllMachines(value string) CloudStackFiller {
	return func(config CloudStackConfig) {
		for _, m := range config.machineConfigs {
			m.Spec.Template.Name = value
		}
	}
}

func WithCloudStackConfigNamespace(ns string) CloudStackFiller {
	return func(config CloudStackConfig) {
		config.datacenterConfig.Namespace = ns
		for _, m := range config.machineConfigs {
			m.Namespace = ns
		}
	}
}

func WithCloudStackSSHAuthorizedKey(value string) CloudStackFiller {
	return func(config CloudStackConfig) {
		for _, m := range config.machineConfigs {
			if len(m.Spec.Users) == 0 {
				m.Spec.Users = []anywherev1.UserConfiguration{{Name: "capc"}}
			}
			m.Spec.Users[0].SshAuthorizedKeys = []string{value}
		}
	}
}

func WithCloudStackDomain(value string) CloudStackFiller {
	return func(config CloudStackConfig) {
		for _, az := range config.datacenterConfig.Spec.AvailabilityZones {
			az.Domain = value
		}
	}
}

func WithCloudStackAccount(value string) CloudStackFiller {
	return func(config CloudStackConfig) {
		for _, az := range config.datacenterConfig.Spec.AvailabilityZones {
			az.Account = value
		}
	}
}

// WithCloudStackCredentialsRef returns a CloudStackFiller that updates the edentialsRef of all availability zones.
func WithCloudStackCredentialsRef(value string) CloudStackFiller {
	return func(config CloudStackConfig) {
		zones := []anywherev1.CloudStackAvailabilityZone{}
		for _, az := range config.datacenterConfig.Spec.AvailabilityZones {
			az.CredentialsRef = value
			zones = append(zones, az)
		}

		config.datacenterConfig.Spec.AvailabilityZones = zones
	}
}

func WithCloudStackStringFromEnvVar(envVar string, opt func(string) CloudStackFiller) CloudStackFiller {
	return opt(os.Getenv(envVar))
}

func WithCloudStackAzFromEnvVars(cloudstackAccountVar, cloudstackDomainVar, cloudstackZoneVar, cloudstackCredentialsVar, cloudstackNetworkVar, cloudstackManagementServerVar string, opt func(zone anywherev1.CloudStackAvailabilityZone) CloudStackFiller) CloudStackFiller {
	az := anywherev1.CloudStackAvailabilityZone{
		Name:           strings.ToLower(fmt.Sprintf("az-%s", os.Getenv(cloudstackZoneVar))),
		CredentialsRef: os.Getenv(cloudstackCredentialsVar),
		Zone: anywherev1.CloudStackZone{
			Name: os.Getenv(cloudstackZoneVar),
			Network: anywherev1.CloudStackResourceIdentifier{
				Name: os.Getenv(cloudstackNetworkVar),
			},
		},
		Domain:                os.Getenv(cloudstackDomainVar),
		Account:               os.Getenv(cloudstackAccountVar),
		ManagementApiEndpoint: os.Getenv(cloudstackManagementServerVar),
	}
	return opt(az)
}

func WithCloudStackMachineConfig(name string, fillers ...CloudStackMachineConfigFiller) CloudStackFiller {
	return func(config CloudStackConfig) {
		m, ok := config.machineConfigs[name]
		if !ok {
			m = &anywherev1.CloudStackMachineConfig{
				TypeMeta: metav1.TypeMeta{
					Kind:       anywherev1.CloudStackMachineConfigKind,
					APIVersion: anywherev1.SchemeBuilder.GroupVersion.String(),
				},
				ObjectMeta: metav1.ObjectMeta{
					Name: name,
				},
			}
			config.machineConfigs[name] = m
		}

		FillCloudStackMachineConfig(m, fillers...)
	}
}

// WithCloudStackConfigNamespaceForAllMachinesAndDatacenter sets the namespace for all Machines and Datacenter objects.
func WithCloudStackConfigNamespaceForAllMachinesAndDatacenter(ns string) CloudStackFiller {
	return func(config CloudStackConfig) {
		config.datacenterConfig.Namespace = ns
		for _, m := range config.machineConfigs {
			m.Namespace = ns
		}
	}
}