package executables

import (
	"bytes"
	"context"
	_ "embed"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/filewriter"
	"github.com/aws/eks-anywhere/pkg/logger"
	"github.com/aws/eks-anywhere/pkg/providers/cloudstack/decoder"
	"github.com/aws/eks-anywhere/pkg/templater"
)

//go:embed config/cmk.ini
var cmkConfigTemplate string

const (
	cmkPath                           = "cmk"
	cmkConfigFileNameTemplate         = "cmk_%s.ini"
	defaultCloudStackPreflightTimeout = "30"
	rootDomain                        = "ROOT"
	domainDelimiter                   = "/"
)

// Cmk this struct wraps around the CloudMonkey executable CLI to perform operations against a CloudStack endpoint.
type Cmk struct {
	writer     filewriter.FileWriter
	executable Executable
	configMap  map[string]decoder.CloudStackProfileConfig
}

type listTemplatesResponse struct {
	CmkTemplates []cmkTemplate `json:"template"`
}

func (c *Cmk) Close(ctx context.Context) error {
	return nil
}

func (c *Cmk) ValidateTemplatePresent(ctx context.Context, profile string, domainId string, zoneId string, account string, template v1alpha1.CloudStackResourceIdentifier) error {
	command := newCmkCommand("list templates")
	applyCmkArgs(&command, appendArgs("templatefilter=all"), appendArgs("listall=true"))
	if len(template.Id) > 0 {
		applyCmkArgs(&command, withCloudStackId(template.Id))
	} else {
		applyCmkArgs(&command, withCloudStackName(template.Name))
	}

	applyCmkArgs(&command, withCloudStackZoneId(zoneId))
	if len(domainId) > 0 {
		applyCmkArgs(&command, withCloudStackDomainId(domainId))
		if len(account) > 0 {
			applyCmkArgs(&command, withCloudStackAccount(account))
		}
	}
	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return fmt.Errorf("getting templates info - %s: %v", result.String(), err)
	}
	if result.Len() == 0 {
		return fmt.Errorf("template %s not found", template)
	}

	response := listTemplatesResponse{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return fmt.Errorf("parsing response into json: %v", err)
	}
	templates := response.CmkTemplates
	if len(templates) > 1 {
		return fmt.Errorf("duplicate templates %s found", template)
	} else if len(templates) == 0 {
		return fmt.Errorf("template %s not found", template)
	}
	return nil
}

// SearchTemplate looks for a template by name or by id and returns template name if found.
func (c *Cmk) SearchTemplate(ctx context.Context, profile string, template v1alpha1.CloudStackResourceIdentifier) (string, error) {
	command := newCmkCommand("list templates")
	applyCmkArgs(&command, appendArgs("templatefilter=all"), appendArgs("listall=true"))
	if len(template.Id) > 0 {
		applyCmkArgs(&command, withCloudStackId(template.Id))
	} else {
		applyCmkArgs(&command, withCloudStackName(template.Name))
	}

	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return "", fmt.Errorf("getting templates info - %s: %v", result.String(), err)
	}
	if result.Len() == 0 {
		return "", nil
	}

	response := listTemplatesResponse{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return "", fmt.Errorf("parsing response into json: %v", err)
	}
	templates := response.CmkTemplates
	if len(templates) > 1 {
		return "", fmt.Errorf("duplicate templates %s found", template)
	} else if len(templates) == 0 {
		return "", nil
	}
	return templates[0].Name, nil
}

func (c *Cmk) ValidateServiceOfferingPresent(ctx context.Context, profile string, zoneId string, serviceOffering v1alpha1.CloudStackResourceIdentifier) error {
	command := newCmkCommand("list serviceofferings")
	if len(serviceOffering.Id) > 0 {
		applyCmkArgs(&command, withCloudStackId(serviceOffering.Id))
	} else {
		applyCmkArgs(&command, withCloudStackName(serviceOffering.Name))
	}
	applyCmkArgs(&command, withCloudStackZoneId(zoneId))
	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return fmt.Errorf("getting service offerings info - %s: %v", result.String(), err)
	}
	if result.Len() == 0 {
		return fmt.Errorf("service offering %s not found", serviceOffering)
	}

	response := struct {
		CmkServiceOfferings []cmkServiceOffering `json:"serviceoffering"`
	}{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return fmt.Errorf("parsing response into json: %v", err)
	}
	offerings := response.CmkServiceOfferings
	if len(offerings) > 1 {
		return fmt.Errorf("duplicate service offering %s found", serviceOffering)
	} else if len(offerings) == 0 {
		return fmt.Errorf("service offering %s not found", serviceOffering)
	}

	return nil
}

func (c *Cmk) ValidateDiskOfferingPresent(ctx context.Context, profile string, zoneId string, diskOffering v1alpha1.CloudStackResourceDiskOffering) error {
	command := newCmkCommand("list diskofferings")
	if len(diskOffering.Id) > 0 {
		applyCmkArgs(&command, withCloudStackId(diskOffering.Id))
	} else {
		applyCmkArgs(&command, withCloudStackName(diskOffering.Name))
	}
	applyCmkArgs(&command, withCloudStackZoneId(zoneId))
	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return fmt.Errorf("getting disk offerings info - %s: %v", result.String(), err)
	}
	if result.Len() == 0 {
		return fmt.Errorf("disk offering ID/Name %s/%s not found", diskOffering.Id, diskOffering.Name)
	}

	response := struct {
		CmkDiskOfferings []cmkDiskOffering `json:"diskoffering"`
	}{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return fmt.Errorf("parsing response into json: %v", err)
	}
	offerings := response.CmkDiskOfferings
	if len(offerings) > 1 {
		return fmt.Errorf("duplicate disk offering ID/Name %s/%s found", diskOffering.Id, diskOffering.Name)
	} else if len(offerings) == 0 {
		return fmt.Errorf("disk offering ID/Name %s/%s not found", diskOffering.Id, diskOffering.Name)
	}

	if offerings[0].Customized && diskOffering.CustomSize <= 0 {
		return fmt.Errorf("disk offering size %d <= 0 for customized disk offering", diskOffering.CustomSize)
	}
	if !offerings[0].Customized && diskOffering.CustomSize > 0 {
		return fmt.Errorf("disk offering size %d > 0 for non-customized disk offering", diskOffering.CustomSize)
	}
	return nil
}

func (c *Cmk) ValidateAffinityGroupsPresent(ctx context.Context, profile string, domainId string, account string, affinityGroupIds []string) error {
	for _, affinityGroupId := range affinityGroupIds {
		command := newCmkCommand("list affinitygroups")
		applyCmkArgs(&command, withCloudStackId(affinityGroupId))
		// account must be specified with a domainId
		// domainId can be specified without account
		if len(domainId) > 0 {
			applyCmkArgs(&command, withCloudStackDomainId(domainId))
			if len(account) > 0 {
				applyCmkArgs(&command, withCloudStackAccount(account))
			}
		}

		result, err := c.exec(ctx, profile, command...)
		if err != nil {
			return fmt.Errorf("getting affinity group info - %s: %v", result.String(), err)
		}
		if result.Len() == 0 {
			return fmt.Errorf("affinity group %s not found", affinityGroupId)
		}

		response := struct {
			CmkAffinityGroups []cmkAffinityGroup `json:"affinitygroup"`
		}{}
		if err = json.Unmarshal(result.Bytes(), &response); err != nil {
			return fmt.Errorf("parsing response into json: %v", err)
		}
		affinityGroup := response.CmkAffinityGroups
		if len(affinityGroup) > 1 {
			return fmt.Errorf("duplicate affinity group %s found", affinityGroupId)
		} else if len(affinityGroup) == 0 {
			return fmt.Errorf("affinity group %s not found", affinityGroupId)
		}
	}
	return nil
}

func (c *Cmk) ValidateZoneAndGetId(ctx context.Context, profile string, zone v1alpha1.CloudStackZone) (string, error) {
	command := newCmkCommand("list zones")
	if len(zone.Id) > 0 {
		applyCmkArgs(&command, withCloudStackId(zone.Id))
	} else {
		applyCmkArgs(&command, withCloudStackName(zone.Name))
	}
	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return "", fmt.Errorf("getting zones info - %s: %v", result.String(), err)
	}
	if result.Len() == 0 {
		return "", fmt.Errorf("zone %s not found", zone)
	}

	response := struct {
		CmkZones []cmkResourceIdentifier `json:"zone"`
	}{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return "", fmt.Errorf("parsing response into json: %v", err)
	}
	cmkZones := response.CmkZones
	if len(cmkZones) > 1 {
		return "", fmt.Errorf("duplicate zone %s found", zone)
	} else if len(cmkZones) == 0 {
		return "", fmt.Errorf("zone %s not found", zone)
	}
	return cmkZones[0].Id, nil
}

func (c *Cmk) ValidateDomainAndGetId(ctx context.Context, profile string, domain string) (string, error) {
	domainId := ""
	command := newCmkCommand("list domains")
	// "list domains" API does not support querying by domain path, so here we extract the domain name which is the last part of the input domain
	tokens := strings.Split(domain, domainDelimiter)
	domainName := tokens[len(tokens)-1]
	applyCmkArgs(&command, withCloudStackName(domainName), appendArgs("listall=true"))

	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return domainId, fmt.Errorf("getting domain info - %s: %v", result.String(), err)
	}
	if result.Len() == 0 {
		return domainId, fmt.Errorf("domain %s not found", domain)
	}

	response := struct {
		CmkDomains []cmkDomain `json:"domain"`
	}{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return domainId, fmt.Errorf("parsing response into json: %v", err)
	}
	domains := response.CmkDomains
	var domainPath string
	if domain == rootDomain {
		domainPath = rootDomain
	} else {
		domainPath = strings.Join([]string{rootDomain, domain}, domainDelimiter)
	}
	for _, d := range domains {
		if d.Path == domainPath {
			domainId = d.Id
			break
		}
	}
	if domainId == "" {
		return domainId, fmt.Errorf("domain(s) found for domain name %s, but not found a domain with domain path %s", domain, domainPath)
	}

	return domainId, nil
}

// EnsureNoDuplicateNetwork ensures that there are no duplicate networks with the name networkName.
// If it finds duplicates that are not shared networks, it deletes them.
func (c *Cmk) EnsureNoDuplicateNetwork(ctx context.Context, profile string, networkName string) error {
	command := newCmkCommand(fmt.Sprintf("list networks filter=name,id,type keyword=%s", networkName))
	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return fmt.Errorf("getting network info - %s: %v", result.String(), err)
	}

	response := struct {
		CmkNetworks []cmkNetwork `json:"network"`
	}{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return fmt.Errorf("parsing response into json: %v", err)
	}

	for _, network := range response.CmkNetworks {
		if !strings.EqualFold(network.Type, "Shared") {
			command := newCmkCommand(fmt.Sprintf("delete network id=%s force=true", network.ID))
			result, err := c.exec(ctx, profile, command...)
			if err != nil {
				return fmt.Errorf("deleting duplicate network with ID %s - %s: %v", network, result.String(), err)
			}
		}
	}

	return nil
}

func (c *Cmk) ValidateNetworkPresent(ctx context.Context, profile string, domainId string, network v1alpha1.CloudStackResourceIdentifier, zoneId string, account string) error {
	command := newCmkCommand("list networks")
	// account must be specified within a domainId
	// domainId can be specified without account
	if len(domainId) > 0 {
		applyCmkArgs(&command, withCloudStackDomainId(domainId))
		if len(account) > 0 {
			applyCmkArgs(&command, withCloudStackAccount(account))
		}
	}
	applyCmkArgs(&command, withCloudStackZoneId(zoneId))
	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return fmt.Errorf("getting network info - %s: %v", result.String(), err)
	}
	if result.Len() == 0 {
		return fmt.Errorf("network %s not found in zone %s", network, zoneId)
	}

	response := struct {
		CmkNetworks []cmkResourceIdentifier `json:"network"`
	}{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return fmt.Errorf("parsing response into json: %v", err)
	}
	networks := response.CmkNetworks

	// filter by network name -- cmk does not support name= filter
	// if network id and name are both provided, the following code is to confirm name matches return value retrieved by id.
	// if only name is provided, the following code is to only get networks with specified name.

	if len(network.Name) > 0 {
		networks = []cmkResourceIdentifier{}
		for _, net := range response.CmkNetworks {
			if net.Name == network.Name {
				networks = append(networks, net)
			}
		}
	}

	if len(networks) > 1 {
		return fmt.Errorf("duplicate network %s found", network)
	} else if len(networks) == 0 {
		return fmt.Errorf("network %s not found in zoneRef %s", network, zoneId)
	}
	return nil
}

func (c *Cmk) ValidateAccountPresent(ctx context.Context, profile string, account string, domainId string) error {
	// If account is not specified then no need to check its presence
	if len(account) == 0 {
		return nil
	}

	command := newCmkCommand("list accounts")
	applyCmkArgs(&command, withCloudStackName(account), withCloudStackDomainId(domainId))
	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return fmt.Errorf("getting accounts info - %s: %v", result.String(), err)
	}
	if result.Len() == 0 {
		return fmt.Errorf("account %s not found", account)
	}

	response := struct {
		CmkAccounts []cmkAccount `json:"account"`
	}{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return fmt.Errorf("parsing response into json: %v", err)
	}
	accounts := response.CmkAccounts
	if len(accounts) > 1 {
		return fmt.Errorf("duplicate account %s found", account)
	} else if len(accounts) == 0 {
		return fmt.Errorf("account %s not found", account)
	}
	return nil
}

// NewCmk initializes CloudMonkey executable to query CloudStack via CLI.
func NewCmk(executable Executable, writer filewriter.FileWriter, config *decoder.CloudStackExecConfig) (*Cmk, error) {
	if config == nil {
		return nil, fmt.Errorf("nil exec config for CloudMonkey, unable to proceed")
	}
	configMap := make(map[string]decoder.CloudStackProfileConfig, len(config.Profiles))
	for _, profile := range config.Profiles {
		configMap[profile.Name] = profile
	}

	return &Cmk{
		writer:     writer,
		executable: executable,
		configMap:  configMap,
	}, nil
}

func (c *Cmk) GetManagementApiEndpoint(profile string) (string, error) {
	config, exist := c.configMap[profile]
	if exist {
		return config.ManagementUrl, nil
	}
	return "", fmt.Errorf("profile %s does not exist", profile)
}

func (c *Cmk) CleanupVms(ctx context.Context, profile string, clusterName string, dryRun bool) error {
	command := newCmkCommand("list virtualmachines")
	applyCmkArgs(&command, withCloudStackKeyword(clusterName), appendArgs("listall=true"))
	result, err := c.exec(ctx, profile, command...)
	if err != nil {
		return fmt.Errorf("listing virtual machines in cluster %s: %s: %v", clusterName, result.String(), err)
	}
	if result.Len() == 0 {
		logger.Info("virtual machines not found", "cluster", clusterName)
		return nil
	}
	response := struct {
		CmkVirtualMachines []cmkResourceIdentifier `json:"virtualmachine"`
	}{}
	if err = json.Unmarshal(result.Bytes(), &response); err != nil {
		return fmt.Errorf("parsing response into json: %v", err)
	}
	for _, vm := range response.CmkVirtualMachines {
		if dryRun {
			logger.Info("Found ", "vm_name", vm.Name)
			continue
		}
		stopCommand := newCmkCommand("stop virtualmachine")
		applyCmkArgs(&stopCommand, withCloudStackId(vm.Id), appendArgs("forced=true"))
		stopResult, err := c.exec(ctx, profile, stopCommand...)
		if err != nil {
			return fmt.Errorf("stopping virtual machine with name %s and id %s: %s: %v", vm.Name, vm.Id, stopResult.String(), err)
		}
		destroyCommand := newCmkCommand("destroy virtualmachine")
		applyCmkArgs(&destroyCommand, withCloudStackId(vm.Id), appendArgs("expunge=true"))
		destroyResult, err := c.exec(ctx, profile, destroyCommand...)
		if err != nil {
			return fmt.Errorf("destroying virtual machine with name %s and id %s: %s: %v", vm.Name, vm.Id, destroyResult.String(), err)
		}
		logger.Info("Deleted ", "vm_name", vm.Name, "vm_id", vm.Id)
	}

	return nil
}

func (c *Cmk) exec(ctx context.Context, profile string, args ...string) (stdout bytes.Buffer, err error) {
	configFile, err := c.buildCmkConfigFile(profile)
	if err != nil {
		return bytes.Buffer{}, fmt.Errorf("failed cmk validations: %v", err)
	}

	argsWithConfigFile := append([]string{"-c", configFile}, args...)
	return c.executable.Execute(ctx, argsWithConfigFile...)
}

func (c *Cmk) buildCmkConfigFile(profile string) (configFile string, err error) {
	config, exist := c.configMap[profile]
	if !exist {
		return "", fmt.Errorf("profile %s does not exist", profile)
	}

	t := templater.New(c.writer)

	config.Timeout = defaultCloudStackPreflightTimeout
	if timeout, isSet := os.LookupEnv("CLOUDSTACK_PREFLIGHT_TIMEOUT"); isSet {
		if _, err := strconv.ParseUint(timeout, 10, 16); err != nil {
			return "", fmt.Errorf("CLOUDSTACK_PREFLIGHT_TIMEOUT must be a number: %v", err)
		}
		config.Timeout = timeout
	}
	writtenFileName, err := t.WriteToFile(cmkConfigTemplate, config, fmt.Sprintf(cmkConfigFileNameTemplate, profile))
	if err != nil {
		return "", fmt.Errorf("creating file for cmk config: %v", err)
	}
	configFile, err = filepath.Abs(writtenFileName)
	if err != nil {
		return "", fmt.Errorf("failed to generate absolute filepath for generated config file at %s", writtenFileName)
	}

	return configFile, nil
}

type cmkTemplate struct {
	Id       string `json:"id"`
	Name     string `json:"name"`
	Zonename string `json:"zonename"`
}

type cmkServiceOffering struct {
	CpuNumber int    `json:"cpunumber"`
	CpuSpeed  int    `json:"cpuspeed"`
	Memory    int    `json:"memory"`
	Id        string `json:"id"`
	Name      string `json:"name"`
}

type cmkNetwork struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Type string `json:"type"`
}

type cmkResourceIdentifier struct {
	Id   string `json:"id"`
	Name string `json:"name"`
}

type cmkDiskOffering struct {
	Id         string `json:"id"`
	Name       string `json:"name"`
	Customized bool   `json:"iscustomized"`
}

type cmkAffinityGroup struct {
	Type string `json:"type"`
	Id   string `json:"id"`
	Name string `json:"name"`
}

type cmkDomain struct {
	Id   string `json:"id"`
	Name string `json:"name"`
	Path string `json:"path"`
}

type cmkAccount struct {
	RoleType string `json:"roletype"`
	Domain   string `json:"domain"`
	Id       string `json:"id"`
	Name     string `json:"name"`
}
