// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package eni

import (
	"context"
	"log/slog"
	"maps"

	eniTypes "github.com/cilium/cilium/pkg/alibabacloud/eni/types"
	"github.com/cilium/cilium/pkg/alibabacloud/types"
	"github.com/cilium/cilium/pkg/ipam"
	ipamTypes "github.com/cilium/cilium/pkg/ipam/types"
	v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/logging/logfields"
	"github.com/cilium/cilium/pkg/time"
)

// AlibabaCloudAPI is the API surface used of the ECS API
type AlibabaCloudAPI interface {
	GetInstance(ctx context.Context, vpcs ipamTypes.VirtualNetworkMap, subnets ipamTypes.SubnetMap, instanceID string) (*ipamTypes.Instance, error)
	GetInstances(ctx context.Context, vpcs ipamTypes.VirtualNetworkMap, subnets ipamTypes.SubnetMap) (*ipamTypes.InstanceMap, error)
	GetVSwitches(ctx context.Context) (ipamTypes.SubnetMap, error)
	GetVPC(ctx context.Context, vpcID string) (*ipamTypes.VirtualNetwork, error)
	GetVPCs(ctx context.Context) (ipamTypes.VirtualNetworkMap, error)
	GetSecurityGroups(ctx context.Context) (types.SecurityGroupMap, error)
	CreateNetworkInterface(ctx context.Context, secondaryPrivateIPCount int, vSwitchID string, groups []string, tags map[string]string) (string, *eniTypes.ENI, error)
	AttachNetworkInterface(ctx context.Context, instanceID, eniID string) error
	WaitENIAttached(ctx context.Context, eniID string) (string, error)
	DeleteNetworkInterface(ctx context.Context, eniID string) error
	AssignPrivateIPAddresses(ctx context.Context, eniID string, toAllocate int) ([]string, error)
	UnassignPrivateIPAddresses(ctx context.Context, eniID string, addresses []string) error
}

// InstancesManager maintains the list of instances. It must be kept up to date
// by calling resync() regularly.
type InstancesManager struct {
	logger *slog.Logger

	// resyncLock ensures instance incremental resync do not run at the same time as a full API resync
	resyncLock lock.RWMutex

	// mutex protects the fields below
	mutex          lock.RWMutex
	instances      *ipamTypes.InstanceMap
	vSwitches      ipamTypes.SubnetMap
	vpcs           ipamTypes.VirtualNetworkMap
	securityGroups types.SecurityGroupMap
	api            AlibabaCloudAPI
}

// NewInstancesManager returns a new instances manager
func NewInstancesManager(logger *slog.Logger, api AlibabaCloudAPI) *InstancesManager {
	return &InstancesManager{
		logger:    logger.With(subsysLogAttr...),
		instances: ipamTypes.NewInstanceMap(),
		api:       api,
	}
}

// CreateNode
func (m *InstancesManager) CreateNode(obj *v2.CiliumNode, node *ipam.Node) ipam.NodeOperations {
	return &Node{logger: m.logger, k8sObj: obj, manager: m, node: node, instanceID: node.InstanceID()}
}

// HasInstance returns whether the instance is in instances
func (m *InstancesManager) HasInstance(instanceID string) bool {
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	return m.instances.Exists(instanceID)
}

// GetPoolQuota returns the number of available IPs in all IP pools
func (m *InstancesManager) GetPoolQuota() ipamTypes.PoolQuotaMap {
	pool := ipamTypes.PoolQuotaMap{}
	for subnetID, subnet := range m.GetVSwitches() {
		pool[ipamTypes.PoolID(subnetID)] = ipamTypes.PoolQuota{
			AvailabilityZone: subnet.AvailabilityZone,
			AvailableIPs:     subnet.AvailableAddresses,
		}
	}
	return pool
}

// Resync fetches the list of ECS instances and vSwitches and updates the local
// cache in the instanceManager. It returns the time when the resync has
// started or time.Time{} if it did not complete.
func (m *InstancesManager) Resync(ctx context.Context) time.Time {
	// Full API resync should block the instance incremental resync from all nodes.
	m.resyncLock.Lock()
	defer m.resyncLock.Unlock()
	// An empty instanceID indicates the full resync.
	return m.resync(ctx, "")
}

// InstanceSync fetches the ECS instance by the given ID and vSwitches and updates the local
// cache in the instanceManager. It returns the time when the resync has
// started or time.Time{} if it did not complete.
func (m *InstancesManager) InstanceSync(ctx context.Context, instanceID string) time.Time {
	// Instance incremental resync from different nodes should be executed in parallel,
	// but must block the full API resync.
	m.resyncLock.RLock()
	defer m.resyncLock.RUnlock()
	return m.resync(ctx, instanceID)
}

func (m *InstancesManager) resync(ctx context.Context, instanceID string) time.Time {
	resyncStart := time.Now()

	vpcs, err := m.api.GetVPCs(ctx)
	if err != nil {
		m.logger.Warn("Unable to synchronize VPC list", logfields.Error, err)
		return time.Time{}
	}

	vSwitches, err := m.api.GetVSwitches(ctx)
	if err != nil {
		m.logger.Warn("Unable to retrieve VPC vSwitches list", logfields.Error, err)
		return time.Time{}
	}

	securityGroups, err := m.api.GetSecurityGroups(ctx)
	if err != nil {
		m.logger.Warn("Unable to retrieve ECS security group list", logfields.Error, err)
		return time.Time{}
	}

	// An empty instanceID indicates that this is full resync, ENIs from all instances
	// will be refetched from ECS API and updated to the local cache. Otherwise only
	// the given instance will be updated.
	if instanceID == "" {
		instances, err := m.api.GetInstances(ctx, vpcs, vSwitches)
		if err != nil {
			m.logger.Warn("Unable to synchronize ECS interface list", logfields.Error, err)
			return time.Time{}
		}

		m.logger.Info(
			"Synchronized ENI information",
			logfields.NumInstances, instances.NumInstances(),
			logfields.NumVPCs, len(vpcs),
			logfields.NumVSwitches, len(vSwitches),
			logfields.NumSecurityGroups, len(securityGroups),
		)

		m.mutex.Lock()
		defer m.mutex.Unlock()
		m.instances = instances
	} else {
		instance, err := m.api.GetInstance(ctx, vpcs, vSwitches, instanceID)
		if err != nil {
			m.logger.Warn("Unable to synchronize ECS interface list", logfields.Error, err)
			return time.Time{}
		}

		m.logger.Info(
			"Synchronized ENI information for the corresponding instance",
			logfields.InstanceID, instanceID,
			logfields.NumVPCs, len(vpcs),
			logfields.NumVSwitches, len(vSwitches),
			logfields.NumSecurityGroups, len(securityGroups),
		)

		m.mutex.Lock()
		defer m.mutex.Unlock()
		m.instances.UpdateInstance(instanceID, instance)
	}

	m.vSwitches = vSwitches
	m.vpcs = vpcs
	m.securityGroups = securityGroups

	return resyncStart
}

// GetVSwitches returns all the tracked vSwitches
// The returned subnetMap is immutable so it can be safely accessed
func (m *InstancesManager) GetVSwitches() ipamTypes.SubnetMap {
	m.mutex.RLock()
	defer m.mutex.RUnlock()

	subnetsCopy := make(ipamTypes.SubnetMap)
	maps.Copy(subnetsCopy, m.vSwitches)

	return subnetsCopy
}

// GetVSwitch return vSwitch by id
func (m *InstancesManager) GetVSwitch(id string) *ipamTypes.Subnet {
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	return m.vSwitches[id]
}

// ForeachInstance will iterate over each instance inside `instances`, and call
// `fn`. This function is read-locked for the entire execution.
func (m *InstancesManager) ForeachInstance(instanceID string, fn ipamTypes.InterfaceIterator) {
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	m.instances.ForeachInterface(instanceID, fn)
}

// UpdateENI updates the ENI definition of an ENI for a particular instance. If
// the ENI is already known, the definition is updated, otherwise the ENI is
// added to the instance.
func (m *InstancesManager) UpdateENI(instanceID string, eni *eniTypes.ENI) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	eniRevision := ipamTypes.InterfaceRevision{Resource: eni}
	m.instances.Update(instanceID, eniRevision)
}

// FindOneVSwitch returns the vSwitch with the most available addresses, matching vpc and az.
// If we have explicit ID or tag constraints, chose a matching vSwitch. ID constraints take
// precedence.
func (m *InstancesManager) FindOneVSwitch(spec eniTypes.Spec, toAllocate int) *ipamTypes.Subnet {
	if len(spec.VSwitches) > 0 {
		return m.FindVSwitchByIDs(spec, toAllocate)
	}
	var bestSubnet *ipamTypes.Subnet
	for _, vSwitch := range m.GetVSwitches() {
		if vSwitch.VirtualNetworkID != spec.VPCID {
			continue
		}
		if vSwitch.AvailabilityZone != spec.AvailabilityZone {
			continue
		}
		if vSwitch.AvailableAddresses < toAllocate {
			continue
		}
		if !vSwitch.Tags.Match(spec.VSwitchTags) {
			continue
		}
		if bestSubnet == nil || bestSubnet.AvailableAddresses < vSwitch.AvailableAddresses {
			bestSubnet = vSwitch
		}
	}
	return bestSubnet
}

// FindVSwitchByIDs returns the vSwitch within a provided list of vSwitch IDs with the most available addresses,
// matching vpc and az.
func (m *InstancesManager) FindVSwitchByIDs(spec eniTypes.Spec, toAllocate int) *ipamTypes.Subnet {
	m.mutex.RLock()
	defer m.mutex.RUnlock()

	var bestSubnet *ipamTypes.Subnet
	for _, vSwitch := range m.vSwitches {
		if vSwitch.VirtualNetworkID != spec.VPCID || vSwitch.AvailabilityZone != spec.AvailabilityZone {
			continue
		}
		if vSwitch.AvailableAddresses < toAllocate {
			continue
		}
		for _, vSwitchID := range spec.VSwitches {
			if vSwitch.ID != vSwitchID {
				continue
			}
			if bestSubnet == nil || bestSubnet.AvailableAddresses < vSwitch.AvailableAddresses {
				bestSubnet = vSwitch
			}
		}
	}
	return bestSubnet
}

// FindSecurityGroupByTags returns the security groups matching VPC ID and all required tags
// The returned security groups slice is immutable so it can be safely accessed
func (m *InstancesManager) FindSecurityGroupByTags(vpcID string, required ipamTypes.Tags) []*types.SecurityGroup {
	m.mutex.RLock()
	defer m.mutex.RUnlock()

	securityGroups := []*types.SecurityGroup{}
	for _, securityGroup := range m.securityGroups {
		if securityGroup.VPCID == vpcID && securityGroup.Tags.Match(required) {
			securityGroups = append(securityGroups, securityGroup)
		}
	}

	return securityGroups
}

// DeleteInstance delete instance from m.instances
func (m *InstancesManager) DeleteInstance(instanceID string) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	m.instances.Delete(instanceID)
}
