package executables

import (
	"bufio"
	"bytes"
	"context"
	"crypto/tls"
	_ "embed"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"sigs.k8s.io/yaml"

	"github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/config"
	"github.com/aws/eks-anywhere/pkg/filewriter"
	"github.com/aws/eks-anywhere/pkg/logger"
	"github.com/aws/eks-anywhere/pkg/retrier"
	"github.com/aws/eks-anywhere/pkg/types"
)

const (
	govcPath             = "govc"
	govcUsernameKey      = "GOVC_USERNAME"
	govcPasswordKey      = "GOVC_PASSWORD"
	govcURLKey           = "GOVC_URL"
	govcInsecure         = "GOVC_INSECURE"
	govcDatacenterKey    = "GOVC_DATACENTER"
	govcTlsHostsFile     = "govc_known_hosts"
	govcTlsKnownHostsKey = "GOVC_TLS_KNOWN_HOSTS"
	vSphereServerKey     = "VSPHERE_SERVER"
	byteToGiB            = 1073741824.0
	DeployOptsFile       = "deploy-opts.json"
	disk1                = "Hard disk 1"
	disk2                = "Hard disk 2"
	MemoryAvailable      = "Memory_Available"
)

var requiredEnvs = []string{govcUsernameKey, govcPasswordKey, govcURLKey, govcInsecure, govcDatacenterKey}

type networkMapping struct {
	Name    string `json:"Name,omitempty"`
	Network string `json:"Network,omitempty"`
}

type deployOption struct {
	DiskProvisioning string           `json:"DiskProvisioning,omitempty"`
	NetworkMapping   []networkMapping `json:"NetworkMapping,omitempty"`
}

type FolderType string

const (
	datastore     FolderType = "datastore"
	vm            FolderType = "vm"
	maxRetries               = 5
	backOffPeriod            = 5 * time.Second
)

type Govc struct {
	writer filewriter.FileWriter
	Executable
	*retrier.Retrier
	requiredEnvs *syncSlice
	envMap       map[string]string
}

type GovcOpt func(*Govc)

func NewGovc(executable Executable, writer filewriter.FileWriter, opts ...GovcOpt) *Govc {
	envVars := newSyncSlice()
	envVars.append(requiredEnvs...)

	g := &Govc{
		writer:       writer,
		Executable:   executable,
		Retrier:      retrier.NewWithMaxRetries(maxRetries, backOffPeriod),
		requiredEnvs: envVars,
	}

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

	return g
}

func WithGovcEnvMap(envMap map[string]string) GovcOpt {
	return func(g *Govc) {
		g.envMap = envMap
	}
}

func (g *Govc) exec(ctx context.Context, args ...string) (stdout bytes.Buffer, err error) {
	envMap, err := g.validateAndSetupCreds()
	if err != nil {
		return bytes.Buffer{}, fmt.Errorf("failed govc validations: %v", err)
	}

	return g.ExecuteWithEnv(ctx, envMap, args...)
}

func (g *Govc) Close(ctx context.Context) error {
	if g == nil {
		return nil
	}

	return g.Logout(ctx)
}

func (g *Govc) Logout(ctx context.Context) error {
	logger.V(3).Info("Logging out from current govc session")
	if _, err := g.exec(ctx, "session.logout"); err != nil {
		return fmt.Errorf("govc returned error when logging out: %v", err)
	}

	// Commands that skip cert verification will have a different session.
	// So we try to destroy it as well here to avoid leaving it orphaned
	if _, err := g.exec(ctx, "session.logout", "-k"); err != nil {
		return fmt.Errorf("govc returned error when logging out from session without cert verification: %v", err)
	}

	return nil
}

// SearchTemplate looks for a vm template with the same base name as the provided template path.
// If found, it returns the full qualified path to the template.
// If multiple matching templates are found, it returns an error.
func (g *Govc) SearchTemplate(ctx context.Context, datacenter, template string) (string, error) {
	params := []string{"find", "-json", "/" + datacenter, "-type", "VirtualMachine", "-name", filepath.Base(template)}

	var templateResponse bytes.Buffer
	var err error
	err = g.Retry(func() error {
		templateResponse, err = g.exec(ctx, params...)
		return err
	})
	if err != nil {
		return "", fmt.Errorf("getting template: %v", err)
	}

	templateJson := templateResponse.String()
	templateJson = strings.TrimSuffix(templateJson, "\n")
	if templateJson == "null" || templateJson == "" {
		logger.V(2).Info(fmt.Sprintf("Template not found: %s", template))
		return "", nil
	}

	templateInfo := make([]string, 0)
	if err = json.Unmarshal([]byte(templateJson), &templateInfo); err != nil {
		logger.V(2).Info(fmt.Sprintf("Failed unmarshalling govc response: %s, %v", templateJson, err))
		return "", nil
	}

	bTemplateFound := false
	var foundTemplate string
	for _, t := range templateInfo {
		if strings.HasSuffix(t, template) {
			if bTemplateFound {
				return "", fmt.Errorf("specified template '%s' maps to multiple paths within the datacenter '%s'", template, datacenter)
			}
			bTemplateFound = true
			foundTemplate = t
		}
	}
	if !bTemplateFound {
		logger.V(2).Info(fmt.Sprintf("Template '%s' not found", template))
		return "", nil
	}

	return foundTemplate, nil
}

func (g *Govc) LibraryElementExists(ctx context.Context, library string) (bool, error) {
	response, err := g.exec(ctx, "library.ls", library)
	if err != nil {
		return false, fmt.Errorf("govc failed getting library to check if it exists: %v", err)
	}

	return response.Len() > 0, nil
}

type libElement struct {
	ContentVersion string `json:"content_version"`
}

func (g *Govc) GetLibraryElementContentVersion(ctx context.Context, element string) (string, error) {
	response, err := g.exec(ctx, "library.info", "-json", element)
	if err != nil {
		return "", fmt.Errorf("govc failed getting library element info: %v", err)
	}
	elementInfoJson := response.String()
	elementInfoJson = strings.TrimSuffix(elementInfoJson, "\n")
	if elementInfoJson == "null" {
		return "-1", nil
	}

	elementInfo := make([]libElement, 0)
	err = yaml.Unmarshal([]byte(elementInfoJson), &elementInfo)
	if err != nil {
		return "", fmt.Errorf("unmarshalling library element info: %v", err)
	}

	if len(elementInfo) == 0 {
		return "", fmt.Errorf("govc failed to return element info for library element %v", element)
	}
	return elementInfo[0].ContentVersion, nil
}

func (g *Govc) DeleteLibraryElement(ctx context.Context, element string) error {
	_, err := g.exec(ctx, "library.rm", element)
	if err != nil {
		return fmt.Errorf("govc failed deleting library item: %v", err)
	}

	return nil
}

func (g *Govc) ResizeDisk(ctx context.Context, datacenter, template, diskName string, diskSizeInGB int) error {
	_, err := g.exec(ctx, "vm.disk.change", "-dc", datacenter, "-vm", template, "-disk.name", diskName, "-size", strconv.Itoa(diskSizeInGB)+"G")
	if err != nil {
		return fmt.Errorf("failed to resize disk %s: %v", diskName, err)
	}
	return nil
}

type deviceInfoResponse struct {
	Devices []VirtualDevice
}

// VirtualDevice describes a virtual device for a VM.
type VirtualDevice struct {
	Name         string
	DeviceInfo   deviceInfo
	CapacityInKB float64
}

type deviceInfo struct {
	Label string
}

// DevicesInfo returns the device info for te provided virtual machine.
func (g *Govc) DevicesInfo(ctx context.Context, datacenter, template string, args ...string) ([]VirtualDevice, error) {
	params := []string{"device.info", "-dc", datacenter, "-vm", template, "-json"}
	params = append(params, args...)
	response, err := g.exec(ctx, params...)
	if err != nil {
		return nil, fmt.Errorf("getting template device information: %v", err)
	}

	var devicesInfo deviceInfoResponse
	err = yaml.Unmarshal(response.Bytes(), &devicesInfo)
	if err != nil {
		return nil, fmt.Errorf("unmarshalling devices info: %v", err)
	}
	return devicesInfo.Devices, nil
}

// GetVMDiskSizeInGB returns the size of the first disk on the VM in GB.
func (g *Govc) GetVMDiskSizeInGB(ctx context.Context, vm, datacenter string) (int, error) {
	devicesInfo, err := g.DevicesInfo(ctx, datacenter, vm, "disk-*")
	if err != nil {
		return 0, fmt.Errorf("getting disk size for vm %s: %v", vm, err)
	}

	if len(devicesInfo) == 0 {
		return 0, fmt.Errorf("no disks found for vm %s", vm)
	}

	return int(devicesInfo[0].CapacityInKB / 1024 / 1024), nil
}

// GetHardDiskSize returns the size of all the hard disks for given VM.
func (g *Govc) GetHardDiskSize(ctx context.Context, vm, datacenter string) (map[string]float64, error) {
	devicesInfo, err := g.DevicesInfo(ctx, datacenter, vm, "disk-*")
	if err != nil {
		return nil, fmt.Errorf("getting hard disk sizes for vm %s: %v", vm, err)
	}

	if len(devicesInfo) == 0 {
		return nil, fmt.Errorf("no hard disks found for vm %s", vm)
	}

	hardDiskMap := make(map[string]float64)
	for _, deviceInfo := range devicesInfo {
		if strings.EqualFold(deviceInfo.DeviceInfo.Label, disk1) {
			hardDiskMap[disk1] = deviceInfo.CapacityInKB
		} else if strings.EqualFold(deviceInfo.DeviceInfo.Label, disk2) {
			hardDiskMap[disk2] = deviceInfo.CapacityInKB
		}
	}
	return hardDiskMap, nil
}

func (g *Govc) TemplateHasSnapshot(ctx context.Context, template string) (bool, error) {
	envMap, err := g.validateAndSetupCreds()
	if err != nil {
		return false, fmt.Errorf("failed govc validations: %v", err)
	}

	params := []string{"snapshot.tree", "-vm", template}
	snap, err := g.ExecuteWithEnv(ctx, envMap, params...)
	if err != nil {
		return false, fmt.Errorf("failed to get snapshot details: %v", err)
	}
	if snap.String() == "" {
		return false, nil
	}
	return true, nil
}

type datastoreResponse struct {
	Datastores []types.Datastores `json:"Datastores"`
}

func (g *Govc) GetWorkloadAvailableSpace(ctx context.Context, datastore string) (float64, error) {
	envMap, err := g.validateAndSetupCreds()
	if err != nil {
		return 0, fmt.Errorf("failed govc validations: %v", err)
	}

	params := []string{"datastore.info", "-json=true", datastore}
	result, err := g.ExecuteWithEnv(ctx, envMap, params...)
	if err != nil {
		return 0, fmt.Errorf("getting datastore info: %v", err)
	}

	response := &datastoreResponse{}
	err = json.Unmarshal(result.Bytes(), response)
	if err != nil {
		return -1, nil
	}
	if len(response.Datastores) > 0 {
		freeSpace := response.Datastores[0].Info.FreeSpace
		spaceGiB := freeSpace / byteToGiB
		return spaceGiB, nil
	}
	return 0, fmt.Errorf("getting datastore available space response: %v", err)
}

func (g *Govc) CreateLibrary(ctx context.Context, datastore, library string) error {
	if _, err := g.exec(ctx, "library.create", "-ds", datastore, library); err != nil {
		return fmt.Errorf("creating library %s: %v", library, err)
	}
	return nil
}

func (g *Govc) DeployTemplateFromLibrary(ctx context.Context, templateDir, templateName, library, datacenter, datastore, network, resourcePool string, resizeBRDisk bool) error {
	logger.V(4).Info("Deploying template", "dir", templateDir, "templateName", templateName)

	if err := g.DeployTemplate(ctx, library, templateName, templateName, templateDir, datacenter, datastore, network, resourcePool, nil); err != nil {
		return err
	}

	if resizeBRDisk {
		// Get devices information template to identify second disk properly
		logger.V(4).Info("Getting devices info for template")
		devicesInfo, err := g.DevicesInfo(ctx, datacenter, templateName)
		if err != nil {
			return err
		}
		// For 1.22 we switched to using one disk for BR, so it for now as long as the boolean is set, and we only see
		// one disk, we can assume this is for 1.22. This loop would need to change if that assumption changes
		// in the future, but 1.20 and 1.21 are still using dual disks which is why we need to check for the second
		// disk first. Since this loop will get all kinds of devices and not just hard disks, we need to do these
		// checks based on the label.
		disk1 := ""
		disk2 := ""
		for _, device := range devicesInfo {
			deviceLabel := device.DeviceInfo.Label
			// Get the name of the hard disk and resize the disk to 20G
			if strings.EqualFold(deviceLabel, "Hard disk 1") {
				disk1 = device.Name
			} else if strings.EqualFold(deviceLabel, "Hard disk 2") {
				disk2 = device.Name
				break
			}
		}
		diskName := ""
		var diskSizeInGB int
		if disk2 != "" {
			logger.V(4).Info("Resizing disk 2 of template to 20G")
			diskName = disk2
			diskSizeInGB = 20
		} else if disk1 != "" {
			logger.V(4).Info("Resizing disk 1 of template to 22G")
			diskName = disk1
			diskSizeInGB = 22
		} else {
			return fmt.Errorf("template %v is not valid as there are no associated disks", templateName)
		}

		err = g.ResizeDisk(ctx, datacenter, templateName, diskName, diskSizeInGB)
		if err != nil {
			return fmt.Errorf("resizing disk %v to %dG: %v", diskName, diskSizeInGB, err)
		}
	}

	templateFullPath := filepath.Join(templateDir, templateName)

	logger.V(4).Info("Taking template snapshot", "templateName", templateFullPath)
	if err := g.createVMSnapshot(ctx, datacenter, templateFullPath); err != nil {
		return err
	}

	logger.V(4).Info("Marking vm as template", "templateName", templateFullPath)
	if err := g.markVMAsTemplate(ctx, datacenter, templateFullPath); err != nil {
		return err
	}

	return nil
}

func (g *Govc) ImportTemplate(ctx context.Context, library, ovaURL, name string) error {
	logger.V(4).Info("Importing template", "ova", ovaURL, "templateName", name)
	if _, err := g.exec(ctx, "library.import", "-k", "-pull", "-n", name, library, ovaURL); err != nil {
		return fmt.Errorf("importing template: %v", err)
	}
	return nil
}

func (g *Govc) DeployTemplate(ctx context.Context, library, templateName, vmName, deployFolder, datacenter, datastore, network, resourcePool string, deployOptionsOverride []byte) error {
	envMap, err := g.validateAndSetupCreds()
	if err != nil {
		return fmt.Errorf("failed govc validations: %v", err)
	}

	templateInLibraryPath := filepath.Join(library, templateName)
	if !filepath.IsAbs(templateInLibraryPath) {
		templateInLibraryPath = fmt.Sprintf("/%s", templateInLibraryPath)
	}

	deployOpts, err := getDeployOptions(network)
	if err != nil {
		return err
	}

	if len(deployOptionsOverride) > 0 {
		deployOpts = deployOptionsOverride
	}

	deployOptsPath, err := g.writer.Write(DeployOptsFile, deployOpts, filewriter.PersistentFile)
	if err != nil {
		return fmt.Errorf("failed writing deploy options file to disk: %v", err)
	}

	bFolderNotFound := false
	params := []string{"folder.info", deployFolder}
	err = g.Retry(func() error {
		errBuffer, err := g.ExecuteWithEnv(ctx, envMap, params...)
		errString := strings.ToLower(errBuffer.String())
		if err != nil {
			if !strings.Contains(errString, "not found") {
				return fmt.Errorf("obtaining folder information: %v", err)
			} else {
				bFolderNotFound = true
			}
		}
		return nil
	})
	if err != nil || bFolderNotFound {
		params = []string{"folder.create", deployFolder}
		err = g.Retry(func() error {
			errBuffer, err := g.ExecuteWithEnv(ctx, envMap, params...)
			errString := strings.ToLower(errBuffer.String())
			if err != nil && !strings.Contains(errString, "already exists") {
				return fmt.Errorf("creating folder: %v", err)
			}
			return nil
		})
		if err != nil {
			return fmt.Errorf("creating folder: %v", err)
		}
	}

	params = []string{
		"library.deploy",
		"-dc", datacenter,
		"-ds", datastore,
		"-pool", resourcePool,
		"-folder", deployFolder,
		"-options", deployOptsPath,
		templateInLibraryPath, vmName,
	}
	if _, err := g.exec(ctx, params...); err != nil {
		return fmt.Errorf("deploying template: %v", err)
	}

	return nil
}

func (g *Govc) DeleteTemplate(ctx context.Context, resourcePool, templatePath string) error {
	if err := g.markAsVM(ctx, resourcePool, templatePath); err != nil {
		return err
	}
	if err := g.removeSnapshotsFromVM(ctx, templatePath); err != nil {
		return err
	}
	if err := g.deleteVM(ctx, templatePath); err != nil {
		return err
	}

	return nil
}

func (g *Govc) markAsVM(ctx context.Context, resourcePool, path string) error {
	if _, err := g.exec(ctx, "vm.markasvm", "-pool", resourcePool, path); err != nil {
		return fmt.Errorf("failed marking as vm: %v", err)
	}
	return nil
}

func (g *Govc) removeSnapshotsFromVM(ctx context.Context, path string) error {
	if _, err := g.exec(ctx, "snapshot.remove", "-vm", path, "*"); err != nil {
		return fmt.Errorf("removing snapshots from vm: %v", err)
	}
	return nil
}

func (g *Govc) deleteVM(ctx context.Context, path string) error {
	if _, err := g.exec(ctx, "vm.destroy", path); err != nil {
		return fmt.Errorf("deleting vm: %v", err)
	}
	return nil
}

func (g *Govc) createVMSnapshot(ctx context.Context, datacenter, name string) error {
	if _, err := g.exec(ctx, "snapshot.create", "-dc", datacenter, "-m=false", "-vm", name, "root"); err != nil {
		return fmt.Errorf("govc failed taking vm snapshot: %v", err)
	}
	return nil
}

func (g *Govc) markVMAsTemplate(ctx context.Context, datacenter, vmName string) error {
	if _, err := g.exec(ctx, "vm.markastemplate", "-dc", datacenter, vmName); err != nil {
		return fmt.Errorf("marking VM as template: %v", err)
	}
	return nil
}

func (g *Govc) getEnvMap() (map[string]string, error) {
	if g.envMap != nil {
		return g.envMap, nil
	}

	envMap := make(map[string]string)
	for key := range g.requiredEnvs.iterate() {
		if env, ok := os.LookupEnv(key); ok && len(env) > 0 {
			envMap[key] = env
		} else {
			if key != govcInsecure {
				return nil, fmt.Errorf("warning required env not set %s", key)
			}
			err := os.Setenv(govcInsecure, "false")
			if err != nil {
				logger.Info("Warning: Unable to set <%s>", govcInsecure)
			}
		}
	}

	return envMap, nil
}

func (g *Govc) validateAndSetupCreds() (map[string]string, error) {
	if g.envMap != nil {
		return g.envMap, nil
	}

	var vSphereUsername, vSpherePassword, vSphereURL string
	var ok bool
	var envMap map[string]string
	if vSphereUsername, ok = os.LookupEnv(config.EksavSphereUsernameKey); ok && len(vSphereUsername) > 0 {
		if err := os.Setenv(govcUsernameKey, vSphereUsername); err != nil {
			return nil, fmt.Errorf("unable to set %s: %v", govcUsernameKey, err)
		}
	} else if govcUsername, ok := os.LookupEnv(govcUsernameKey); !ok || len(govcUsername) <= 0 {
		return nil, fmt.Errorf("%s is not set or is empty: %t", govcUsernameKey, ok)
	}
	if vSpherePassword, ok = os.LookupEnv(config.EksavSpherePasswordKey); ok && len(vSpherePassword) > 0 {
		if err := os.Setenv(govcPasswordKey, vSpherePassword); err != nil {
			return nil, fmt.Errorf("unable to set %s: %v", govcPasswordKey, err)
		}
	} else if govcPassword, ok := os.LookupEnv(govcPasswordKey); !ok || len(govcPassword) <= 0 {
		return nil, fmt.Errorf("%s is not set or is empty: %t", govcPasswordKey, ok)
	}
	if vSphereURL, ok = os.LookupEnv(vSphereServerKey); ok && len(vSphereURL) > 0 {
		if err := os.Setenv(govcURLKey, vSphereURL); err != nil {
			return nil, fmt.Errorf("unable to set %s: %v", govcURLKey, err)
		}
	} else if govcURL, ok := os.LookupEnv(govcURLKey); !ok || len(govcURL) <= 0 {
		return nil, fmt.Errorf("%s is not set or is empty: %t", govcURLKey, ok)
	}
	if govcDatacenter, ok := os.LookupEnv(govcDatacenterKey); !ok || len(govcDatacenter) <= 0 {
		return nil, fmt.Errorf("%s is not set or is empty: %t", govcDatacenterKey, ok)
	}

	envMap, err := g.getEnvMap()
	if err != nil {
		return nil, fmt.Errorf("%v", err)
	}

	return envMap, nil
}

func (g *Govc) CleanupVms(ctx context.Context, clusterName string, dryRun bool) error {
	envMap, err := g.validateAndSetupCreds()
	if err != nil {
		return fmt.Errorf("failed govc validations: %v", err)
	}

	var params []string
	var result bytes.Buffer

	params = strings.Fields("find /" + envMap[govcDatacenterKey] + " -type VirtualMachine -name " + clusterName + "*")
	result, err = g.ExecuteWithEnv(ctx, envMap, params...)
	if err != nil {
		return fmt.Errorf("getting vm list: %v", err)
	}
	scanner := bufio.NewScanner(strings.NewReader(result.String()))
	for scanner.Scan() {
		vmName := scanner.Text()
		if dryRun {
			logger.Info("Found ", "vm_name", vmName)
			continue
		}
		params = strings.Fields("vm.power -off -force " + vmName)
		result, err = g.ExecuteWithEnv(ctx, envMap, params...)
		if err != nil {
			logger.Info("WARN: Failed to power off vm ", "vm_name", vmName, "error", err)
		}

		params = strings.Fields("object.destroy " + vmName)
		result, err = g.ExecuteWithEnv(ctx, envMap, params...)
		if err != nil {
			logger.Info("WARN: Failed to delete vm ", "vm_name", vmName, "error", err)
		} else {
			logger.Info("Deleted ", "vm_name", vmName)
		}
	}

	if err := scanner.Err(); err != nil {
		return fmt.Errorf("failure reading output of vm list")
	}
	return nil
}

func (g *Govc) ValidateVCenterConnection(ctx context.Context, server string) error {
	skipVerifyTransport := http.DefaultTransport.(*http.Transport).Clone()
	skipVerifyTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	client := &http.Client{Transport: skipVerifyTransport}

	if _, err := client.Get("https://" + server); err != nil {
		return fmt.Errorf("failed to reach server %s: %v", server, err)
	}

	return nil
}

func (g *Govc) ValidateVCenterAuthentication(ctx context.Context) error {
	err := g.Retry(func() error {
		_, err := g.exec(ctx, "about", "-k")
		return err
	})
	if err != nil {
		return fmt.Errorf("vSphere authentication failed: %v", err)
	}

	return nil
}

func (g *Govc) IsCertSelfSigned(ctx context.Context) bool {
	_, err := g.exec(ctx, "about")
	return err != nil
}

func (g *Govc) GetCertThumbprint(ctx context.Context) (string, error) {
	bufferResponse, err := g.exec(ctx, "about.cert", "-thumbprint", "-k")
	if err != nil {
		return "", fmt.Errorf("unable to retrieve thumbprint: %v", err)
	}

	data := strings.Split(strings.Trim(bufferResponse.String(), "\n"), " ")
	if len(data) != 2 {
		return "", fmt.Errorf("invalid thumbprint format")
	}

	return data[1], nil
}

func (g *Govc) ConfigureCertThumbprint(ctx context.Context, server, thumbprint string) error {
	path, err := g.writer.Write(filepath.Base(govcTlsHostsFile), []byte(fmt.Sprintf("%s %s", server, thumbprint)))
	if err != nil {
		return fmt.Errorf("writing to file %s: %v", govcTlsHostsFile, err)
	}

	if err = os.Setenv(govcTlsKnownHostsKey, path); err != nil {
		return fmt.Errorf("unable to set %s: %v", govcTlsKnownHostsKey, err)
	}

	g.requiredEnvs.append(govcTlsKnownHostsKey)

	return nil
}

func (g *Govc) DatacenterExists(ctx context.Context, datacenter string) (bool, error) {
	exists := false
	err := g.Retry(func() error {
		result, err := g.exec(ctx, "datacenter.info", datacenter)
		if err == nil {
			exists = true
			return nil
		}

		if strings.HasSuffix(result.String(), "not found") {
			exists = false
			return nil
		}

		return err
	})
	if err != nil {
		return false, fmt.Errorf("failed to get datacenter: %v", err)
	}

	return exists, nil
}

func (g *Govc) NetworkExists(ctx context.Context, network string) (bool, error) {
	exists := false

	err := g.Retry(func() error {
		networkResponse, err := g.exec(ctx, "find", "-maxdepth=1", filepath.Dir(network), "-type", "n", "-name", filepath.Base(network))
		if err != nil {
			return err
		}

		if networkResponse.String() == "" {
			exists = false
			return nil
		}

		exists = true
		return nil
	})
	if err != nil {
		return false, fmt.Errorf("failed checking if network '%s' exists: %v", network, err)
	}

	return exists, nil
}

// GetDatastorePath validates and returns the full path to a datastore in the specified datacenter.
// Returns an error if the datastore doesn't exist or if the path is invalid.
func (g *Govc) GetDatastorePath(ctx context.Context, datacenter string, datastorePath string, envMap map[string]string) (string, error) {
	fullPath, err := prependPath(datastore, datastorePath, datacenter)
	if err != nil {
		return "", err
	}

	params := []string{"datastore.info", fullPath}
	err = g.Retry(func() error {
		_, err = g.ExecuteWithEnv(ctx, envMap, params...)
		if err != nil {
			parentPath := filepath.Dir(fullPath)
			isValidDatastorePath := g.isValidPath(ctx, envMap, parentPath)
			if isValidDatastorePath {
				leafDir := filepath.Base(fullPath)
				return fmt.Errorf("valid path, but '%s' is not a datastore", leafDir)
			} else {
				return fmt.Errorf("failed to get datastore: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		return "", fmt.Errorf("failed to get datastore: %v", err)
	}
	logger.MarkPass("Datastore validated")
	return fullPath, nil
}

// GetFolderPath validates or creates a folder in the specified datacenter.
// Returns the full path to the folder or an error if creation fails.
func (g *Govc) GetFolderPath(ctx context.Context, datacenter string, folder string, envMap map[string]string) (string, error) {
	if len(folder) == 0 {
		return "", nil
	}

	fullPath, err := prependPath(vm, folder, datacenter)
	if err != nil {
		return "", err
	}

	params := []string{"folder.info", fullPath}
	err = g.Retry(func() error {
		_, err := g.ExecuteWithEnv(ctx, envMap, params...)
		if err != nil {
			err = g.createFolder(ctx, envMap, fullPath)
			if err != nil {
				currPath := "/" + datacenter + "/"
				dirs := strings.Split(fullPath, "/")
				for _, dir := range dirs[2:] {
					currPath += dir + "/"
					if !g.isValidPath(ctx, envMap, currPath) {
						return fmt.Errorf("%s is an invalid intermediate directory", currPath)
					}
				}
				return err
			}
		}
		return nil
	})
	if err != nil {
		return "", fmt.Errorf("failed to get folder: %v", err)
	}
	logger.MarkPass("Folder validated")
	return fullPath, nil
}

// GetResourcePoolPath finds and validates a resource pool in the specified datacenter.
// Returns an error if the pool doesn't exist or if multiple matching pools are found.
func (g *Govc) GetResourcePoolPath(ctx context.Context, datacenter string, resourcePool string, envMap map[string]string) (string, error) {
	var poolInfoResponse bytes.Buffer
	params := []string{"find", "-json", "/" + datacenter, "-type", "p", "-name", filepath.Base(resourcePool)}

	err := g.Retry(func() error {
		var err error
		poolInfoResponse, err = g.ExecuteWithEnv(ctx, envMap, params...)
		return err
	})
	if err != nil {
		return "", fmt.Errorf("getting resource pool: %v", err)
	}

	poolInfoJson := poolInfoResponse.String()
	poolInfoJson = strings.TrimSuffix(poolInfoJson, "\n")
	if poolInfoJson == "null" || poolInfoJson == "" {
		return "", fmt.Errorf("resource pool '%s' not found", resourcePool)
	}

	poolInfo := make([]string, 0)
	if err = json.Unmarshal([]byte(poolInfoJson), &poolInfo); err != nil {
		return "", fmt.Errorf("failed unmarshalling govc response: %v", err)
	}

	resourcePool = strings.TrimPrefix(resourcePool, "*/")
	bPoolFound := false
	var foundPool string
	for _, p := range poolInfo {
		if strings.HasSuffix(p, resourcePool) {
			if bPoolFound {
				return "", fmt.Errorf("specified resource pool '%s' maps to multiple paths within the datacenter '%s'", resourcePool, datacenter)
			}
			bPoolFound = true
			foundPool = p
		}
	}
	if !bPoolFound {
		return "", fmt.Errorf("resource pool '%s' not found", resourcePool)
	}

	logger.MarkPass("Resource pool validated")
	return foundPool, nil
}

// GetComputeClusterPath finds and validates a compute cluster in the specified datacenter.
// Returns an error if the compute cluster doesn't exist or if multiple matching compute clusters are found.
func (g *Govc) GetComputeClusterPath(ctx context.Context, datacenter string, computeCluster string, envMap map[string]string) (string, error) {
	var computeClusterResponse bytes.Buffer
	params := []string{"find", "-json", "/" + datacenter, "-type", "c", "-name", filepath.Base(computeCluster)}

	err := g.Retry(func() error {
		var err error
		computeClusterResponse, err = g.ExecuteWithEnv(ctx, envMap, params...)
		return err
	})
	if err != nil {
		return "", fmt.Errorf("getting compute cluster: %v", err)
	}

	computeClusterJSON := computeClusterResponse.String()
	computeClusterJSON = strings.TrimSuffix(computeClusterJSON, "\n")
	if computeClusterJSON == "null" || computeClusterJSON == "" {
		return "", fmt.Errorf("compute cluster '%s' not found", computeCluster)
	}

	computeClusterInfo := make([]string, 0)
	if err = json.Unmarshal([]byte(computeClusterJSON), &computeClusterInfo); err != nil {
		return "", fmt.Errorf("failed unmarshalling govc response: %v", err)
	}

	computeCluster = strings.TrimPrefix(computeCluster, "*/")
	computeClusterFound := false
	var foundCluster string
	for _, cc := range computeClusterInfo {
		if strings.HasSuffix(cc, computeCluster) {
			if computeClusterFound {
				return "", fmt.Errorf("specified compute cluster '%s' maps to multiple paths within the datacenter '%s'", computeCluster, datacenter)
			}
			computeClusterFound = true
			foundCluster = cc
		}
	}
	if !computeClusterFound {
		return "", fmt.Errorf("compute cluster '%s' not found", computeCluster)
	}

	logger.MarkPass("Compute cluster validated")
	return foundCluster, nil
}

// ValidateVCenterSetupMachineConfig validates that all resources specified in a
// VSphereMachineConfig exist and are accessible.
func (g *Govc) ValidateVCenterSetupMachineConfig(ctx context.Context, datacenterConfig *v1alpha1.VSphereDatacenterConfig, machineConfig *v1alpha1.VSphereMachineConfig, _ *bool) error {
	envMap, err := g.validateAndSetupCreds()
	if err != nil {
		return fmt.Errorf("failed govc validations: %v", err)
	}

	datastore, err := g.GetDatastorePath(ctx, datacenterConfig.Spec.Datacenter, machineConfig.Spec.Datastore, envMap)
	if err != nil {
		return err
	}
	machineConfig.Spec.Datastore = datastore

	folder, err := g.GetFolderPath(ctx, datacenterConfig.Spec.Datacenter, machineConfig.Spec.Folder, envMap)
	if err != nil {
		return err
	}
	machineConfig.Spec.Folder = folder

	resourcePool, err := g.GetResourcePoolPath(ctx, datacenterConfig.Spec.Datacenter, machineConfig.Spec.ResourcePool, envMap)
	if err != nil {
		return err
	}
	machineConfig.Spec.ResourcePool = resourcePool

	return nil
}

// ValidateFailureDomainConfig validates that all resources specified in a VSphere
// failure domain exist and are accessible.
func (g *Govc) ValidateFailureDomainConfig(ctx context.Context, datacenterConfig *v1alpha1.VSphereDatacenterConfig, failureDomain *v1alpha1.FailureDomain) error {
	envMap, err := g.validateAndSetupCreds()
	if err != nil {
		return fmt.Errorf("failed govc validations: %v", err)
	}

	datastore, err := g.GetDatastorePath(ctx, datacenterConfig.Spec.Datacenter, failureDomain.Datastore, envMap)
	if err != nil {
		return err
	}
	failureDomain.Datastore = datastore

	folder, err := g.GetFolderPath(ctx, datacenterConfig.Spec.Datacenter, failureDomain.Folder, envMap)
	if err != nil {
		return err
	}
	failureDomain.Folder = folder

	resourcePool, err := g.GetResourcePoolPath(ctx, datacenterConfig.Spec.Datacenter, failureDomain.ResourcePool, envMap)
	if err != nil {
		return err
	}
	failureDomain.ResourcePool = resourcePool

	computeCluster, err := g.GetComputeClusterPath(ctx, datacenterConfig.Spec.Datacenter, failureDomain.ComputeCluster, envMap)
	if err != nil {
		return err
	}
	failureDomain.ComputeCluster = computeCluster

	return nil
}

func prependPath(folderType FolderType, folderPath string, datacenter string) (string, error) {
	prefix := fmt.Sprintf("/%s", datacenter)
	modPath := folderPath
	if !strings.HasPrefix(folderPath, prefix) {
		modPath = fmt.Sprintf("%s/%s/%s", prefix, folderType, folderPath)
		logger.V(4).Info(fmt.Sprintf("Relative %s path specified, using path %s", folderType, modPath))
		return modPath, nil
	}
	prefix += fmt.Sprintf("/%s", folderType)
	if !strings.HasPrefix(folderPath, prefix) {
		return folderPath, fmt.Errorf("invalid folder type, expected path under %s", prefix)
	}
	return modPath, nil
}

func (g *Govc) createFolder(ctx context.Context, envMap map[string]string, folderPath string) error {
	params := []string{"folder.create", folderPath}
	err := g.Retry(func() error {
		_, err := g.ExecuteWithEnv(ctx, envMap, params...)
		if err != nil {
			return fmt.Errorf("creating folder: %v", err)
		}
		return nil
	})
	return err
}

func (g *Govc) isValidPath(ctx context.Context, envMap map[string]string, path string) bool {
	params := []string{"folder.info", path}
	_, err := g.ExecuteWithEnv(ctx, envMap, params...)
	return err == nil
}

func (g *Govc) GetTags(ctx context.Context, path string) ([]string, error) {
	var tagsResponse bytes.Buffer
	var err error
	err = g.Retry(func() error {
		tagsResponse, err = g.exec(ctx, "tags.attached.ls", "-json", "-r", path)
		return err
	})
	if err != nil {
		return nil, fmt.Errorf("govc returned error when listing tags for %s: %v", path, err)
	}

	tagsJson := tagsResponse.String()
	if tagsJson == "null" {
		return nil, nil
	}

	tags := make([]string, 0)
	if err = json.Unmarshal([]byte(tagsJson), &tags); err != nil {
		return nil, fmt.Errorf("failed unmarshalling govc response to get tags for %s: %v", path, err)
	}

	return tags, nil
}

// Tag struct to represent a vSphere Tag.
type Tag struct {
	Id         string
	Name       string
	CategoryId string `json:"category_id,omitempty"`
}

// ListTags list all vSphere tags in vCenter.
func (g *Govc) ListTags(ctx context.Context) ([]Tag, error) {
	tagsResponse, err := g.exec(ctx, "tags.ls", "-json")
	if err != nil {
		return nil, fmt.Errorf("govc returned error when listing tags: %v", err)
	}

	tagsJson := tagsResponse.String()
	if tagsJson == "null" {
		return nil, nil
	}

	tags := make([]Tag, 0)
	if err = json.Unmarshal([]byte(tagsJson), &tags); err != nil {
		return nil, fmt.Errorf("failed unmarshalling govc response from list tags: %v", err)
	}

	return tags, nil
}

func (g *Govc) AddTag(ctx context.Context, path, tag string) error {
	if _, err := g.exec(ctx, "tags.attach", tag, path); err != nil {
		return fmt.Errorf("govc returned error when attaching tag to %s: %v", path, err)
	}
	return nil
}

func (g *Govc) CreateTag(ctx context.Context, tag, category string) error {
	if _, err := g.exec(ctx, "tags.create", "-c", category, tag); err != nil {
		return fmt.Errorf("govc returned error when creating tag %s: %v", tag, err)
	}
	return nil
}

type category struct {
	Id              string
	Name            string
	Cardinality     string
	AssociableTypes []string `json:"associable_types,omitempty"`
}

func (g *Govc) ListCategories(ctx context.Context) ([]string, error) {
	categoriesResponse, err := g.exec(ctx, "tags.category.ls", "-json")
	if err != nil {
		return nil, fmt.Errorf("govc returned error when listing categories: %v", err)
	}

	categoriesJson := categoriesResponse.String()
	if categoriesJson == "null" {
		return nil, nil
	}

	categories := make([]category, 0)
	if err = json.Unmarshal([]byte(categoriesJson), &categories); err != nil {
		return nil, fmt.Errorf("failed unmarshalling govc response from list categories: %v", err)
	}

	categoryNames := make([]string, 0, len(categories))
	for _, c := range categories {
		categoryNames = append(categoryNames, c.Name)
	}

	return categoryNames, nil
}

type objectType string

const virtualMachine objectType = "VirtualMachine"

func (g *Govc) CreateCategoryForVM(ctx context.Context, name string) error {
	return g.createCategory(ctx, name, []objectType{virtualMachine})
}

func (g *Govc) createCategory(ctx context.Context, name string, objectTypes []objectType) error {
	params := []string{"tags.category.create"}
	for _, t := range objectTypes {
		params = append(params, "-t", string(t))
	}
	params = append(params, name)

	if _, err := g.exec(ctx, params...); err != nil {
		return fmt.Errorf("govc returned error when creating category %s: %v", name, err)
	}
	return nil
}

func getDeployOptions(network string) ([]byte, error) {
	deployOptsStruct := deployOption{
		DiskProvisioning: "thin",
		NetworkMapping: []networkMapping{
			{
				Name:    "nic0", // needed for Ubuntu
				Network: network,
			},
			{
				Name:    "VM Network", // needed for Bottlerocket
				Network: network,
			},
		},
	}

	deployOpts, err := json.Marshal(deployOptsStruct)
	if err != nil {
		return nil, fmt.Errorf("marshalling template deployment options: %v", err)
	}

	return deployOpts, err
}

// CreateUser creates a user.
func (g *Govc) CreateUser(ctx context.Context, username string, password string) error {
	params := []string{
		"sso.user.create", "-p", password, username,
	}

	if _, err := g.exec(ctx, params...); err != nil {
		return fmt.Errorf("govc returned error %v", err)
	}
	return nil
}

// UserExists checks if a user exists.
func (g *Govc) UserExists(ctx context.Context, username string) (bool, error) {
	params := []string{
		"sso.user.ls",
		username,
	}

	response, err := g.exec(ctx, params...)
	if err != nil {
		return false, err
	}

	return response.Len() > 0, nil
}

// CreateGroup creates a group.
func (g *Govc) CreateGroup(ctx context.Context, name string) error {
	params := []string{
		"sso.group.create", name,
	}

	if _, err := g.exec(ctx, params...); err != nil {
		return fmt.Errorf("govc returned error %v", err)
	}

	return nil
}

// GroupExists checks if a group exists.
func (g *Govc) GroupExists(ctx context.Context, name string) (bool, error) {
	params := []string{
		"sso.group.ls",
		name,
	}

	response, err := g.exec(ctx, params...)
	if err != nil && strings.Contains(err.Error(), fmt.Sprintf("group %s doesn't exist", name)) {
		return false, nil
	} else if err != nil {
		return false, err
	}
	// govc returns empty response when group exists
	return response.Len() == 0, nil
}

// AddUserToGroup adds a user to a group.
func (g *Govc) AddUserToGroup(ctx context.Context, name string, username string) error {
	params := []string{
		"sso.group.update",
		"-a", username,
		name,
	}
	if _, err := g.exec(ctx, params...); err != nil {
		return fmt.Errorf("govc returned error %v", err)
	}

	return nil
}

// RoleExists checks if a role exists.
func (g *Govc) RoleExists(ctx context.Context, name string) (bool, error) {
	params := []string{
		"role.ls",
		name,
	}

	_, err := g.exec(ctx, params...)
	if err != nil && strings.Contains(err.Error(), fmt.Sprintf("role \"%s\" not found", name)) {
		return false, nil
	} else if err != nil {
		return false, err
	}

	return true, nil
}

// CreateRole creates a role with specified privileges.
func (g *Govc) CreateRole(ctx context.Context, name string, privileges []string) error {
	params := append([]string{"role.create", name}, privileges...)

	if _, err := g.exec(ctx, params...); err != nil {
		return fmt.Errorf("govc returned error %v", err)
	}

	return nil
}

// SetGroupRoleOnObject sets a role for a given group on target object.
func (g *Govc) SetGroupRoleOnObject(ctx context.Context, principal string, role string, object string, domain string) error {
	principal = principal + "@" + domain

	params := []string{
		"permissions.set",
		"-group=true",
		"-principal", principal,
		"-role", role,
		object,
	}

	if _, err := g.exec(ctx, params...); err != nil {
		return fmt.Errorf("govc returned error %v", err)
	}

	return nil
}

type resourcePoolInfo struct {
	ResourcePoolIdentifier *resourcePool
}

type resourcePool struct {
	memoryUsage string
	memoryLimit string
}

// GetResourcePoolInfo returns the pool info for the provided resource pool.
func (g *Govc) GetResourcePoolInfo(ctx context.Context, datacenter, resourcepool string, args ...string) (map[string]int, error) {
	params := []string{"pool.info", "-dc", datacenter, resourcepool}
	params = append(params, args...)
	response, err := g.exec(ctx, params...)
	if err != nil {
		return nil, fmt.Errorf("getting resource pool information: %v", err)
	}

	scanner := bufio.NewScanner(strings.NewReader(response.String()))
	var resourcePoolInfoResponse resourcePoolInfo
	resourcePoolInfoResponse.ResourcePoolIdentifier = new(resourcePool)
	for scanner.Scan() {
		metaData := scanner.Text()
		if strings.Contains(metaData, "Mem Usage") {
			resourcePoolInfoResponse.ResourcePoolIdentifier.memoryUsage = strings.Split(metaData, ":")[1]
		}
		if strings.Contains(metaData, "Mem Limit") {
			resourcePoolInfoResponse.ResourcePoolIdentifier.memoryLimit = strings.Split(metaData, ":")[1]
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("failure reading memory allocation for resource pool")
	}

	poolInfo, err := getPoolInfo(resourcePoolInfoResponse.ResourcePoolIdentifier)
	if err != nil {
		return nil, err
	}
	return poolInfo, nil
}

// getPoolInfo parses resource pool response and returns memory requirements.
func getPoolInfo(rp *resourcePool) (map[string]int, error) {
	memoryUsed, err := getValueFromString(rp.memoryUsage)
	if err != nil {
		return nil, fmt.Errorf("unable to obtain memory usage for resource pool %s: %v", rp.memoryUsage, err)
	}
	memoryLimit, err := getValueFromString(rp.memoryLimit)
	if err != nil {
		return nil, fmt.Errorf("unable to obtain memory limit for resource pool %s: %v", rp.memoryLimit, err)
	}
	poolInfo := make(map[string]int)
	if memoryLimit != -1 {
		poolInfo[MemoryAvailable] = memoryLimit - memoryUsed
	} else {
		poolInfo[MemoryAvailable] = memoryLimit
	}
	return poolInfo, nil
}

// getValueFromString cleans the input string and returns the extracted numerical value.
func getValueFromString(str string) (int, error) {
	splitResponse := strings.Split(strings.TrimSpace(str), " ")
	nonNumericRegex := regexp.MustCompile(`[^0-9- ]+`)
	cleanedString := nonNumericRegex.ReplaceAllString(splitResponse[0], "")
	numValue, err := strconv.Atoi(cleanedString)
	if err != nil {
		return 0, err
	}
	return numValue, nil
}
