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

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"
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/go-cty/cty"
	log "github.com/sirupsen/logrus"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker/compute"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker/disks"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker/tasks"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/controller"

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

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
	enabled := d.Get("enabled").(bool)

	if enabled {
		req := compute.EnableRequest{
			ComputeID: computeId,
		}

		if _, err := c.CloudBroker().Compute().Enable(ctx, req); err != nil {
			return err
		}
	} else {
		req := compute.DisableRequest{
			ComputeID: computeId,
		}

		if _, err := c.CloudBroker().Compute().Disable(ctx, req); err != nil {
			return err
		}
	}
	log.Debugf("resourceComputeUpdate: enable=%v Compute ID %s after completing its resource configuration", enabled, d.Id())

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	if d.Get("started").(bool) {
		req := compute.StartRequest{
			ComputeID: computeId,
		}
		if altBootId, ok := d.Get("alt_boot_id").(int); ok {
			req.AltBootID = uint64(altBootId)
		}
		if nodeId, ok := d.Get("node_id").(int); ok {
			req.NodeID = uint64(nodeId)
		}
		if _, err := c.CloudBroker().Compute().Start(ctx, req); err != nil {
			return err
		}
	} else {
		req := compute.StopRequest{
			ComputeID: computeId,
		}
		if force, ok := d.Get("force_stop").(bool); ok {
			req.Force = force
		}
		if _, err := c.CloudBroker().Compute().Stop(ctx, req); err != nil {
			return err
		}
	}
	return nil
}

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

	computeId, err := strconv.ParseUint(d.Id(), 10, 64)
	if err != nil {
		return err
	}

	req := compute.ChangeReadOnlyRequest{
		ComputeID: computeId,
		ReadOnly:  d.Get("read_only").(bool),
	}

	if _, err := c.CloudBroker().Compute().ChangeReadOnly(ctx, req); err != nil {
		return err
	}

	log.Debugf("resourceCompute: read_only=%t for Compute ID %d", req.ReadOnly, computeId)

	return nil
}

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

	var isStopRequired bool

	old, new := d.GetChange("cpu")
	if d.Get("started").(bool) && (old.(int) > new.(int)) && d.Get("force_resize").(bool) {
		isStopRequired = true
	}
	if isStopRequired {
		stopReq := compute.StopRequest{
			ComputeID: computeId,
			Force:     false,
		}
		if _, err := c.CloudBroker().Compute().Stop(ctx, stopReq); err != nil {
			return err
		}
	}

	resizeReq := compute.ResizeRequest{
		ComputeID: computeId,
	}
	forceResize, ok := d.GetOk("force_resize")
	if ok {
		resizeReq.Force = forceResize.(bool)
	}

	doUpdate := false

	oldCpu, newCpu := d.GetChange("cpu")
	if oldCpu.(int) > newCpu.(int) && !forceResize.(bool) {
		return fmt.Errorf("сannot resize compute ID %d: enable 'force_resize' to reduce compute vCPUs", computeId)
	}
	if oldCpu.(int) != newCpu.(int) {
		resizeReq.CPU = uint64(newCpu.(int))
		doUpdate = true
	} else {
		resizeReq.CPU = 0
	}

	if resizeReq.CPU != 0 {
		if preferredCPU, ok := d.GetOk("preferred_cpu"); ok {
			preferredList := preferredCPU.([]interface{})
			if len(preferredList) > 0 {
				for _, v := range preferredList {
					cpuNum := v.(int)
					resizeReq.PreferredCPU = append(resizeReq.PreferredCPU, int64(cpuNum))
				}
			}
		}
		oldPCPU, newPCPU := d.GetChange("preferred_cpu")
		if len(oldPCPU.([]interface{})) != 0 && len(newPCPU.([]interface{})) == 0 {
			resizeReq.PreferredCPU = []int64{-1}
		}
	}

	oldRam, newRam := d.GetChange("ram")
	if oldRam.(int) != newRam.(int) {
		resizeReq.RAM = uint64(newRam.(int))
		doUpdate = true
	} else {
		resizeReq.RAM = 0
	}

	if doUpdate {
		log.Debugf("resourceComputeUpdate: changing CPU %d -> %d and/or RAM %d -> %d",
			oldCpu.(int), newCpu.(int),
			oldRam.(int), newRam.(int))
		_, err := c.CloudBroker().Compute().Resize(ctx, resizeReq)
		if err != nil {
			return err
		}
	}

	if isStopRequired {
		req := compute.StartRequest{ComputeID: computeId}
		if altBootID, ok := d.Get("alt_boot_id").(int); ok {
			req.AltBootID = uint64(altBootID)
		}
		if _, err := c.CloudBroker().Compute().Start(ctx, req); err != nil {
			return err
		}
	}

	return nil
}

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

	oldSize, newSize := d.GetChange("boot_disk_size")
	if oldSize.(int) < newSize.(int) {
		req := compute.DiskResizeRequest{ComputeID: computeId, Size: uint64(newSize.(int))}
		if diskId, ok := d.GetOk("boot_disk_id"); ok {
			req.DiskID = uint64(diskId.(int))

		} else {
			bootDisk, err := utilityComputeBootDiskCheckPresence(ctx, d, m)
			if err != nil {
				return err
			}

			req.DiskID = bootDisk.ID
		}

		log.Debugf("resourceComputeUpdate: compute ID %s, boot disk ID %d resize %d -> %d",
			d.Id(), d.Get("boot_disk_id").(int), oldSize.(int), newSize.(int))

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

	} else if oldSize.(int) > newSize.(int) {
		log.Warnf("resourceComputeUpdate: compute ID %s - shrinking boot disk is not allowed", d.Id())
	}

	return nil
}

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

	deletedDisks := make([]interface{}, 0)
	addedDisks := make([]interface{}, 0)
	resizedDisks := make([]interface{}, 0)
	renamedDisks := make([]interface{}, 0)
	changeStoragePolicyDisks := make([]interface{}, 0)
	cacheUpdatedDisks := make([]interface{}, 0)
	blkDiscardUpdatedDisks := make([]interface{}, 0)
	blockSizeUpdatedDisks := make([]interface{}, 0)
	iotuneUpdatedDisks := make([]interface{}, 0)
	migratedDisks := make([]interface{}, 0)
	presentNewDisks := make([]interface{}, 0)
	presentOldDisks := make([]interface{}, 0)

	chipset := d.Get("chipset").(string)

	oldDisks, newDisks := d.GetChange("disks")
	oldConv := oldDisks.([]interface{})
	newConv := newDisks.([]interface{})

	for _, el := range oldConv {
		changeNodes, newEl := isChangeNodesDisk(newConv, el)
		if changeNodes {
			presentNewDisks = append(presentNewDisks, newEl)
			presentOldDisks = append(presentOldDisks, el)
		}
		// !isRenameDisk(newConv, el) && !isResizeDisk(newConv, el) are required in case two or more disks are being created and their disk_id is the same (=0)
		if !isContainsDisk(newConv, el) && !isRenameDisk(newConv, el) && !isResizeDisk(newConv, el) && !isChangeStoragePolicy(newConv, el) {
			flag := false
			extraDisks := d.Get("extra_disks").(*schema.Set).List()
			delDisk := el.(map[string]interface{})
			delDiskId := delDisk["disk_id"].(int)

			for _, extraDiskId := range extraDisks {
				if extraDiskId.(int) == delDiskId {
					flag = true
					break
				}
			}

			if !flag {
				deletedDisks = append(deletedDisks, el)
			} else {
				log.Debugf("disk %d will not be deleted because it is present in the extra_disks block", delDiskId)
			}
		}
	}

	for _, el := range newConv {
		if !isContainsDisk(oldConv, el) {
			addedDisks = append(addedDisks, el)
		}
		if isResizeDisk(oldConv, el) {
			resizedDisks = append(resizedDisks, el)
		}
		if isRenameDisk(oldConv, el) {
			renamedDisks = append(renamedDisks, el)
		}
		if isMigrateDisk(oldConv, el) {
			migratedDisks = append(migratedDisks, el)
		} else if isChangeStoragePolicy(oldConv, el) {
			changeStoragePolicyDisks = append(changeStoragePolicyDisks, el)
		}
		if isChangeCacheDisk(oldConv, el) {
			cacheUpdatedDisks = append(cacheUpdatedDisks, el)
		}

		if isChangeBLKDiscardDisk(oldConv, el) {
			blkDiscardUpdatedDisks = append(blkDiscardUpdatedDisks, el)
		}

		if isChangeBlockSizeDisk(oldConv, el) {
			blockSizeUpdatedDisks = append(blockSizeUpdatedDisks, el)
		}

		if isChangeIOTuneDisk(oldConv, el) {
			iotuneUpdatedDisks = append(iotuneUpdatedDisks, el)
		}
	}

	if len(deletedDisks) > 0 {
		for _, disk := range deletedDisks {
			diskConv := disk.(map[string]interface{})
			if isBootDisk(diskConv, chipset) {
				continue
			}

			req := compute.DiskDelRequest{
				ComputeID:   computeId,
				DiskID:      uint64(diskConv["disk_id"].(int)),
				Permanently: diskConv["permanently"].(bool),
			}

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

	if len(addedDisks) > 0 {
		for _, disk := range addedDisks {
			diskConv := disk.(map[string]interface{})
			req := compute.DiskAddRequest{
				ComputeID:       computeId,
				DiskName:        diskConv["disk_name"].(string),
				Size:            uint64(diskConv["size"].(int)),
				StoragePolicyID: uint64(diskConv["storage_policy_id"].(int)),
			}
			if diskConv["sep_id"].(int) != 0 {
				req.SepID = uint64(diskConv["sep_id"].(int))
			}
			if diskConv["pool"].(string) != "" {
				req.Pool = diskConv["pool"].(string)
			}
			if diskConv["desc"].(string) != "" {
				req.Description = diskConv["desc"].(string)
			}
			if diskConv["image_id"].(int) != 0 {
				req.ImageID = uint64(diskConv["image_id"].(int))
			}
			if cacheVal, ok := diskConv["cache"].(string); ok {
				req.Cache = cacheVal
			}
			diskID, err := c.CloudBroker().Compute().DiskAdd(ctx, req)
			if err != nil {
				return err
			}
			if blockSize, ok := diskConv["block_size"].(string); ok && blockSize != "" {
				updateReq := disks.UpdateRequest{
					DiskID:    diskID,
					BlockSize: blockSize,
				}
				_, err := c.CloudBroker().Disks().Update(ctx, updateReq)
				if err != nil {
					return err
				}
			}
			if nodeIDs, ok := diskConv["node_ids"]; ok {
				presentIDs := nodeIDs.(*schema.Set).List()
				if len(presentIDs) > 0 {
					log.Debugf("resourceComputeUpdate: start presents new disk ID:%d to nodes", diskID)
				}
				for _, presentID := range presentIDs {
					nodeID := uint64(presentID.(int))
					req := disks.PresentRequest{
						DiskID: diskID,
						NodeID: nodeID,
					}
					_, err := c.CloudBroker().Disks().Present(ctx, req)
					if err != nil {
						return err
					}
				}
			}
			if iotuneRaw, ok := diskConv["iotune"].([]interface{}); ok && len(iotuneRaw) > 0 {
				iotuneMap := iotuneRaw[0].(map[string]interface{})
				limitReq := disks.LimitIORequest{
					DiskID:           diskID,
					ReadBytesSec:     uint64(iotuneMap["read_bytes_sec"].(int)),
					ReadBytesSecMax:  uint64(iotuneMap["read_bytes_sec_max"].(int)),
					ReadIOPSSec:      uint64(iotuneMap["read_iops_sec"].(int)),
					ReadIOPSSecMax:   uint64(iotuneMap["read_iops_sec_max"].(int)),
					SizeIOPSSec:      uint64(iotuneMap["size_iops_sec"].(int)),
					TotalBytesSec:    uint64(iotuneMap["total_bytes_sec"].(int)),
					TotalBytesSecMax: uint64(iotuneMap["total_bytes_sec_max"].(int)),
					TotalIOPSSec:     uint64(iotuneMap["total_iops_sec"].(int)),
					TotalIOPSSecMax:  uint64(iotuneMap["total_iops_sec_max"].(int)),
					WriteBytesSec:    uint64(iotuneMap["write_bytes_sec"].(int)),
					WriteBytesSecMax: uint64(iotuneMap["write_bytes_sec_max"].(int)),
					WriteIOPSSec:     uint64(iotuneMap["write_iops_sec"].(int)),
					WriteIOPSSecMax:  uint64(iotuneMap["write_iops_sec_max"].(int)),
				}
				_, err := c.CloudBroker().Disks().LimitIO(ctx, limitReq)
				if err != nil {
					return err
				}
			}
			if err != nil {
				return err
			}
		}
	}

	if len(resizedDisks) > 0 {
		for _, disk := range resizedDisks {
			diskConv := disk.(map[string]interface{})
			if isBootDisk(diskConv, chipset) {
				continue
			}
			req := compute.DiskResizeRequest{
				ComputeID: computeId,
				DiskID:    uint64(diskConv["disk_id"].(int)),
				Size:      uint64(diskConv["size"].(int)),
			}

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

	if len(renamedDisks) > 0 {
		for _, disk := range renamedDisks {
			diskConv := disk.(map[string]interface{})

			req := disks.RenameRequest{
				DiskID: uint64(diskConv["disk_id"].(int)),
				Name:   diskConv["disk_name"].(string),
			}

			_, err := c.CloudBroker().Disks().Rename(ctx, req)
			if err != nil {
				return err
			}
		}
	}
	if len(changeStoragePolicyDisks) > 0 {
		for _, disk := range changeStoragePolicyDisks {
			diskConv := disk.(map[string]interface{})

			req := disks.ChangeDiskStoragePolicyRequest{
				DiskID:          uint64(diskConv["disk_id"].(int)),
				StoragePolicyID: uint64(diskConv["storage_policy_id"].(int)),
			}

			_, err := c.CloudBroker().Disks().ChangeDiskStoragePolicy(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	if len(cacheUpdatedDisks) > 0 {
		for _, disk := range cacheUpdatedDisks {
			diskConv := disk.(map[string]interface{})
			if isBootDisk(diskConv, chipset) {
				continue
			}

			diskID := uint64(diskConv["disk_id"].(int))
			if diskID == 0 {
				continue
			}

			req := disks.UpdateRequest{
				DiskID: diskID,
				Cache:  diskConv["cache"].(string),
			}
			_, err := c.CloudBroker().Disks().Update(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	if len(blkDiscardUpdatedDisks) > 0 {
		for _, disk := range blkDiscardUpdatedDisks {
			diskConv := disk.(map[string]interface{})
			if isBootDisk(diskConv, chipset) {
				continue
			}

			diskID := uint64(diskConv["disk_id"].(int))
			if diskID == 0 {
				continue
			}

			req := disks.UpdateRequest{
				DiskID:     diskID,
				BLKDiscard: diskConv["blk_discard"].(bool),
			}
			_, err := c.CloudBroker().Disks().Update(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	if len(blockSizeUpdatedDisks) > 0 {
		for _, disk := range blockSizeUpdatedDisks {
			diskConv := disk.(map[string]interface{})
			if isBootDisk(diskConv, chipset) {
				continue
			}

			diskID := uint64(diskConv["disk_id"].(int))
			if diskID == 0 {
				continue
			}

			req := disks.UpdateRequest{
				DiskID:    diskID,
				BlockSize: diskConv["block_size"].(string),
			}
			_, err := c.CloudBroker().Disks().Update(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	if len(iotuneUpdatedDisks) > 0 {
		for _, disk := range iotuneUpdatedDisks {
			diskConv := disk.(map[string]interface{})
			if isBootDisk(diskConv, chipset) {
				continue
			}

			diskID := uint64(diskConv["disk_id"].(int))
			if diskID == 0 {
				continue
			}

			iotuneRaw := diskConv["iotune"].([]interface{})
			if len(iotuneRaw) == 0 {
				continue
			}
			iotuneMap := iotuneRaw[0].(map[string]interface{})
			req := disks.LimitIORequest{
				DiskID:           diskID,
				ReadBytesSec:     uint64(iotuneMap["read_bytes_sec"].(int)),
				ReadBytesSecMax:  uint64(iotuneMap["read_bytes_sec_max"].(int)),
				ReadIOPSSec:      uint64(iotuneMap["read_iops_sec"].(int)),
				ReadIOPSSecMax:   uint64(iotuneMap["read_iops_sec_max"].(int)),
				SizeIOPSSec:      uint64(iotuneMap["size_iops_sec"].(int)),
				TotalBytesSec:    uint64(iotuneMap["total_bytes_sec"].(int)),
				TotalBytesSecMax: uint64(iotuneMap["total_bytes_sec_max"].(int)),
				TotalIOPSSec:     uint64(iotuneMap["total_iops_sec"].(int)),
				TotalIOPSSecMax:  uint64(iotuneMap["total_iops_sec_max"].(int)),
				WriteBytesSec:    uint64(iotuneMap["write_bytes_sec"].(int)),
				WriteBytesSecMax: uint64(iotuneMap["write_bytes_sec_max"].(int)),
				WriteIOPSSec:     uint64(iotuneMap["write_iops_sec"].(int)),
				WriteIOPSSecMax:  uint64(iotuneMap["write_iops_sec_max"].(int)),
			}
			_, err := c.CloudBroker().Disks().LimitIO(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	if len(migratedDisks) > 0 {
		if err := utilityComputeMigrateDisks(ctx, d, m, migratedDisks, oldConv); err != nil {
			return err
		}
	}

	for i := range presentNewDisks {
		newDisk := presentNewDisks[i].(map[string]interface{})
		oldDisk := presentOldDisks[i].(map[string]interface{})
		newArr := newDisk["node_ids"]
		oldArr := oldDisk["node_ids"]
		diskID := uint64(newDisk["disk_id"].(int))
		presentIDs := (newArr.(*schema.Set).Difference(oldArr.(*schema.Set))).List()
		depresentIDs := (oldArr.(*schema.Set).Difference(newArr.(*schema.Set))).List()
		for _, presentID := range presentIDs {
			nodeID := uint64(presentID.(int))

			req := disks.PresentRequest{
				DiskID: diskID,
				NodeID: nodeID,
			}
			log.Debugf("resourceComputeUpdate: start presents disk ID:%d from nodes %d", req.DiskID, req.NodeID)
			_, err := c.CloudBroker().Disks().Present(ctx, req)
			if err != nil {
				return err
			}
		}

		for _, depresentID := range depresentIDs {
			nodeID := uint64(depresentID.(int))

			req := disks.DepresentRequest{
				DiskID: diskID,
				NodeID: nodeID,
			}
			log.Debugf("resourceComputeUpdate: start depresents disk ID:%d from nodes %d", req.DiskID, req.NodeID)
			_, err := c.CloudBroker().Disks().Depresent(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

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

	newCache := d.Get("boot_disk_cache").(string)

	var bootDiskID uint64
	if v, ok := d.GetOk("boot_disk_id"); ok {
		if id, ok := v.(int); ok {
			bootDiskID = uint64(id)
		}
	}

	if bootDiskID == 0 {
		return fmt.Errorf("cannot update boot_disk_cache: boot disk ID is unknown for compute %s", d.Id())
	}

	req := disks.UpdateRequest{
		DiskID: bootDiskID,
		Cache:  newCache,
	}

	_, err := c.CloudBroker().Disks().Update(ctx, req)
	return err
}

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

	newBLKDiscard := d.Get("boot_disk_blk_discard").(bool)

	var bootDiskID uint64
	if v, ok := d.GetOk("boot_disk_id"); ok {
		if id, ok := v.(int); ok {
			bootDiskID = uint64(id)
		}
	}

	if bootDiskID == 0 {
		return fmt.Errorf("cannot update boot_disk_cache: boot disk ID is unknown for compute %s", d.Id())
	}

	req := disks.UpdateRequest{
		DiskID:     bootDiskID,
		BLKDiscard: newBLKDiscard,
	}

	_, err := c.CloudBroker().Disks().Update(ctx, req)
	return err
}

func utilityComputeExtraDisksConfigure(ctx context.Context, d *schema.ResourceData, m interface{}, do_delta bool) 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.CloudBroker().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.CloudBroker().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)),
		}

		_, err := c.CloudBroker().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 utilityComputeCheckPresence(ctx context.Context, d *schema.ResourceData, m interface{}) (*compute.RecordCompute, error) {
	c := m.(*controller.ControllerCfg)
	req := compute.GetRequest{}

	if d.Id() != "" {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req.ComputeID = computeId
	} else {
		req.ComputeID = uint64(d.Get("compute_id").(int))
	}

	res, err := c.CloudBroker().Compute().Get(ctx, req)
	if err != nil {
		return nil, err
	}

	return res, nil
}

func utilityComputePCIDevicesList(ctx context.Context, d *schema.ResourceData, m interface{}) (*compute.ListPCIDevices, error) {
	c := m.(*controller.ControllerCfg)
	req := compute.ListPCIDeviceRequest{}

	if d.Id() != "" {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req.ComputeID = computeId
	} else {
		req.ComputeID = uint64(d.Get("compute_id").(int))
	}

	res, err := c.CloudBroker().Compute().ListPCIDevice(ctx, req)
	if err != nil {
		return nil, err
	}

	return res, nil
}

func isBootDisk(diskConv map[string]interface{}, chipset string) bool {
	if chipset == "i440fx" {
		return diskConv["pci_slot"].(int) == 6
	}
	return diskConv["bus_number"].(int) == 6
}

func findBootDisk(disks compute.ListDisks, chipset string) *compute.ItemDisk {
	for _, disk := range disks {
		if chipset == "i440fx" {
			if disk.PCISlot == 6 {
				return &disk
			}
		} else {
			if disk.BusNumber == 6 {
				return &disk
			}
		}
	}
	return &compute.ItemDisk{}
}

func networkSubresIPAddreDiffSupperss(key, oldVal, newVal string, d *schema.ResourceData) bool {
	if newVal != "" && newVal != oldVal {
		log.Debugf("networkSubresIPAddreDiffSupperss: key=%s, oldVal=%q, newVal=%q -> suppress=FALSE", key, oldVal, newVal)
		return false
	}
	log.Debugf("networkSubresIPAddreDiffSupperss: key=%s, oldVal=%q, newVal=%q -> suppress=TRUE", key, oldVal, newVal)
	return true // suppress difference
}

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()

	updateNetwork := differenceNetwork(oldList, newList)
	apiErrCount := 0
	var lastSavedError error

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

		_, err := c.CloudBroker().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(updateNetwork.ChangeIPMap), d.Id())
	for _, netData := range updateNetwork.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.CloudBroker().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

	oldLibvirtSet, newLibvirtSet := d.GetChange("libvirt_settings")
	addedLibvirtSettings := (newLibvirtSet.(*schema.Set).Difference(oldLibvirtSet.(*schema.Set))).List()
	libvirtSettingsMap := addAttachedNetwork(addedLibvirtSettings, newLibvirtSet.(*schema.Set).List(), updateNetwork.AttachMap)

	if oldSet.(*schema.Set).Len() == len(updateNetwork.DetachMap) || oldSet.(*schema.Set).Len() == 0 || len(libvirtSettingsMap) > 0 || hasDPDKnetwork(updateNetwork.AttachMap) || len(updateNetwork.ChangeMacMap) > 0 {
		if err := utilityComputeStop(ctx, d, m); err != nil {
			apiErrCount++
			lastSavedError = err
		}
		if start := d.Get("started"); start.(bool) {
			needStart = true
		}
	}

	log.Debugf("utilityComputeNetworksConfigure: changeMac set has %d items for Compute ID %s", len(updateNetwork.ChangeMacMap), d.Id())
	for _, netData := range updateNetwork.ChangeMacMap {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req := compute.ChangeMACRequest{
			ComputeID:  computeId,
			NewMAC:     netData["mac"].(string),
			СurrentMAC: netData["old_mac"].(string),
		}

		_, err := c.CloudBroker().Compute().ChangeMAC(ctx, req)
		if err != nil {
			log.Errorf("utilityComputeNetworksConfigure: failed to change mac %s to  %s from Compute ID %s: %s",
				req.СurrentMAC, req.NewMAC, d.Id(), err)
			apiErrCount++
			lastSavedError = err
		}
	}

	sort.Slice(updateNetwork.AttachMap, func(i, j int) bool {
		weightI := updateNetwork.AttachMap[i]["weight"].(int)
		weightJ := updateNetwork.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(updateNetwork.AttachMap), d.Id())
	for _, netData := range updateNetwork.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 req.NetType == "DPDK" {
			req.MTU = uint64(netData["mtu"].(int))
		}

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

		if netData["mac"].(string) != "" {
			req.MACAddr = netData["mac"].(string)
		}

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

		if req.NetType == "DPDK" || req.NetType == "VFNIC" {
			if netMask, ok := netData["net_mask"].(int); ok && netMask > 0 {
				req.NetMask = uint64(netMask)
			}
		}

		if netData["sdn_interface_id"].(string) != "" {
			req.SDNInterfaceID = netData["sdn_interface_id"].(string)
		}

		_, err := c.CloudBroker().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 len(libvirtSettingsMap) > 0 {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		computeRec, err := utilityComputeCheckPresence(ctx, d, m)
		if err != nil {
			log.Errorf("utilityComputeNetworksConfigure: failed to read information about compute with ID %s: %s",
				d.Id(), err)
			apiErrCount++
			lastSavedError = err
		}

		if computeRec != nil {
			log.Debugf("utilityComputeNetworksConfigure: libvirt virtio set has %d items for Compute ID %s", len(libvirtSettingsMap), d.Id())
			for _, libvirtSetting := range libvirtSettingsMap {
				netType := libvirtSetting["net_type"].(string)
				netId := uint64(libvirtSetting["net_id"].(int))
				var mac string
				for _, iface := range computeRec.Interfaces {
					if iface.NetID == netId && iface.NetType == netType {
						mac = iface.MAC
						break
					}
				}
				log.Debugf("utilityComputeNetworksConfigure: Configure libvirt virtio interface parameters on Network with type %s and id %d", netType, netId)
				req := compute.SetNetConfigRequest{
					ComputeID:   computeId,
					MAC:         mac,
					TXMode:      libvirtSetting["txmode"].(string),
					IOEventFD:   libvirtSetting["ioeventfd"].(string),
					EventIDx:    libvirtSetting["event_idx"].(string),
					Queues:      uint64(libvirtSetting["queues"].(int)),
					RXQueueSize: uint64(libvirtSetting["rx_queue_size"].(int)),
					TXQueueSize: uint64(libvirtSetting["tx_queue_size"].(int)),
				}

				_, err := c.CloudBroker().Compute().SetNetConfig(ctx, req)
				if err != nil {
					log.Errorf("utilityComputeNetworksConfigure: failed to set net config to net ID %d of type %s to Compute ID %s: %s",
						netId, netType, d.Id(), err)
					apiErrCount++
					lastSavedError = err
				}
			}
		}
	}

	if needStart {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		var altBootID uint64
		if altBootIDRaw, ok := d.Get("alt_boot_id").(int); ok {
			altBootID = uint64(altBootIDRaw)
		} else {
			altBootID = 0
		}
		if numErr, err := utilityComputeStart(ctx, computeId, altBootID, m); err != nil {
			apiErrCount += numErr
			lastSavedError = err
		}
	}

	log.Debugf("utilityComputeNetworksConfigure: changeMTU set has %d items for Compute ID %s", len(updateNetwork.ChangeMTUMap), d.Id())
	for _, netData := range updateNetwork.ChangeMTUMap {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req := compute.ChangeMTURequest{
			ComputeID: computeId,
			Interface: netData["mac"].(string),
			MTU:       uint64(netData["mtu"].(int)),
		}

		_, err := c.CloudBroker().Compute().ChangeMTU(ctx, req)
		if err != nil {
			log.Errorf("utilityComputeNetworksConfigure: failed to change MTU 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: enableMap set has %d items for Compute ID %s", len(updateNetwork.EnableMap), d.Id())
	for _, netData := range updateNetwork.EnableMap {
		computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
		req := compute.ChangeLinkStateRequest{
			ComputeID: computeId,
			Interface: netData["mac"].(string),
			State:     "off",
		}

		if netData["enabled"].(bool) {
			req.State = "on"
		}

		_, err := c.CloudBroker().Compute().ChangeLinkState(ctx, req)
		if err != nil {
			log.Errorf("utilityComputeNetworksConfigure: failed to change link state network ID %d of type %s from Compute ID %s: %s",
				netData["net_id"].(int), netData["net_type"].(string), d.Id(), err)
			apiErrCount++
			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 differenceNetwork(oldList, newList []interface{}) *updatedNetwork {
	attachMap := make([]map[string]interface{}, 0)
	changeIpMap := make([]map[string]interface{}, 0)
	changeMacMap := make([]map[string]interface{}, 0)
	changeMTUMap := make([]map[string]interface{}, 0)
	detachMap := make([]map[string]interface{}, 0)
	enableMap := 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 compareNetwork(newMap, oldMap) {
				found = true
				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)
				}
				if newMap["mac"] != oldMap["mac"] && newMap["mac"].(string) != "" {
					newMap["old_mac"] = oldMap["mac"]
					changeMacMap = append(changeMacMap, newMap)
				}
				if (newMap["net_type"].(string) == "EXTNET" || newMap["net_type"].(string) == "DPDK") && (newMap["mtu"] != oldMap["mtu"] && newMap["mtu"].(int) != 0) {
					changeMTUMap = append(changeMTUMap, newMap)
				}
				if newMap["enabled"].(bool) != oldMap["enabled"].(bool) {
					mac, _ := newMap["mac"].(string)
					if mac == "" {
						newMap["mac"] = oldMap["mac"]
					}
					enableMap = append(enableMap, newMap)
				}
			}
			if found {
				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 compareNetwork(newMap, oldMap) {
				found = true
				break
			}
		}
		if found {
			continue
		}
		attachMap = append(attachMap, newMap)
	}

	res := updatedNetwork{
		DetachMap:    detachMap,
		ChangeIPMap:  changeIpMap,
		ChangeMacMap: changeMacMap,
		ChangeMTUMap: changeMTUMap,
		AttachMap:    attachMap,
		EnableMap:    enableMap,
	}

	return &res
}

func compareNetwork(newMap, oldMap map[string]interface{}) bool {
	return newMap["net_type"] == oldMap["net_type"] && newMap["net_id"] == oldMap["net_id"] && newMap["sdn_interface_id"] == oldMap["sdn_interface_id"] && newMap["weight"] == oldMap["weight"]
}

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

// addAttachedNetwork adds libvirt_settings of attached networks to the libvirt_settings settings list
func addAttachedNetwork(addedLibvirtSettings []interface{}, newLibvirtSettings []interface{}, networkAttachMap []map[string]interface{}) (addedLibvirtSettingsMap []map[string]interface{}) {
	addedLibvirtSettingsMap = make([]map[string]interface{}, 0)

	for _, attach := range networkAttachMap {
		found := false
		for _, elem := range addedLibvirtSettings {
			addedLVSMap := elem.(map[string]interface{})
			if attach["net_id"] == addedLVSMap["net_id"] && attach["net_type"] == addedLVSMap["net_type"] {
				found = true
				break
			}
		}
		if found {
			continue
		}
		for _, elem := range newLibvirtSettings {
			newVirtSettingMap := elem.(map[string]interface{})
			if attach["net_id"] == newVirtSettingMap["net_id"] && attach["net_type"] == newVirtSettingMap["net_type"] {
				addedLibvirtSettingsMap = append(addedLibvirtSettingsMap, newVirtSettingMap)
				found = true
				break
			}
		}
	}

	for _, elem := range addedLibvirtSettings {
		addedLVSMap := elem.(map[string]interface{})
		addedLibvirtSettingsMap = append(addedLibvirtSettingsMap, addedLVSMap)
	}

	return
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)
	simpleCompRec, err := utilityComputeCheckPresence(ctx, d, m)
	if err != nil {
		return err
	}

	apiErrCount := 0
	var lastSavedError error

	oldSecGroups, newSecGroups := d.GetChange("security_groups")
	updateSecGroups := (newSecGroups.(*schema.Set).Difference(oldSecGroups.(*schema.Set))).List()

	log.Debugf("utilityComputeSecGroupsConfigure: update security groups has %d items for Compute ID %s", len(updateSecGroups), d.Id())
	if len(updateSecGroups) > 0 {
		for _, elem := range updateSecGroups {
			secGroupsMap := elem.(map[string]interface{})

			netType := secGroupsMap["net_type"].(string)
			netId := uint64(secGroupsMap["net_id"].(int))
			var mac string
			for _, iface := range simpleCompRec.Interfaces {
				if iface.NetID == netId && iface.NetType == netType {
					mac = iface.MAC
					break
				}
			}
			if mac == "" {
				log.Errorf("utilityComputeSecGroupsConfigure: Network with type %s and id %d is not connected to the compute %s",
					netType, netId, d.Id())
				apiErrCount++
				lastSavedError = errors.New(fmt.Sprintf("utilityComputeSecGroupsConfigure: Network with type %s and id %d is not connected to the compute %s",
					netType, netId, d.Id()))
				continue
			}
			secGroupsIDs := make([]uint64, 0)
			for _, id := range secGroupsMap["security_groups"].(*schema.Set).List() {
				secGroupsIDs = append(secGroupsIDs, uint64(id.(int)))
			}
			log.Debugf("utilityComputeSecGroupsConfigure: Configure security groups interface parameters on Network with type %s and id %d", netType, netId)
			req := compute.ChangeSecGroupsRequest{
				ComputeID: computeId,
				Interface: mac,
				SecGroups: secGroupsIDs,
			}

			if secGroupsMap["enable_secgroups"] != nil {
				req.EnableSecGroups = secGroupsMap["enable_secgroups"].(bool)
			}

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

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

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	req := compute.UpdateRequest{
		ComputeID: computeId,
	}

	if d.HasChange("name") {
		req.Name = d.Get("name").(string)
	}
	if d.HasChange("description") {
		req.Description = d.Get("description").(string)
	}
	if d.HasChange("numa_affinity") {
		req.NumaAffinity = d.Get("numa_affinity").(string)
	}
	if d.HasChange("chipset") {
		req.Chipset = d.Get("chipset").(string)
	}

	if d.HasChange("loader_type") {
		req.LoaderType = d.Get("loader_type").(string)
	}

	if d.HasChange("boot_type") {
		req.BootType = d.Get("boot_type").(string)
	}

	if d.HasChange("hot_resize") {
		req.HotResize = d.Get("hot_resize").(bool)
	}

	if d.HasChange("network_interface_naming") {
		req.NetworkInterfaceNaming = d.Get("network_interface_naming").(string)
	}

	if d.HasChange("os_version") {
		req.OSVersion = d.Get("os_version").(string)
	}

	if d.HasChange("weight") {
		req.Weight = uint64(d.Get("weight").(int))
	}

	req.CPUPin = d.Get("cpu_pin").(bool)
	req.HPBacked = d.Get("hp_backed").(bool)
	req.AutoStart = d.Get("auto_start_w_node").(bool)

	if d.HasChange("preferred_cpu") {
		if preferredCPU, ok := d.GetOk("preferred_cpu"); ok {
			preferredList := preferredCPU.([]interface{})
			if len(preferredList) > 0 {
				for _, v := range preferredList {
					cpuNum := v.(int)
					req.PreferredCPU = append(req.PreferredCPU, int64(cpuNum))
				}
			}
		}
		oldPCPU, newPCPU := d.GetChange("preferred_cpu")
		if len(oldPCPU.([]interface{})) != 0 && len(newPCPU.([]interface{})) == 0 {
			req.PreferredCPU = []int64{-1}
		}
	}

	// Note bene: numa_affinity, cpu_pin and hp_backed are not allowed to be changed for compute in STARTED tech status.
	// If STARTED, we need to stop it before update
	var isStopRequired bool
	if d.HasChanges("numa_affinity", "cpu_pin", "hp_backed", "chipset", "preferred_cpu", "hot_resize") && d.Get("started").(bool) {
		isStopRequired = true
	}
	if isStopRequired {
		stopReq := compute.StopRequest{
			ComputeID: computeId,
			Force:     false,
		}

		if _, err := c.CloudBroker().Compute().Stop(ctx, stopReq); err != nil {
			return err
		}
	}

	// perform update
	if _, err := c.CloudBroker().Compute().Update(ctx, req); err != nil {
		return err
	}

	// If used to be STARTED, we need to start it after update
	if isStopRequired {
		req := compute.StartRequest{ComputeID: computeId}
		if altBootID, ok := d.Get("alt_boot_id").(int); ok {
			req.AltBootID = uint64(altBootID)
		}
		if _, err := c.CloudBroker().Compute().Start(ctx, req); err != nil {
			return err
		}
	}

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	affinityLabel := d.Get("affinity_label").(string)
	if affinityLabel == "" {
		req := compute.AffinityLabelRemoveRequest{
			ComputeIDs: []uint64{computeId},
		}

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

	req := compute.AffinityLabelSetRequest{
		ComputeIDs:    []uint64{computeId},
		AffinityLabel: affinityLabel,
	}

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

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	deletedAR := make([]interface{}, 0)
	addedAR := make([]interface{}, 0)

	oldAR, newAR := d.GetChange("affinity_rules")
	oldConv := oldAR.([]interface{})
	newConv := newAR.([]interface{})

	if len(newConv) == 0 {
		req := compute.AffinityRulesClearRequest{
			ComputeIDs: []uint64{computeId},
		}

		_, err := c.CloudBroker().Compute().AffinityRulesClear(ctx, req)
		if err != nil {
			return err
		}
	} else {
		for _, el := range oldConv {
			if !isContainsAR(newConv, el) {
				deletedAR = append(deletedAR, el)
			}
		}
		for _, el := range newConv {
			if !isContainsAR(oldConv, el) {
				addedAR = append(addedAR, el)
			}
		}

		if len(deletedAR) > 0 {
			for _, ar := range deletedAR {
				arConv := ar.(map[string]interface{})
				req := compute.AffinityRuleRemoveRequest{
					ComputeIDs: []uint64{computeId},
					Topology:   arConv["topology"].(string),
					Policy:     arConv["policy"].(string),
					Mode:       arConv["mode"].(string),
					Key:        arConv["key"].(string),
					Value:      arConv["value"].(string),
				}

				_, err := c.CloudBroker().Compute().AffinityRuleRemove(ctx, req)
				if err != nil {
					return err
				}
			}
		}
		if len(addedAR) > 0 {
			for _, ar := range addedAR {
				arConv := ar.(map[string]interface{})
				req := compute.AffinityRuleAddRequest{
					ComputeIDs: []uint64{computeId},
					Topology:   arConv["topology"].(string),
					Policy:     arConv["policy"].(string),
					Mode:       arConv["mode"].(string),
					Key:        arConv["key"].(string),
					Value:      arConv["value"].(string),
				}

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

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	deletedAR := make([]interface{}, 0)
	addedAR := make([]interface{}, 0)

	oldAR, newAR := d.GetChange("anti_affinity_rules")
	oldConv := oldAR.([]interface{})
	newConv := newAR.([]interface{})

	if len(newConv) == 0 {
		req := compute.AntiAffinityRulesClearRequest{
			ComputeIDs: []uint64{computeId},
		}

		_, err := c.CloudBroker().Compute().AntiAffinityRulesClear(ctx, req)
		if err != nil {
			return err
		}
	} else {
		for _, el := range oldConv {
			if !isContainsAR(newConv, el) {
				deletedAR = append(deletedAR, el)
			}
		}
		for _, el := range newConv {
			if !isContainsAR(oldConv, el) {
				addedAR = append(addedAR, el)
			}
		}

		if len(deletedAR) > 0 {
			for _, ar := range deletedAR {
				arConv := ar.(map[string]interface{})
				req := compute.AntiAffinityRuleRemoveRequest{
					ComputeIDs: []uint64{computeId},
					Topology:   arConv["topology"].(string),
					Policy:     arConv["policy"].(string),
					Mode:       arConv["mode"].(string),
					Key:        arConv["key"].(string),
					Value:      arConv["value"].(string),
				}

				_, err := c.CloudBroker().Compute().AntiAffinityRuleRemove(ctx, req)
				if err != nil {
					return err
				}
			}
		}
		if len(addedAR) > 0 {
			for _, ar := range addedAR {
				arConv := ar.(map[string]interface{})
				req := compute.AntiAffinityRuleAddRequest{
					ComputeIDs: []uint64{computeId},
					Topology:   arConv["topology"].(string),
					Policy:     arConv["policy"].(string),
					Mode:       arConv["mode"].(string),
					Key:        arConv["key"].(string),
					Value:      arConv["value"].(string),
				}

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

	return 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.CloudBroker().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.CloudBroker().Compute().AttachPCIDevice(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldSet, newSet := d.GetChange("tags")
	deletedTags := (oldSet.(*schema.Set).Difference(newSet.(*schema.Set))).List()
	if len(deletedTags) > 0 {
		for _, tagInterface := range deletedTags {
			tagItem := tagInterface.(map[string]interface{})
			req := compute.TagRemoveRequest{
				ComputeIDs: []uint64{computeId},
				Key:        tagItem["key"].(string),
			}

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

	addedTags := (newSet.(*schema.Set).Difference(oldSet.(*schema.Set))).List()
	if len(addedTags) > 0 {
		for _, tagInterface := range addedTags {
			tagItem := tagInterface.(map[string]interface{})
			req := compute.TagAddRequest{
				ComputeIDs: []uint64{computeId},
				Key:        tagItem["key"].(string),
				Value:      tagItem["value"].(string),
			}

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

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldSet, newSet := d.GetChange("port_forwarding")
	deletedPfws := (oldSet.(*schema.Set).Difference(newSet.(*schema.Set))).List()
	if len(deletedPfws) > 0 {
		for _, pfwInterface := range deletedPfws {
			pfwItem := pfwInterface.(map[string]interface{})
			req := compute.PFWDelRequest{
				ComputeID:       computeId,
				PublicPortStart: uint64(pfwItem["public_port_start"].(int)),
				Proto:           pfwItem["proto"].(string),
				RuleID:          uint64(pfwItem["rule_id"].(int)),
			}
			if pfwItem["local_port"].(int) != 0 {
				req.LocalBasePort = uint64(pfwItem["local_port"].(int))
			}
			if pfwItem["public_port_end"].(int) == -1 {
				req.PublicPortEnd = req.PublicPortStart
			} else {
				req.PublicPortEnd = uint64(pfwItem["public_port_end"].(int))
			}

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

	addedPfws := (newSet.(*schema.Set).Difference(oldSet.(*schema.Set))).List()
	if len(addedPfws) > 0 {
		for _, pfwInterface := range addedPfws {
			pfwItem := pfwInterface.(map[string]interface{})
			req := compute.PFWAddRequest{
				ComputeID:       computeId,
				PublicPortStart: uint64(pfwItem["public_port_start"].(int)),
				PublicPortEnd:   int64(pfwItem["public_port_end"].(int)),
				Proto:           pfwItem["proto"].(string),
			}
			if pfwItem["local_port"].(int) != 0 {
				req.LocalBasePort = uint64(pfwItem["local_port"].(int))
			}

			pwfId, err := c.CloudBroker().Compute().PFWAdd(ctx, req)
			if err != nil {
				return err
			}
			d.Set("rule_id", pwfId)
		}
	}

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	restoreReq := compute.RestoreRequest{ComputeID: computeId}

	_, err := c.CloudBroker().Compute().Restore(ctx, restoreReq)
	if err != nil {
		return err
	}

	if _, ok := d.GetOk("enabled"); ok {
		if err := utilityComputeEnabled(ctx, d, m); err != nil {
			return err
		}
	}

	if _, ok := d.GetOk("started"); ok {
		if err := utilityComputeStarted(ctx, d, m); err != nil {
			return err
		}
	}

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldSet, newSet := d.GetChange("user_access")
	deletedUserAcess := (oldSet.(*schema.Set).Difference(newSet.(*schema.Set))).List()
	if len(deletedUserAcess) > 0 {
		for _, userAcessInterface := range deletedUserAcess {
			userAccessItem := userAcessInterface.(map[string]interface{})
			req := compute.UserRevokeRequest{
				ComputeID: computeId,
				Username:  userAccessItem["username"].(string),
			}

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

	addedUserAccess := (newSet.(*schema.Set).Difference(oldSet.(*schema.Set))).List()
	if len(addedUserAccess) > 0 {
		for _, userAccessInterface := range addedUserAccess {
			userAccessItem := userAccessInterface.(map[string]interface{})
			req := compute.UserGrantRequest{
				ComputeID:  computeId,
				Username:   userAccessItem["username"].(string),
				AccessType: userAccessItem["access_type"].(string),
			}

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

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldSet, newSet := d.GetChange("snapshot")
	deletedSnapshots := (oldSet.(*schema.Set).Difference(newSet.(*schema.Set))).List()
	if len(deletedSnapshots) > 0 {
		for _, snapshotInterface := range deletedSnapshots {
			snapshotItem := snapshotInterface.(map[string]interface{})
			req := compute.SnapshotDeleteRequest{
				ComputeID: computeId,
				Label:     snapshotItem["label"].(string),
			}

			asyncMode, ok := d.GetOk("snapshot_delete_async")
			if ok && asyncMode.(bool) {
				_, err := c.CloudBroker().Compute().SnapshotDeleteAsync(ctx, req)
				if err != nil {
					return err
				}
			} else {
				_, err := c.CloudBroker().Compute().SnapshotDelete(ctx, req)
				if err != nil {
					return err
				}
			}
		}
	}

	addedSnapshots := (newSet.(*schema.Set).Difference(oldSet.(*schema.Set))).List()
	if len(addedSnapshots) > 0 {
		for _, snapshotInterface := range addedSnapshots {
			snapshotItem := snapshotInterface.(map[string]interface{})
			req := compute.SnapshotCreateRequest{
				ComputeID: computeId,
				Label:     snapshotItem["label"].(string),
			}

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

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	if rollback, ok := d.GetOk("rollback"); ok {
		req := compute.StopRequest{
			ComputeID: computeId,
			Force:     false,
		}

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

		rollbackInterface := rollback.(*schema.Set).List()[0]
		rollbackItem := rollbackInterface.(map[string]interface{})

		rollbackReq := compute.SnapshotRollbackRequest{
			ComputeID: computeId,
			Label:     rollbackItem["label"].(string),
		}

		_, err = c.CloudBroker().Compute().SnapshotRollback(ctx, rollbackReq)
		if err != nil {
			return err
		}

		startReq := compute.StartRequest{ComputeID: computeId}
		if altBootID, ok := d.Get("alt_boot_id").(int); ok {
			startReq.AltBootID = uint64(altBootID)
		}

		log.Debugf("utilityComputeRollback: starting compute %d", computeId)

		_, err = c.CloudBroker().Compute().Start(ctx, startReq)
		if err != nil {
			return err
		}
	}

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldSet, newSet := d.GetChange("cd")
	deletedCd := (oldSet.(*schema.Set).Difference(newSet.(*schema.Set))).List()
	if len(deletedCd) > 0 {
		req := compute.CDEjectRequest{
			ComputeID: computeId,
		}

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

	addedCd := (newSet.(*schema.Set).Difference(oldSet.(*schema.Set))).List()
	if len(addedCd) > 0 {
		cdItem := addedCd[0].(map[string]interface{})
		req := compute.CDInsertRequest{
			ComputeID: computeId,
			CDROMID:   uint64(cdItem["cdrom_id"].(int)),
		}

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

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldPin, newPin := d.GetChange("pin_to_node")
	if oldPin.(bool) && !newPin.(bool) {
		req := compute.UnpinFromNodeRequest{
			ComputeID: computeId,
		}

		_, err := c.CloudBroker().Compute().UnpinFromNode(ctx, req)
		if err != nil {
			return err
		}
	}
	if !oldPin.(bool) && newPin.(bool) {
		start, _ := d.GetOk("started")
		_, nodeOk := d.GetOk("node_id")

		if !start.(bool) && !nodeOk {
			return errors.New("cannot pin to node a VM, that is not started and node_id is not set")
		}

		req := compute.PinToNodeRequest{
			ComputeID:    computeId,
			TargetNodeID: uint64(d.Get("node_id").(int)),
		}

		if force, ok := d.Get("force_pin").(bool); ok {
			req.Force = force
		}

		if autoStart, ok := d.Get("auto_start_w_node").(bool); ok {
			req.AutoStart = autoStart
		}

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

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldPause, newPause := d.GetChange("pause")
	if oldPause.(bool) && !newPause.(bool) {
		req := compute.ResumeRequest{
			ComputeID: computeId,
		}
		_, err := c.CloudBroker().Compute().Resume(ctx, req)
		if err != nil {
			return err
		}
	}
	if !oldPause.(bool) && newPause.(bool) {
		req := compute.PauseRequest{
			ComputeID: computeId,
		}

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

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldReset, newReset := d.GetChange("reset")
	if !oldReset.(bool) && newReset.(bool) {
		req := compute.ResetRequest{
			ComputeID: computeId,
		}
		_, err := c.CloudBroker().Compute().Reset(ctx, req)
		if err != nil {
			return err
		}
	}
	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	oldImage, newImage := d.GetChange("image_id")
	stopReq := compute.StopRequest{
		ComputeID: computeId,
		Force:     false,
	}
	if forceStop, ok := d.GetOk("force_stop"); ok {
		stopReq.Force = forceStop.(bool)
	}

	_, err := c.CloudBroker().Compute().Stop(ctx, stopReq)
	if err != nil {
		return err
	}

	if oldImage.(int) != newImage.(int) {
		req := compute.RedeployRequest{
			ComputeID:       computeId,
			ImageID:         uint64(newImage.(int)),
			DataDisks:       "KEEP",
			StoragePolicyID: uint64(d.Get("storage_policy_id").(int)),
		}

		if diskSize, ok := d.GetOk("boot_disk_size"); ok {
			req.DiskSize = uint64(diskSize.(int))
		}
		if autoStart, ok := d.GetOk("started"); ok {
			req.AutoStart = autoStart.(bool)
		}
		if forceStop, ok := d.GetOk("force_stop"); ok {
			req.ForceStop = forceStop.(bool)
		}

		if osVersion, ok := d.GetOk("os_version"); ok {
			req.OSVersion = osVersion.(string)
		}

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

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	req := compute.MigrateToZoneRequest{
		ComputeID: computeId,
	}

	zoneID, ok := d.GetOk("zone_id")
	if ok {
		req.ZoneID = uint64(zoneID.(int))
	}

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

	return nil
}

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

	computeId, _ := strconv.ParseUint(d.Id(), 10, 64)

	val := d.Get("custom_fields").(string)
	val = strings.ReplaceAll(val, "\\", "")
	val = strings.ReplaceAll(val, "\n", "")
	val = strings.ReplaceAll(val, "\t", "")
	val = strings.TrimSpace(val)

	if len(val) > 0 {
		req := compute.SetCustomFieldsRequest{
			ComputeID:    computeId,
			CustomFields: val,
		}

		_, err := c.CloudBroker().Compute().SetCustomFields(ctx, req)
		if err != nil {
			return err
		}
	} else {
		req := compute.DeleteCustomFieldsRequest{
			ComputeID: computeId,
		}

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

func utilityComputeStop(ctx context.Context, d *schema.ResourceData, m interface{}) error {
	c := m.(*controller.ControllerCfg)
	req := compute.StopRequest{
		Force: true,
	}
	req.ComputeID = uint64(d.Get("compute_id").(int))

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

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

	if altBootID != 0 {
		startReq.AltBootID = altBootID
	}

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

func utilityComputeCreateBlockSize(ctx context.Context, d *schema.ResourceData, m interface{}) error {
	c := m.(*controller.ControllerCfg)
	diskList := d.Get("disks").([]interface{})

	blockSizeArr := make([]string, 0, len(diskList))
	hasAny := false
	for _, elem := range diskList {
		diskVal := elem.(map[string]interface{})
		bs := diskVal["block_size"].(string)
		blockSizeArr = append(blockSizeArr, bs)
		if bs != "" {
			hasAny = true
		}
	}

	if !hasAny {
		return nil
	}

	computeRec, err := utilityComputeCheckPresence(ctx, d, m)
	if err != nil {
		return err
	}
	bootDisk := findBootDisk(computeRec.Disks, computeRec.Chipset)
	computeDisksIDs := getComputeDiskIDs(computeRec.Disks, diskList, d.Get("extra_disks").(*schema.Set).List(), bootDisk.ID)

	for i, diskID := range computeDisksIDs {
		if i >= len(blockSizeArr) || blockSizeArr[i] == "" {
			continue
		}
		req := disks.UpdateRequest{
			DiskID:    diskID.(uint64),
			BlockSize: blockSizeArr[i],
		}
		_, err := c.CloudBroker().Disks().Update(ctx, req)
		if err != nil {
			return err
		}
	}

	return nil
}

func utilityComputeCreateIOTune(ctx context.Context, d *schema.ResourceData, m interface{}) error {
	c := m.(*controller.ControllerCfg)
	diskList := d.Get("disks").([]interface{})

	iotuneArr := make([]interface{}, 0, len(diskList))
	hasAny := false
	for _, elem := range diskList {
		diskVal := elem.(map[string]interface{})
		iotune := diskVal["iotune"].([]interface{})
		iotuneArr = append(iotuneArr, iotune)
		if len(iotune) > 0 {
			hasAny = true
		}
	}

	if !hasAny {
		return nil
	}

	computeRec, err := utilityComputeCheckPresence(ctx, d, m)
	if err != nil {
		return err
	}
	bootDisk := findBootDisk(computeRec.Disks, computeRec.Chipset)
	computeDisksIDs := getComputeDiskIDs(computeRec.Disks, diskList, d.Get("extra_disks").(*schema.Set).List(), bootDisk.ID)

	for i, diskID := range computeDisksIDs {
		if i >= len(iotuneArr) {
			continue
		}
		iotune, ok := iotuneArr[i].([]interface{})
		if !ok || len(iotune) == 0 {
			continue
		}
		iotuneMap := iotune[0].(map[string]interface{})
		req := disks.LimitIORequest{
			DiskID:           diskID.(uint64),
			ReadBytesSec:     uint64(iotuneMap["read_bytes_sec"].(int)),
			ReadBytesSecMax:  uint64(iotuneMap["read_bytes_sec_max"].(int)),
			ReadIOPSSec:      uint64(iotuneMap["read_iops_sec"].(int)),
			ReadIOPSSecMax:   uint64(iotuneMap["read_iops_sec_max"].(int)),
			SizeIOPSSec:      uint64(iotuneMap["size_iops_sec"].(int)),
			TotalBytesSec:    uint64(iotuneMap["total_bytes_sec"].(int)),
			TotalBytesSecMax: uint64(iotuneMap["total_bytes_sec_max"].(int)),
			TotalIOPSSec:     uint64(iotuneMap["total_iops_sec"].(int)),
			TotalIOPSSecMax:  uint64(iotuneMap["total_iops_sec_max"].(int)),
			WriteBytesSec:    uint64(iotuneMap["write_bytes_sec"].(int)),
			WriteBytesSecMax: uint64(iotuneMap["write_bytes_sec_max"].(int)),
			WriteIOPSSec:     uint64(iotuneMap["write_iops_sec"].(int)),
			WriteIOPSSecMax:  uint64(iotuneMap["write_iops_sec_max"].(int)),
		}
		_, err := c.CloudBroker().Disks().LimitIO(ctx, req)
		if err != nil {
			return err
		}
	}

	return nil
}

func utilityComputeCreatePresentDisk(ctx context.Context, d *schema.ResourceData, m interface{}) error {
	c := m.(*controller.ControllerCfg)
	var errs error
	diskList := d.Get("disks")
	presentArr := make([]interface{}, 0)
	for _, elem := range diskList.([]interface{}) {
		diskVal := elem.(map[string]interface{})
		presentArr = append(presentArr, diskVal["node_ids"])
	}

	computeRec, err := utilityComputeCheckPresence(ctx, d, m)
	bootDisk := findBootDisk(computeRec.Disks, computeRec.Chipset)
	if err != nil {
		errs = errors.Join(err)
	}
	computeDisksIDs := getComputeDiskIDs(computeRec.Disks, d.Get("disks").([]interface{}), d.Get("extra_disks").(*schema.Set).List(), bootDisk.ID)

	for i, diskID := range computeDisksIDs {
		if len(presentArr) <= i || presentArr[i] == nil {
			continue
		}
		presentIDs := presentArr[i].(*schema.Set).List()

		if len(presentIDs) > 0 {
			log.Debugf("resourceComputeCreate: start presents disk ID:%d to nodes", diskID)
		}

		for _, presentID := range presentIDs {
			nodeID := uint64(presentID.(int))

			req := disks.PresentRequest{
				DiskID: diskID.(uint64),
				NodeID: nodeID,
			}

			_, err := c.CloudBroker().Disks().Present(ctx, req)
			if err != nil {
				errs = errors.Join(err)
			}
		}
	}

	return errs
}

func isResizeDisk(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elConv["disk_id"].(int) &&
			elOldConv["size"].(int) != elConv["size"].(int) {
			return true
		}
	}
	return false
}

func isRenameDisk(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elConv["disk_id"].(int) &&
			elOldConv["disk_name"].(string) != elConv["disk_name"].(string) {
			return true
		}
	}
	return false
}

func isChangeCacheDisk(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elConv["disk_id"].(int) &&
			elOldConv["cache"].(string) != elConv["cache"].(string) {
			return true
		}
	}
	return false
}

func isChangeBLKDiscardDisk(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elConv["disk_id"].(int) &&
			elOldConv["blk_discard"].(bool) != elConv["blk_discard"].(bool) {
			return true
		}
	}
	return false
}

func isChangeBlockSizeDisk(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elConv["disk_id"].(int) &&
			elOldConv["block_size"].(string) != elConv["block_size"].(string) {
			return true
		}
	}
	return false
}

func isChangeIOTuneDisk(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) != elConv["disk_id"].(int) {
			continue
		}
		oldIOTune := elOldConv["iotune"].([]interface{})
		newIOTune := elConv["iotune"].([]interface{})
		if len(oldIOTune) == 0 && len(newIOTune) == 0 {
			return false
		}
		if len(oldIOTune) == 0 || len(newIOTune) == 0 {
			return true
		}
		oldMap := oldIOTune[0].(map[string]interface{})
		newMap := newIOTune[0].(map[string]interface{})
		return oldMap["read_bytes_sec"].(int) != newMap["read_bytes_sec"].(int) ||
			oldMap["read_bytes_sec_max"].(int) != newMap["read_bytes_sec_max"].(int) ||
			oldMap["read_iops_sec"].(int) != newMap["read_iops_sec"].(int) ||
			oldMap["read_iops_sec_max"].(int) != newMap["read_iops_sec_max"].(int) ||
			oldMap["size_iops_sec"].(int) != newMap["size_iops_sec"].(int) ||
			oldMap["total_bytes_sec"].(int) != newMap["total_bytes_sec"].(int) ||
			oldMap["total_bytes_sec_max"].(int) != newMap["total_bytes_sec_max"].(int) ||
			oldMap["total_iops_sec"].(int) != newMap["total_iops_sec"].(int) ||
			oldMap["total_iops_sec_max"].(int) != newMap["total_iops_sec_max"].(int) ||
			oldMap["write_bytes_sec"].(int) != newMap["write_bytes_sec"].(int) ||
			oldMap["write_bytes_sec_max"].(int) != newMap["write_bytes_sec_max"].(int) ||
			oldMap["write_iops_sec"].(int) != newMap["write_iops_sec"].(int) ||
			oldMap["write_iops_sec_max"].(int) != newMap["write_iops_sec_max"].(int)
	}
	return false
}

func isChangeStoragePolicy(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elConv["disk_id"].(int) &&
			elOldConv["storage_policy_id"].(int) != elConv["storage_policy_id"].(int) {
			return true
		}
	}
	return false
}

func isMigrateDisk(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elConv["disk_id"].(int) &&
			elOldConv["disk_id"].(int) != 0 {
			sepIDChanged := elOldConv["sep_id"].(int) != elConv["sep_id"].(int)
			poolChanged := elOldConv["pool"].(string) != elConv["pool"].(string)
			if sepIDChanged || poolChanged {
				return true
			}
		}
	}
	return false
}

func isContainsDisk(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elConv["disk_id"].(int) {
			return true
		}
	}
	return false
}

// isChangeNodesDisk get slice of new disks values and current value disk,
// if need change nodes on disk returns true and new disk value, else return false and nil
func isChangeNodesDisk(els []interface{}, elOld interface{}) (bool, interface{}) {
	for _, elNew := range els {
		elNewConv := elNew.(map[string]interface{})
		elOldConv := elOld.(map[string]interface{})
		if elOldConv["disk_id"].(int) == elNewConv["disk_id"].(int) {
			newArr := elNewConv["node_ids"]
			oldArr := elOldConv["node_ids"]
			presentID := (newArr.(*schema.Set).Difference(oldArr.(*schema.Set))).List()
			depresentID := (oldArr.(*schema.Set).Difference(newArr.(*schema.Set))).List()
			if len(presentID) > 0 || len(depresentID) > 0 {
				return true, elNew
			}
			return false, nil
		}
	}
	return false, nil
}

func utilityComputeMigrateDisks(ctx context.Context, d *schema.ResourceData, m interface{}, migratedDisks, oldDisks []interface{}) error {
	c := m.(*controller.ControllerCfg)

	for _, disk := range migratedDisks {
		diskConv := disk.(map[string]interface{})

		oldDiskID := uint64(diskConv["disk_id"].(int))
		if oldDiskID == 0 {
			log.Debugf("utilityComputeMigrateDisks: skipping disk with id=0")
			continue
		}

		newSepID := uint64(diskConv["sep_id"].(int))
		newPool := diskConv["pool"].(string)
		storagePolicyID := uint64(diskConv["storage_policy_id"].(int))

		log.Debugf("utilityComputeMigrateDisks: migrating disk_id=%d to sep_id=%d, pool=%s", oldDiskID, newSepID, newPool)

		migrateReq := disks.MigrateRequest{
			DiskID:          oldDiskID,
			SEPID:           newSepID,
			PoolName:        newPool,
			StoragePolicyID: storagePolicyID,
		}

		taskID, err := c.CloudBroker().Disks().Migrate(ctx, migrateReq)
		if err != nil {
			return fmt.Errorf("failed to start disk migration for disk_id=%d: %w", oldDiskID, err)
		}

		log.Debugf("utilityComputeMigrateDisks: disk migration started, taskID=%s", taskID)

		newDiskID, err := utilityComputeWaitForMigrationTask(ctx, c, taskID, oldDiskID)
		if err != nil {
			return fmt.Errorf("disk migration task failed for disk_id=%d: %w", oldDiskID, err)
		}

		log.Debugf("utilityComputeMigrateDisks: disk migration completed, old_disk_id=%d, new_disk_id=%d", oldDiskID, newDiskID)

	}

	return nil
}

func utilityComputeWaitForMigrationTask(ctx context.Context, c *controller.ControllerCfg, taskID string, oldDiskID uint64) (uint64, error) {

	for {

		time.Sleep(15 * time.Second)
		taskReq := tasks.GetRequest{
			AuditID: strings.Trim(taskID, `"`),
		}

		taskInfo, err := c.CloudBroker().Tasks().Get(ctx, taskReq)
		if err != nil {
			return 0, fmt.Errorf("failed to get task status: %w", err)
		}

		log.Debugf("utilityComputeWaitForMigrationTask: taskID=%s, completed=%t, status=%s", taskID, taskInfo.Completed, taskInfo.Status)

		if taskInfo.Completed {
			if taskInfo.Error != "" {
				return 0, fmt.Errorf("migration task failed with error: %s", taskInfo.Error)
			}

			resultStr, err := taskInfo.Result.ToString()
			if err != nil {
				return 0, fmt.Errorf("failed to get task result: %w", err)
			}

			log.Debugf("utilityComputeWaitForMigrationTask: migration result: %s", resultStr)

			newDiskID, err := extractNewDiskIDFromResult(resultStr)
			if err != nil {
				return 0, fmt.Errorf("failed to parse migration result: %w", err)
			}

			return newDiskID, nil
		}
	}

}

func extractNewDiskIDFromResult(result string) (uint64, error) {
	re := regexp.MustCompile(`Disk ID \d+ successfully migrated to Disk ID (\d+)`)
	matches := re.FindStringSubmatch(result)

	if len(matches) < 2 {
		return 0, fmt.Errorf("could not extract new disk ID from result: %s", result)
	}

	newDiskID, err := strconv.ParseUint(matches[1], 10, 64)
	if err != nil {
		return 0, fmt.Errorf("failed to parse new disk ID: %w", err)
	}

	return newDiskID, nil
}

func isContainsAR(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["key"].(string) == elConv["key"].(string) &&
			elOldConv["value"].(string) == elConv["value"].(string) &&
			elOldConv["mode"].(string) == elConv["mode"].(string) &&
			elOldConv["topology"].(string) == elConv["topology"].(string) &&
			elOldConv["policy"].(string) == elConv["policy"].(string) {
			return true
		}
	}
	return false
}

func getComputeDiskIDs(disksList compute.ListDisks, disksBlocks, extraDisks []interface{}, bootDiskId uint64) []interface{} {
	res := make([]interface{}, 0)

	if len(disksBlocks) == 0 {
		return res
	}

	sort.Slice(disksList, func(i, j int) bool {
		return disksList[i].ID < disksList[j].ID
	})

	for _, disk := range disksList {
		if disk.ID == bootDiskId || findInExtraDisks(uint(disk.ID), extraDisks) { //skip main bootdisk and extraDisks
			continue
		}

		res = append(res, disk.ID)
	}

	return res
}

func enabledNetwork(rawNetworkConfig cty.Value, netID uint64, netType string) bool {
	for _, netConfigVal := range rawNetworkConfig.AsValueSlice() {
		if netConfigVal.IsNull() {
			continue
		}

		netConfig := netConfigVal.AsValueMap()

		tempID, _ := netConfig["net_id"].AsBigFloat().Int64()
		configNetID := uint64(tempID)

		configNetType := netConfig["net_type"].AsString()

		if configNetID == netID && configNetType == netType {
			enabledVal := netConfig["enabled"]
			return !enabledVal.IsNull()
		}
	}

	return false
}
