package v1alpha1

import (
	"fmt"
	"os"
	"strings"

	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/yaml"
)

// NutanixIdentifierType is an enumeration of different resource identifier types.
type NutanixIdentifierType string

func (c NutanixIdentifierType) String() string {
	return string(c)
}

const (
	// NutanixMachineConfigKind is the kind for a NutanixMachineConfig.
	NutanixMachineConfigKind = "NutanixMachineConfig"

	// NutanixIdentifierUUID is a resource identifier identifying the object by UUID.
	NutanixIdentifierUUID NutanixIdentifierType = "uuid"
	// NutanixIdentifierName is a resource identifier identifying the object by Name.
	NutanixIdentifierName NutanixIdentifierType = "name"

	defaultNutanixOSFamily         = Ubuntu
	defaultNutanixSystemDiskSizeGi = "40Gi"
	defaultNutanixMemorySizeGi     = "4Gi"
	defaultNutanixVCPUsPerSocket   = 1
	defaultNutanixVCPUSockets      = 2

	// DefaultNutanixMachineConfigUser is the default username we set in machine config.
	DefaultNutanixMachineConfigUser string = "eksa"
)

// NutanixResourceIdentifier holds the identity of a Nutanix Prism resource (cluster, image, subnet, etc.)
//
// +union.
type NutanixResourceIdentifier struct {
	// Type is the identifier type to use for this resource.
	// +kubebuilder:validation:Required
	// +kubebuilder:validation:Enum:=uuid;name
	Type NutanixIdentifierType `json:"type"`

	// uuid is the UUID of the resource in the PC.
	// +optional
	UUID *string `json:"uuid,omitempty"`

	// name is the resource name in the PC
	// +optional
	Name *string `json:"name,omitempty"`
}

// NutanixCategoryIdentifier holds the identity of a Nutanix Prism Central category.
type NutanixCategoryIdentifier struct {
	// key is the Key of the category in the Prism Central.
	// +kubebuilder:validation:Required
	Key string `json:"key,omitempty"`

	// value is the category value linked to the key in the Prism Central.
	// +kubebuilder:validation:Required
	Value string `json:"value,omitempty"`
}

// NutanixMachineConfigGenerateOpt is a functional option that can be passed to NewNutanixMachineConfigGenerate to
// customize the generated machine config
//
// +kubebuilder:object:generate=false
type NutanixMachineConfigGenerateOpt func(config *NutanixMachineConfigGenerate)

// NewNutanixMachineConfigGenerate returns a new instance of NutanixMachineConfigGenerate
// used for generating yaml for generate clusterconfig command.
func NewNutanixMachineConfigGenerate(name string, opts ...NutanixMachineConfigGenerateOpt) *NutanixMachineConfigGenerate {
	enterNameString := "<Enter %s name here>"
	machineConfig := &NutanixMachineConfigGenerate{
		TypeMeta: metav1.TypeMeta{
			Kind:       NutanixMachineConfigKind,
			APIVersion: SchemeBuilder.GroupVersion.String(),
		},
		ObjectMeta: ObjectMeta{
			Name: name,
		},
		Spec: NutanixMachineConfigSpec{
			OSFamily: defaultNutanixOSFamily,
			Users: []UserConfiguration{
				{
					Name:              DefaultNutanixMachineConfigUser,
					SshAuthorizedKeys: []string{"ssh-rsa AAAA..."},
				},
			},
			VCPUsPerSocket: defaultNutanixVCPUsPerSocket,
			VCPUSockets:    defaultNutanixVCPUSockets,
			MemorySize:     resource.MustParse(defaultNutanixMemorySizeGi),
			Image:          NutanixResourceIdentifier{Type: NutanixIdentifierName, Name: func() *string { s := fmt.Sprintf(enterNameString, "image"); return &s }()},
			Cluster:        NutanixResourceIdentifier{Type: NutanixIdentifierName, Name: func() *string { s := fmt.Sprintf(enterNameString, "Prism Element cluster"); return &s }()},
			Subnet:         NutanixResourceIdentifier{Type: NutanixIdentifierName, Name: func() *string { s := fmt.Sprintf(enterNameString, "subnet"); return &s }()},
			SystemDiskSize: resource.MustParse(defaultNutanixSystemDiskSizeGi),
		},
	}

	for _, opt := range opts {
		opt(machineConfig)
	}

	return machineConfig
}

func (c *NutanixMachineConfigGenerate) APIVersion() string {
	return c.TypeMeta.APIVersion
}

func (c *NutanixMachineConfigGenerate) Kind() string {
	return c.TypeMeta.Kind
}

func (c *NutanixMachineConfigGenerate) Name() string {
	return c.ObjectMeta.Name
}

func GetNutanixMachineConfigs(fileName string) (map[string]*NutanixMachineConfig, error) {
	configs := make(map[string]*NutanixMachineConfig)
	content, err := os.ReadFile(fileName)
	if err != nil {
		return nil, fmt.Errorf("unable to read file due to: %v", err)
	}
	for _, c := range strings.Split(string(content), YamlSeparator) {
		config := NutanixMachineConfig{
			TypeMeta: metav1.TypeMeta{},
			ObjectMeta: metav1.ObjectMeta{
				Annotations: map[string]string{},
			},
		}
		if err = yaml.UnmarshalStrict([]byte(c), &config); err == nil {
			if config.Kind == NutanixMachineConfigKind {
				configs[config.Name] = &config
				continue
			}
		}
		_ = yaml.Unmarshal([]byte(c), &config) // this is to check if there is a bad spec in the file
		if config.Kind == NutanixMachineConfigKind {
			return nil, fmt.Errorf("unable to unmarshall content from file due to: %v", err)
		}
	}
	if len(configs) == 0 {
		return nil, fmt.Errorf("unable to find kind %v in file", NutanixMachineConfigKind)
	}
	return configs, nil
}

func setNutanixMachineConfigDefaults(machineConfig *NutanixMachineConfig) {
	initUser := UserConfiguration{
		Name:              DefaultNutanixMachineConfigUser,
		SshAuthorizedKeys: []string{""},
	}
	if machineConfig.Spec.Users == nil || len(machineConfig.Spec.Users) <= 0 {
		machineConfig.Spec.Users = []UserConfiguration{initUser}
	}

	user := machineConfig.Spec.Users[0]
	if user.Name == "" {
		machineConfig.Spec.Users[0].Name = DefaultNutanixMachineConfigUser
	}

	if user.SshAuthorizedKeys == nil || len(user.SshAuthorizedKeys) <= 0 {
		machineConfig.Spec.Users[0].SshAuthorizedKeys = []string{""}
	}

	if machineConfig.Spec.OSFamily == "" {
		machineConfig.Spec.OSFamily = defaultNutanixOSFamily
	}
}

func validateNutanixMachineConfig(c *NutanixMachineConfig) error {
	if err := validateObjectMeta(c.ObjectMeta); err != nil {
		return fmt.Errorf("NutanixMachineConfig: %v", err)
	}

	if err := validateNutanixReferences(c); err != nil {
		return fmt.Errorf("NutanixMachineConfig: %v", err)
	}

	if err := validateMinimumNutanixMachineSpecs(c); err != nil {
		return fmt.Errorf("NutanixMachineConfig: %v", err)
	}

	if c.Spec.OSFamily != Ubuntu {
		return fmt.Errorf(
			"NutanixMachineConfig: unsupported spec.osFamily (%v); Please use one of the following: %s",
			c.Spec.OSFamily,
			Ubuntu,
		)
	}

	if err := validateMachineConfigUsers(c.Name, NutanixMachineConfigKind, c.Spec.Users); err != nil {
		return err
	}

	return nil
}

func validateMinimumNutanixMachineSpecs(c *NutanixMachineConfig) error {
	if c.Spec.VCPUSockets < defaultNutanixVCPUSockets {
		return fmt.Errorf("NutanixMachineConfig: vcpu sockets must be greater than or equal to %d", defaultNutanixVCPUSockets)
	}

	if c.Spec.VCPUsPerSocket < defaultNutanixVCPUsPerSocket {
		return fmt.Errorf("NutanixMachineConfig: vcpu per socket must be greater than or equal to %d", defaultNutanixVCPUsPerSocket)
	}

	if c.Spec.MemorySize.Cmp(resource.MustParse(defaultNutanixMemorySizeGi)) < 0 {
		return fmt.Errorf("NutanixMachineConfig: memory size must be greater than or equal to %s", defaultNutanixMemorySizeGi)
	}

	if c.Spec.SystemDiskSize.Cmp(resource.MustParse(defaultNutanixSystemDiskSizeGi)) < 0 {
		return fmt.Errorf("NutanixMachineConfig: system disk size must be greater than %s", defaultNutanixSystemDiskSizeGi)
	}

	return nil
}

func validateNutanixReferences(c *NutanixMachineConfig) error {
	if err := validateNutanixResourceReference(&c.Spec.Subnet, "subnet", c.Name); err != nil {
		return err
	}

	if err := validateNutanixResourceReference(&c.Spec.Cluster, "cluster", c.Name); err != nil {
		return err
	}

	if err := validateNutanixResourceReference(&c.Spec.Image, "image", c.Name); err != nil {
		return err
	}

	if c.Spec.Project != nil {
		if err := validateNutanixResourceReference(c.Spec.Project, "project", c.Name); err != nil {
			return err
		}
	}

	if len(c.Spec.AdditionalCategories) > 0 {
		if err := validateNutanixCategorySlice(c.Spec.AdditionalCategories, c.Name); err != nil {
			return err
		}
	}

	return nil
}

func validateNutanixResourceReference(i *NutanixResourceIdentifier, resource string, mcName string) error {
	if i.Type != NutanixIdentifierName && i.Type != NutanixIdentifierUUID {
		return fmt.Errorf("NutanixMachineConfig: invalid identifier type for %s: %s", resource, i.Type)
	}

	if i.Type == NutanixIdentifierName && i.Name == nil {
		return fmt.Errorf("NutanixMachineConfig: missing %s name: %s", resource, mcName)
	} else if i.Type == NutanixIdentifierUUID && i.UUID == nil {
		return fmt.Errorf("NutanixMachineConfig: missing %s UUID: %s", resource, mcName)
	}

	return nil
}

func validateNutanixCategorySlice(i []NutanixCategoryIdentifier, mcName string) error {
	for _, category := range i {
		if category.Key == "" {
			return fmt.Errorf("NutanixMachineConfig: missing category key: %s", mcName)
		}

		if category.Value == "" {
			return fmt.Errorf("NutanixMachineConfig: missing category value for key %s: %s", category.Key, mcName)
		}
	}

	return nil
}