/*
Copyright (c) 2019-2022 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Authors:
Petr Krutov, <petr.krutov@digitalenergy.online>
Stanislav Solovev, <spsolovev@digitalenergy.online>
Kasim Baybikov, <kmbaybikov@basistech.ru>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/*
Terraform DECORT provider - manage resources provided by DECORT (Digital Energy Cloud
Orchestration Technology) with Terraform by Hashicorp.

Source code: https://repository.basistech.ru/BASIS/terraform-provider-decort

Please see README.md to learn where to place source code so that it
builds seamlessly.

Documentation: https://repository.basistech.ru/BASIS/terraform-provider-decort/wiki
*/

package kvmvm

import (
	"context"
	"regexp"
	"sort"
	"strconv"

	log "github.com/sirupsen/logrus"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi/compute"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/controller"

	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func matchComputes(computeList *compute.ListComputes) *compute.ListComputes {
	matched, _ := regexp.Compile(`[a-zA-Z]+\\d+-[a-zA-Z]+\\d+-[a-zA-Z]+\\d+`)
	result := computeList.FilterFunc(func(ic compute.ItemCompute) bool {
		res := matched.Match([]byte(ic.Name))
		return !res
	})

	return &result
}

func utilityComputeExtraDisksConfigure(ctx context.Context, d *schema.ResourceData, m interface{}, do_delta bool, disks compute.ListComputeDisks) error {
	c := m.(*controller.ControllerCfg)

	log.Debugf("utilityComputeExtraDisksConfigure: called for Compute ID %s with do_delta = %t", d.Id(), do_delta)

	old_set, new_set := d.GetChange("extra_disks")

	apiErrCount := 0
	var lastSavedError error

	if !do_delta {
		if new_set.(*schema.Set).Len() < 1 {
			return nil
		}

		for _, disk := range new_set.(*schema.Set).List() {
			computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
			req := compute.DiskAttachRequest{
				ComputeID: computeId,
				DiskID:    uint64(disk.(int)),
			}

			_, err := c.CloudAPI().Compute().DiskAttach(ctx, req)
			if err != nil {
				apiErrCount++
				lastSavedError = err
			}
		}

		if apiErrCount > 0 {
			log.Errorf("utilityComputeExtraDisksConfigure: there were %d error(s) when attaching disks to Compute ID %s. Last error was: %s",
				apiErrCount, d.Id(), lastSavedError)
			return lastSavedError
		}

		return nil
	}

	detach_set := old_set.(*schema.Set).Difference(new_set.(*schema.Set))
	log.Debugf("utilityComputeExtraDisksConfigure: detach set has %d items for Compute ID %s", detach_set.Len(), d.Id())

	if detach_set.Len() > 0 {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

		for _, diskId := range detach_set.List() {
			req := compute.DiskDetachRequest{
				ComputeID: computeId,
				DiskID:    uint64(diskId.(int)),
			}
			_, err := c.CloudAPI().Compute().DiskDetach(ctx, req)
			if err != nil {
				log.Errorf("utilityComputeExtraDisksConfigure: failed to detach disk ID %d from Compute ID %s: %s", diskId.(int), d.Id(), err)
				apiErrCount++
				lastSavedError = err
			}
		}

	}

	attach_set := new_set.(*schema.Set).Difference(old_set.(*schema.Set))
	log.Debugf("utilityComputeExtraDisksConfigure: attach set has %d items for Compute ID %s", attach_set.Len(), d.Id())
	for _, diskId := range attach_set.List() {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req := compute.DiskAttachRequest{
			ComputeID: computeId,
			DiskID:    uint64(diskId.(int)),
		}

		log.Debug("before attach HERE0")

		flag := false

		// check if there is an extra disk in the disks from the platform, so as not to attach it if it is already attached
		for _, elem := range disks {
			if elem.ID == uint64(diskId.(int)) {
				flag = true
				break
			}
		}

		if flag {
			continue
		}

		_, err := c.CloudAPI().Compute().DiskAttach(ctx, req)
		if err != nil {
			log.Errorf("utilityComputeExtraDisksConfigure: failed to attach disk ID %d to Compute ID %s: %s", diskId.(int), d.Id(), err)
			apiErrCount++
			lastSavedError = err
		}
	}

	if apiErrCount > 0 {
		log.Errorf("utilityComputeExtraDisksConfigure: there were %d error(s) when managing disks of Compute ID %s. Last error was: %s",
			apiErrCount, d.Id(), lastSavedError)
		return lastSavedError
	}

	return nil
}

func utilityComputeNetworksConfigure(ctx context.Context, d *schema.ResourceData, m interface{}) error {
	c := m.(*controller.ControllerCfg)

	oldSet, newSet := d.GetChange("network")

	oldList := oldSet.(*schema.Set).List()
	newList := newSet.(*schema.Set).List()

	detachMap, changeIpMap, attachMap := differenceNetwork(oldList, newList)

	apiErrCount := 0
	var lastSavedError error

	log.Debugf("utilityComputeNetworksConfigure: detach set has %d items for Compute ID %s", len(detachMap), d.Id())
	for _, netData := range detachMap {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req := compute.NetDetachRequest{
			ComputeID: computeId,
			IPAddr:    netData["ip_address"].(string),
			MAC:       netData["mac"].(string),
		}

		_, err := c.CloudAPI().Compute().NetDetach(ctx, req)
		if err != nil {
			log.Errorf("utilityComputeNetworksConfigure: failed to detach net ID %d of type %s from Compute ID %s: %s",
				netData["net_id"].(int), netData["net_type"].(string), d.Id(), err)
			apiErrCount++
			lastSavedError = err
		}
	}

	log.Debugf("utilityComputeNetworksConfigure: changeIp set has %d items for Compute ID %s", len(changeIpMap), d.Id())
	for _, netData := range changeIpMap {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req := compute.ChangeIPRequest{
			ComputeID: computeId,
			NetType:   netData["net_type"].(string),
			NetID:     uint64(netData["net_id"].(int)),
			IPAddr:    netData["ip_address"].(string),
		}

		_, err := c.CloudAPI().Compute().ChangeIP(ctx, req)
		if err != nil {
			log.Errorf("utilityComputeNetworksConfigure: failed to change net ID %d of type %s from Compute ID %s: %s",
				netData["net_id"].(int), netData["net_type"].(string), d.Id(), err)
			apiErrCount++
			lastSavedError = err
		}
	}

	needStart := false

	if oldSet.(*schema.Set).Len() == len(detachMap) || oldSet.(*schema.Set).Len() == 0 || hasDPDKnetwork(attachMap) {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		if err := utilityComputeStop(ctx, computeId, m); err != nil {
			apiErrCount++
			lastSavedError = err
		}
		if start := d.Get("started"); start.(bool) {
			needStart = true
		}
	}

	sort.Slice(attachMap, func(i, j int) bool {
		weightI := attachMap[i]["weight"].(int)
		weightJ := attachMap[j]["weight"].(int)
		if weightI == 0 {
			return false
		}
		if weightJ == 0 {
			return true
		}
		return weightI < weightJ
	})
	log.Debugf("utilityComputeNetworksConfigure: attach set has %d items for Compute ID %s", len(attachMap), d.Id())
	for _, netData := range attachMap {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req := compute.NetAttachRequest{
			ComputeID: computeId,
			NetType:   netData["net_type"].(string),
			NetID:     uint64(netData["net_id"].(int)),
		}

		if netData["ip_address"].(string) != "" {
			req.IPAddr = netData["ip_address"].(string)
		}

		if req.NetType == "DPDK" {
			req.MTU = uint64(netData["mtu"].(int))
		}

		_, err := c.CloudAPI().Compute().NetAttach(ctx, req)
		if err != nil {
			log.Errorf("utilityComputeNetworksConfigure: failed to attach net ID %d of type %s to Compute ID %s: %s",
				netData["net_id"].(int), netData["net_type"].(string), d.Id(), err)
			apiErrCount++
			lastSavedError = err
		}
	}

	if needStart {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		if numErr, err := utilityComputeStart(ctx, computeId, m); err != nil {
			apiErrCount += numErr
			lastSavedError = err
		}
	}

	if apiErrCount > 0 {
		log.Errorf("utilityComputeNetworksConfigure: there were %d error(s) when managing networks of Compute ID %s. Last error was: %s",
			apiErrCount, d.Id(), lastSavedError)
		return lastSavedError
	}

	return nil
}

func hasDPDKnetwork(networkAttachMap []map[string]interface{}) bool {
	for _, elem := range networkAttachMap {
		if elem["net_type"].(string) == "DPDK" {
			return true
		}
	}
	return false
}

func utilityComputeCheckPresence(ctx context.Context, d *schema.ResourceData, m interface{}) (compute.RecordCompute, error) {
	c := m.(*controller.ControllerCfg)
	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
	req := compute.GetRequest{
		ComputeID: computeId,
	}

	computeRecord, err := c.CloudAPI().Compute().Get(ctx, req)
	if err != nil {
		return compute.RecordCompute{}, err
	}

	return *computeRecord, nil
}

func utilityComputeStop(ctx context.Context, computeID uint64, m interface{}) error {
	c := m.(*controller.ControllerCfg)
	req := compute.StopRequest{
		ComputeID: computeID,
		Force:     true,
	}

	log.Debugf("utilityComputeNetworksConfigure: stopping compute %d", computeID)
	_, err := c.CloudAPI().Compute().Stop(ctx, req)
	if err != nil {
		return err
	}
	return nil
}

func utilityComputeStart(ctx context.Context, computeID uint64, m interface{}) (int, error) {
	c := m.(*controller.ControllerCfg)
	startReq := compute.StartRequest{ComputeID: computeID}

	log.Debugf("utilityComputeNetworksConfigure: starting compute %d", computeID)
	_, err := c.CloudAPI().Compute().Start(ctx, startReq)
	if err != nil {
		return 1, err
	}
	return 0, nil
}

func utilityComputeUpdatePciDevices(ctx context.Context, d *schema.ResourceData, m interface{}) error {
	c := m.(*controller.ControllerCfg)
	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
	oldSet, newSet := d.GetChange("pci_devices")
	deletedDevices := (oldSet.(*schema.Set).Difference(newSet.(*schema.Set))).List()
	if len(deletedDevices) > 0 {
		for _, ar := range deletedDevices {
			arConv := ar.(int)
			req := compute.DetachPCIDeviceRequest{
				ComputeID: computeId,
				DeviceID:  uint64(arConv),
			}
			_, err := c.CloudAPI().Compute().DetachPCIDevice(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	added := (newSet.(*schema.Set).Difference(oldSet.(*schema.Set))).List()
	if len(added) > 0 {
		for _, ar := range added {
			arConv := ar.(int)
			req := compute.AttachPCIDeviceRequest{
				ComputeID: computeId,
				DeviceID:  uint64(arConv),
			}

			_, err := c.CloudAPI().Compute().AttachPCIDevice(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func differenceNetwork(oldList, newList []interface{}) (detachMap, changeIpMap, attachMap []map[string]interface{}) {
	attachMap = make([]map[string]interface{}, 0)
	changeIpMap = make([]map[string]interface{}, 0)
	detachMap = make([]map[string]interface{}, 0)
	for _, oldNetwork := range oldList {
		oldMap := oldNetwork.(map[string]interface{})
		found := false
		for _, newNetwork := range newList {
			newMap := newNetwork.(map[string]interface{})
			if newMap["net_type"] == oldMap["net_type"] && newMap["net_id"] == oldMap["net_id"] && newMap["weight"] == oldMap["weight"] && (newMap["mtu"] == oldMap["mtu"] || newMap["mtu"].(int) == 0) {
				if (newMap["net_type"].(string) == "EXTNET" || newMap["net_type"].(string) == "VINS") && (newMap["ip_address"] != oldMap["ip_address"] && newMap["ip_address"].(string) != "") {
					changeIpMap = append(changeIpMap, newMap)
					found = true
					break
				} else if newMap["ip_address"] == oldMap["ip_address"] || newMap["ip_address"].(string) == "" {
					found = true
					break
				}
			}
		}
		if found {
			continue
		}
		detachMap = append(detachMap, oldMap)
	}

	for _, newNetwork := range newList {
		newMap := newNetwork.(map[string]interface{})
		found := false
		for _, oldNetwork := range oldList {
			oldMap := oldNetwork.(map[string]interface{})
			if newMap["net_type"] == oldMap["net_type"] && newMap["net_id"] == oldMap["net_id"] && newMap["weight"] == oldMap["weight"] && (newMap["mtu"] == oldMap["mtu"] || newMap["mtu"].(int) == 0) {
				if newMap["ip_address"] == oldMap["ip_address"] || newMap["ip_address"].(string) == "" ||
					((newMap["net_type"].(string) == "EXTNET" || newMap["net_type"].(string) == "VINS") &&
						newMap["ip_address"] != oldMap["ip_address"] && newMap["ip_address"].(string) != "") {
					found = true
					break
				}
			}
		}
		if found {
			continue
		}
		attachMap = append(attachMap, newMap)
	}

	return
}
