/*
Copyright (c) 2019-2023 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>
Tim Tkachev, <tvtkachev@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 extnet

import (
	"context"
	"errors"
	"fmt"

	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
	log "github.com/sirupsen/logrus"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker/extnet"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/controller"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/service/cloudbroker/ic"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/status"
)

func handleExcludedIPsUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	old_set, new_set := d.GetChange("excluded_ips")

	detach_set := old_set.(*schema.Set).Difference(new_set.(*schema.Set))
	if detach_set.Len() > 0 {
		ips := make([]string, 0)
		for _, detach_ip := range detach_set.List() {
			ips = append(ips, detach_ip.(string))
		}

		log.Debugf("cloudbroker: removing %d IP address(es) from excluded list", detach_set.Len())
		req := extnet.IPsIncludeRequest{
			NetID: recNet.ID,
			IPs:   ips,
		}

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

	attach_set := new_set.(*schema.Set).Difference(old_set.(*schema.Set))
	if attach_set.Len() > 0 {
		ips := make([]string, 0)
		for _, attach_ip := range attach_set.List() {
			ips = append(ips, attach_ip.(string))
		}

		log.Debugf("cloudbroker: excluding %d IP address(es) from extnet with id %d", attach_set.Len(), recNet.ID)
		req := extnet.IPsExcludeRequest{
			NetID: recNet.ID,
			IPs:   ips,
		}

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

	return nil
}

func handleSetDefault(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	set_default := d.Get("set_default").(bool)
	if set_default && !recNet.Default {
		req := extnet.SetDefaultRequest{
			NetID: recNet.ID,
		}

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

	return nil
}

func handleBasicUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	doBasicUpdate := false
	basiUpdateReq := extnet.UpdateRequest{NetID: recNet.ID}

	if d.HasChange("name") {
		basiUpdateReq.Name = d.Get("name").(string)
		doBasicUpdate = true
	}
	if d.HasChange("desc") {
		basiUpdateReq.Description = d.Get("desc").(string)
		doBasicUpdate = true
	}
	if d.HasChange("mtu") {
		basiUpdateReq.MTU = uint64(d.Get("mtu").(int))
		doBasicUpdate = true
	}
	if d.HasChange("enable_secgroups") {
		basiUpdateReq.EnableSecGroups = d.Get("enable_secgroups").(bool)
		doBasicUpdate = true
	}

	if doBasicUpdate {
		_, err := c.CloudBroker().ExtNet().Update(ctx, basiUpdateReq)
		if err != nil {
			return err
		}
	}

	return nil
}

func handleEnableUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	enable := d.Get("enable").(bool)
	if enable {
		if recNet.Status == status.Disabled {
			req := extnet.EnableRequest{NetID: recNet.ID}
			_, err := c.CloudBroker().ExtNet().Enable(ctx, req)
			if err != nil {
				return err
			}
		}
	} else {
		if recNet.Status == status.Enabled {
			req := extnet.DisableRequest{NetID: recNet.ID}
			_, err := c.CloudBroker().ExtNet().Disable(ctx, req)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func handleDefaultQOSUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	qos := d.Get("default_qos").([]interface{})[0].(map[string]interface{})

	req := extnet.DefaultQOSUpdateRequest{
		NetID:        recNet.ID,
		IngressRate:  uint64(qos["in_rate"].(int)),
		IngressBurst: uint64(qos["in_burst"].(int)),
		EgressRate:   uint64(qos["e_rate"].(int)),
	}

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

	return nil
}

func handleNTPUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	changed_list := d.Get("ntp").([]interface{})

	ntp_list := make([]string, 0)
	for _, ntp_address := range changed_list {
		ntp_list = append(ntp_list, ntp_address.(string))
	}

	req := extnet.NTPApplyRequest{
		NetID:   recNet.ID,
		NTPList: ntp_list,
	}

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

	return nil
}

func handleDNSUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	changed_list := d.Get("dns").([]interface{})

	dns_list := make([]string, 0)
	for _, dns_address := range changed_list {
		dns_list = append(dns_list, dns_address.(string))
	}

	req := extnet.DNSApplyRequest{
		NetID:   recNet.ID,
		DNSList: dns_list,
	}

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

	return nil
}

func handleExcludedIPsRangeUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	old_set, new_set := d.GetChange("excluded_ips_range")

	detach_set := old_set.(*schema.Set).Difference(new_set.(*schema.Set))
	if detach_set.Len() > 0 {
		for _, detach_ip_range := range detach_set.List() {

			log.Debugf("cloudbroker: removing range of IP addreses from excluded range list")

			req := extnet.IPsIncludeRangeRequest{
				NetID:   recNet.ID,
				IPStart: detach_ip_range.(map[string]interface{})["ip_start"].(string),
				IPEnd:   detach_ip_range.(map[string]interface{})["ip_end"].(string),
			}

			_, err := c.CloudBroker().ExtNet().IPsIncludeRange(ctx, req)
			if err != nil {
				return err
			}
		}
	}
	attach_set := new_set.(*schema.Set).Difference(old_set.(*schema.Set))
	if attach_set.Len() > 0 {
		for _, attach_ip_range := range attach_set.List() {

			log.Debugf("cloudbroker: excluding range of IP addreses from excluded range list")

			req := extnet.IPsExcludeRangeRequest{
				NetID:   recNet.ID,
				IPStart: attach_ip_range.(map[string]interface{})["ip_start"].(string),
				IPEnd:   attach_ip_range.(map[string]interface{})["ip_end"].(string),
			}

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

func handleSharedWithUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg) error {
	oldSet, newSet := d.GetChange("shared_with")

	deletedAccountIds := (oldSet.(*schema.Set).Difference(newSet.(*schema.Set))).List()
	if len(deletedAccountIds) > 0 {
		for _, accountIdInterface := range deletedAccountIds {
			req := extnet.AccessRemoveRequest{
				NetID:     uint64(d.Get("extnet_id").(int)),
				AccountID: uint64(accountIdInterface.(int)),
			}

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

	addedAccountIds := (newSet.(*schema.Set).Difference(oldSet.(*schema.Set))).List()
	if len(addedAccountIds) > 0 {
		for _, accountIdInterface := range addedAccountIds {
			req := extnet.AccessAddRequest{
				NetID:     uint64(d.Get("extnet_id").(int)),
				AccountID: uint64(accountIdInterface.(int)),
			}

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

	return nil
}

func handleVirtualUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	virtualOld, virtualNew := d.GetChange("virtual")

	if !virtualOld.(bool) && virtualNew.(bool) {
		req := extnet.DeviceRemoveRequest{NetID: recNet.ID}
		_, err := c.CloudBroker().ExtNet().DeviceRemove(ctx, req)
		if err != nil {
			return err
		}

	} else if virtualOld.(bool) && !virtualNew.(bool) {
		req := extnet.DeviceDeployRequest{NetID: recNet.ID}
		_, err := c.CloudBroker().ExtNet().DeviceDeploy(ctx, req)
		if err != nil {
			return err
		}
	}
	return nil
}

func handleRestartUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	restartOld, restartNew := d.GetChange("restart")

	if !restartOld.(bool) && restartNew.(bool) {
		req := extnet.DeviceRestartRequest{NetID: recNet.ID}
		_, err := c.CloudBroker().ExtNet().DeviceRestart(ctx, req)
		if err != nil {
			return err
		}
	}
	return nil
}

func handleMigrateUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	nodeId := uint64(d.Get("migrate").(int))

	if err := ic.ExistNode(ctx, nodeId, c); err != nil {
		return err
	}

	req := extnet.DeviceMigrateRequest{
		NetID:  recNet.ID,
		NodeID: nodeId,
	}

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

	return nil
}

func handleZoneIDUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	zoneID := uint64(d.Get("zone_id").(int))

	req := extnet.MigrateToZoneRequest{
		NetID:  recNet.ID,
		ZoneID: zoneID,
	}

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

	return nil
}

func handleHAUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {
	highlyAvailable := d.Get("highly_available").(bool)

	req := extnet.SetHAModeRequest{
		NetID:  recNet.ID,
		HAMode: highlyAvailable,
	}

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

	return nil
}

func checkReserveIp(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg) error {
	var err error
	if d.Get("reserved_ip").(*schema.Set).Len() > 0 {
		reservedIPList := d.Get("reserved_ip").(*schema.Set).List()
		accountMap := make(map[int]struct{}, len(reservedIPList))
		for _, reservedIP := range reservedIPList {
			reservedIPMap := reservedIP.(map[string]interface{})
			accountId := reservedIPMap["account_id"].(int)
			if _, ok := accountMap[accountId]; ok {
				err = errors.Join(err, fmt.Errorf("checkReserveIp: you must have only one block with id %d", accountId))
			}
			accountMap[accountId] = struct{}{}
			_, okCount := reservedIPMap["ip_count"]
			if !okCount && reservedIPMap["ips"].(*schema.Set).Len() == 0 {
				err = errors.Join(err, fmt.Errorf("checkReserveIp: either ip_count or set of ips must be specified"))
			}
			existErr := ic.ExistAccount(ctx, uint64(accountId), c)
			if existErr != nil {
				err = errors.Join(err, existErr)
			}
		}
	}

	return err
}

func reservedIPsUpdate(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg, recNet *extnet.RecordExtNet) error {

	addSet, delSet, err := differenceIPReserved(ctx, d, c)
	if err != nil {
		return err
	}

	if len(delSet) > 0 {
		for _, del := range delSet {
			delMap := del.(map[string]interface{})

			log.Debugf("reservedIPsUpdate: removing reserved IPs for account %d", delMap["account_id"].(int))

			req := extnet.DelReserveIPRequest{
				AccountID: uint64(delMap["account_id"].(int)),
				ExtNetID:  recNet.ID,
			}
			if ipCount, ok := delMap["ip_count"]; ok {
				req.IPCount = uint64(ipCount.(int))
			}
			if delIPs, ok := delMap["ips"]; ok {
				ips := delIPs.(*schema.Set).List()
				for _, ip := range ips {
					req.IPs = append(req.IPs, ip.(string))
				}
			}

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

	if len(addSet) > 0 {
		for _, add := range addSet {
			addMap := add.(map[string]interface{})

			log.Debugf("reservedIPsUpdate: add reserved IPs for account %d", addMap["account_id"].(int))

			req := extnet.AddReserveIPRequest{
				AccountID: uint64(addMap["account_id"].(int)),
				ExtNetID:  recNet.ID,
			}
			if ipCount, ok := addMap["ip_count"]; ok {
				req.IPCount = uint64(ipCount.(int))
			}
			if addIPs, ok := addMap["ips"]; ok {
				ips := addIPs.(*schema.Set).List()
				for _, ip := range ips {
					req.IPs = append(req.IPs, ip.(string))
				}
			}

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

	return nil
}

func differenceIPReserved(ctx context.Context, d *schema.ResourceData, c *controller.ControllerCfg) (addList, delList []interface{}, errs error) {
	addList = make([]interface{}, 0)
	delList = make([]interface{}, 0)
	oldSet, newSet := d.GetChange("reserved_ip")
	oldList := oldSet.(*schema.Set).List()
	newList := newSet.(*schema.Set).List()
	for _, oldReservedIP := range oldList {
		oldMap := oldReservedIP.(map[string]interface{})
		found := false
		for _, newReservedIP := range newList {
			newMap := newReservedIP.(map[string]interface{})
			if newMap["account_id"] == oldMap["account_id"] && newMap["ip_count"] == oldMap["ip_count"] && newMap["ips"] == oldMap["ips"] {
				found = true
				break
			}
			if newMap["account_id"] == oldMap["account_id"] {
				add := make(map[string]interface{}, 0)
				del := make(map[string]interface{}, 0)
				delSet := oldMap["ips"].(*schema.Set).Difference(newMap["ips"].(*schema.Set))
				addSet := newMap["ips"].(*schema.Set).Difference(oldMap["ips"].(*schema.Set))
				if delSet.Len() > 0 {
					del["account_id"] = oldMap["account_id"].(int)
					del["ips"] = delSet
					if oldMap["ip_count"].(int)-newMap["ip_count"].(int) >= delSet.Len() {
						del["ip_count"] = oldMap["ip_count"].(int) - newMap["ip_count"].(int)
					} else {
						del["ip_count"] = delSet.Len()
						newMap["ip_count"] = newMap["ip_count"].(int) + delSet.Len()
					}
				} else if newMap["ip_count"].(int) < oldMap["ip_count"].(int) {
					del["account_id"] = oldMap["account_id"].(int)
					del["ip_count"] = oldMap["ip_count"].(int) - newMap["ip_count"].(int)
				}
				if addSet.Len() > 0 {
					add["account_id"] = oldMap["account_id"].(int)
					add["ips"] = addSet
					add["ip_count"] = newMap["ip_count"].(int) - oldMap["ip_count"].(int)
					if add["ip_count"].(int) < 0 {
						add["ip_count"] = 0
					}
					if add["ip_count"].(int)-addSet.Len() < 0 {
						del["account_id"] = oldMap["account_id"].(int)
						if _, ok := del["ip_count"]; ok {
							del["ip_count"] = del["ip_count"].(int) + addSet.Len()
						} else {
							del["ip_count"] = addSet.Len()
						}
					}
				} else if newMap["ip_count"].(int)-oldMap["ip_count"].(int) > 0 {
					add["account_id"] = oldMap["account_id"].(int)
					add["ip_count"] = newMap["ip_count"].(int) - oldMap["ip_count"].(int)
				}
				if _, ok := add["account_id"]; ok {
					addList = append(addList, add)
				}
				if _, ok := del["account_id"]; ok {
					ipsLen := 0
					if _, ok := del["ips"]; ok {
						ipsLen = del["ips"].(*schema.Set).Len()
					}
					freeCount := del["ip_count"].(int) - ipsLen
					if freeCount > 0 {
						req := extnet.GetReservedIP{
							AccountID: uint64(del["account_id"].(int)),
							ExtNetID:  uint64(d.Get("extnet_id").(int)),
						}
						resIpsList, err := c.CloudBroker().ExtNet().GetReservedIP(ctx, req)
						if err != nil {
							errs = errors.Join(errs, err)
						}
						freeIPs := getFreeIps(resIpsList[0].Reservations, newMap["ips"], del["ips"])
						if _, ok := del["ips"]; !ok {
							del["ips"] = schema.NewSet(schema.HashString, []interface{}{})
						}
						for i := 0; i < freeCount; i++ {
							del["ips"].(*schema.Set).Add(freeIPs[i])
						}
					}
					delList = append(delList, del)
				}
				found = true
				break
			}
		}
		if found {
			continue
		}
		delList = append(delList, oldReservedIP)
	}

	for _, newReservedIP := range newList {
		newMap := newReservedIP.(map[string]interface{})
		found := false
		for _, oldReservedIP := range oldList {
			oldMap := oldReservedIP.(map[string]interface{})
			if newMap["account_id"] == oldMap["account_id"] {
				found = true
				break
			}
		}
		if found {
			continue
		}
		addList = append(addList, newReservedIP)
	}

	if errs != nil {
		d.Set("reserved_ip", oldSet)
	}

	return
}

// getFreeIps returns array IPs which can be deleted
func getFreeIps(reserved []extnet.Reservations, newIps, delIps interface{}) []string {
	newIpsList := make([]interface{}, 0)
	delIpsList := make([]interface{}, 0)
	if newIps != nil {
		newIpsList = newIps.(*schema.Set).List()
	}
	if delIps != nil {
		delIpsList = delIps.(*schema.Set).List()
	}
	newIpsMap := make(map[string]struct{}, len(newIpsList))
	delIpsMap := make(map[string]struct{}, len(delIpsList))
	freeIPs := make([]string, 0)
	for _, ip := range newIpsList {
		newIpsMap[ip.(string)] = struct{}{}
	}
	for _, ip := range delIpsList {
		delIpsMap[ip.(string)] = struct{}{}
	}
	for _, item := range reserved {
		if _, ok := newIpsMap[item.IP]; ok {
			continue
		}
		if _, ok := delIpsMap[item.IP]; ok {
			continue
		}
		freeIPs = append(freeIPs, item.IP)
	}
	return freeIPs
}

func validateReserveIPs(ctx context.Context, d *schema.ResourceDiff, m interface{}) error {
	list := d.Get("reserved_ip").(*schema.Set).List()
	var errs error
	for _, reservedIP := range list {
		reservedIPMap := reservedIP.(map[string]interface{})
		var countIP, ipsLen int
		if _, ok := reservedIPMap["ip_count"]; ok {
			countIP = reservedIPMap["ip_count"].(int)
		}
		if _, ok := reservedIPMap["ips"]; ok {
			ipsLen = reservedIPMap["ips"].(*schema.Set).Len()
		}
		if ipsLen > countIP {
			errs = errors.Join(errs, fmt.Errorf("for the reserved_ip block with account_id %d the count parameter must be greater than or equal to len the ips array", reservedIPMap["account_id"].(int)))
		}
	}
	return errs
}
