package topology

import (
	"fmt"

	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)

// GetEffectiveAvailableCapacity returns the effective available capacity for a disk
// This considers BOTH pending and assigned tasks for capacity reservation.
//
// Formula: BaseAvailable - (VolumeSlots + ShardSlots/ShardsPerVolumeSlot) from all tasks
//
// The calculation includes:
// - Pending tasks: Reserve capacity immediately when added
// - Assigned tasks: Continue to reserve capacity during execution
// - Recently completed tasks are NOT counted against capacity
func (at *ActiveTopology) GetEffectiveAvailableCapacity(nodeID string, diskID uint32) int64 {
	at.mutex.RLock()
	defer at.mutex.RUnlock()

	diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
	disk, exists := at.disks[diskKey]
	if !exists {
		return 0
	}

	if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
		return 0
	}

	// Use the same logic as getEffectiveAvailableCapacityUnsafe but with locking
	capacity := at.getEffectiveAvailableCapacityUnsafe(disk)
	return int64(capacity.VolumeSlots)
}

// GetEffectiveAvailableCapacityDetailed returns detailed available capacity as StorageSlotChange
// This provides granular information about available volume slots and shard slots
func (at *ActiveTopology) GetEffectiveAvailableCapacityDetailed(nodeID string, diskID uint32) StorageSlotChange {
	at.mutex.RLock()
	defer at.mutex.RUnlock()

	diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
	disk, exists := at.disks[diskKey]
	if !exists {
		return StorageSlotChange{}
	}

	if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
		return StorageSlotChange{}
	}

	return at.getEffectiveAvailableCapacityUnsafe(disk)
}

// GetEffectiveAvailableEcShardSlots returns a disk's free EC shard slots,
// accounting for in-flight task reservations at shard granularity. Unlike the
// volume-slot views (GetDisksWithEffectiveCapacity / GetEffectiveAvailableCapacity),
// this does not truncate sub-volume shard reservations: it subtracts the full
// reservation impact (volume slots converted to shard slots, plus the raw shard
// slots) so a reservation that is not a whole multiple of ShardsPerVolumeSlot is
// not lost. It does NOT subtract the EC shards already persisted on the disk;
// callers that track those (from EcShardInfos) subtract them separately.
//
// shardsPerVolume is the number of EC shards of the target collection that fit in
// one volume slot (i.e. its data-shard count): a 4+2 volume's shards are ~1/4 of a
// volume each, so one volume slot holds 4 of them, not the default
// ShardsPerVolumeSlot. Pass <= 0 to use the default. Using the target ratio keeps
// Place from over-filling a disk for low-data-shard layouts.
func (at *ActiveTopology) GetEffectiveAvailableEcShardSlots(nodeID string, diskID uint32, shardsPerVolume int) int {
	if shardsPerVolume <= 0 {
		shardsPerVolume = ShardsPerVolumeSlot
	}

	at.mutex.RLock()
	defer at.mutex.RUnlock()

	diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
	disk, exists := at.disks[diskKey]
	if !exists || disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
		return 0
	}

	info := disk.DiskInfo.DiskInfo
	base := info.MaxVolumeCount - info.VolumeCount
	if base <= 0 && info.MaxVolumeCount == 0 && info.VolumeCount == 0 &&
		len(info.VolumeInfos) == 0 && len(info.EcShardInfos) == 0 {
		// Freshly started empty servers can report max=0 before publishing concrete
		// limits; keep one provisional slot so EC placement still sees the disk,
		// mirroring getEffectiveAvailableCapacityUnsafe.
		base = 1
	}
	if base < 0 {
		base = 0
	}
	// calculateTaskStorageImpact reports consumption as positive, so subtract it.
	// Volume-slot reservations scale by the target ratio; the sub-volume shard-slot
	// remainder is in default units and subtracted as-is (a small approximation).
	impact := at.getEffectiveCapacityUnsafe(disk)
	// impact.ShardSlots is recorded in default ShardsPerVolumeSlot units; convert it
	// to the target ratio's shard slots before subtracting (identity when
	// shardsPerVolume == ShardsPerVolumeSlot). Round a positive reservation up so a
	// sub-slot reservation (e.g. 1 default slot against a 4-shard target) is not
	// truncated to zero and wrongly counted as free.
	scaledShardImpact := int64(impact.ShardSlots) * int64(shardsPerVolume)
	if scaledShardImpact > 0 {
		scaledShardImpact = (scaledShardImpact + int64(ShardsPerVolumeSlot) - 1) / int64(ShardsPerVolumeSlot)
	} else {
		scaledShardImpact /= int64(ShardsPerVolumeSlot)
	}
	free := base*int64(shardsPerVolume) -
		int64(impact.VolumeSlots)*int64(shardsPerVolume) -
		scaledShardImpact
	if free < 0 {
		free = 0
	}
	return int(free)
}

// GetEffectiveCapacityImpact returns the StorageSlotChange impact for a disk
// This shows the net impact from all pending and assigned tasks
func (at *ActiveTopology) GetEffectiveCapacityImpact(nodeID string, diskID uint32) StorageSlotChange {
	at.mutex.RLock()
	defer at.mutex.RUnlock()

	diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
	disk, exists := at.disks[diskKey]
	if !exists {
		return StorageSlotChange{}
	}

	return at.getEffectiveCapacityUnsafe(disk)
}

// GetDisksWithEffectiveCapacity returns disks with sufficient effective capacity
// This method considers BOTH pending and assigned tasks for capacity reservation using StorageSlotChange.
//
// Parameters:
//   - taskType: type of task to check compatibility for
//   - excludeNodeID: node to exclude from results
//   - minCapacity: minimum effective capacity required (in volume slots)
//
// Returns: DiskInfo objects where VolumeCount reflects capacity reserved by all tasks
func (at *ActiveTopology) GetDisksWithEffectiveCapacity(taskType TaskType, excludeNodeID string, minCapacity int64) []*DiskInfo {
	at.mutex.RLock()
	defer at.mutex.RUnlock()

	var available []*DiskInfo

	glog.V(2).Infof("GetDisksWithEffectiveCapacity checking %d disks for type %s, minCapacity %d", len(at.disks), taskType, minCapacity)
	for _, disk := range at.disks {
		if disk.NodeID == excludeNodeID {
			continue // Skip excluded node
		}

		if at.isDiskAvailable(disk, taskType) {
			effectiveCapacity := at.getEffectiveAvailableCapacityUnsafe(disk)

			// Only include disks that meet minimum capacity requirement
			if int64(effectiveCapacity.VolumeSlots) >= minCapacity {
				// Create a new DiskInfo with current capacity information
				diskCopy := DiskInfo{
					NodeID:     disk.DiskInfo.NodeID,
					Address:    disk.DiskInfo.Address,
					DiskID:     disk.DiskInfo.DiskID,
					DiskType:   disk.DiskInfo.DiskType,
					DataCenter: disk.DiskInfo.DataCenter,
					Rack:       disk.DiskInfo.Rack,
					LoadCount:  len(disk.pendingTasks) + len(disk.assignedTasks), // Count all tasks
				}

				// Create a new protobuf DiskInfo to avoid modifying the original
				diskInfoCopy := &master_pb.DiskInfo{
					DiskId:            disk.DiskInfo.DiskInfo.DiskId,
					MaxVolumeCount:    disk.DiskInfo.DiskInfo.MaxVolumeCount,
					VolumeCount:       disk.DiskInfo.DiskInfo.MaxVolumeCount - int64(effectiveCapacity.VolumeSlots),
					VolumeInfos:       disk.DiskInfo.DiskInfo.VolumeInfos,
					EcShardInfos:      disk.DiskInfo.DiskInfo.EcShardInfos,
					RemoteVolumeCount: disk.DiskInfo.DiskInfo.RemoteVolumeCount,
					ActiveVolumeCount: disk.DiskInfo.DiskInfo.ActiveVolumeCount,
					FreeVolumeCount:   disk.DiskInfo.DiskInfo.FreeVolumeCount,
					Tags:              append([]string(nil), disk.DiskInfo.DiskInfo.Tags...),
				}
				diskCopy.DiskInfo = diskInfoCopy
				diskCopy.DiskInfo.MaxVolumeCount = disk.DiskInfo.DiskInfo.MaxVolumeCount // Ensure Max is set

				available = append(available, &diskCopy)
			} else {
				glog.V(2).Infof("Disk %s:%d capacity %d < %d (Max:%d, Vol:%d)", disk.NodeID, disk.DiskInfo.DiskID, effectiveCapacity.VolumeSlots, minCapacity, disk.DiskInfo.DiskInfo.MaxVolumeCount, disk.DiskInfo.DiskInfo.VolumeCount)
			}
		} else {
			tasksInfo := ""
			for _, t := range disk.pendingTasks {
				tasksInfo += fmt.Sprintf("[P:%s,Vol:%d] ", t.TaskType, t.VolumeID)
			}
			for _, t := range disk.assignedTasks {
				tasksInfo += fmt.Sprintf("[A:%s,Vol:%d] ", t.TaskType, t.VolumeID)
			}
			glog.V(2).Infof("Disk %s:%d unavailable. Load: %d, MaxLoad: %d. Tasks: %s", disk.NodeID, disk.DiskInfo.DiskID, len(disk.pendingTasks)+len(disk.assignedTasks), MaxConcurrentTasksPerDisk, tasksInfo)
		}
	}
	glog.V(2).Infof("GetDisksWithEffectiveCapacity found %d available disks", len(available))

	return available
}

// GetDisksForPlanning returns disks considering both active and pending tasks for planning decisions
// This helps avoid over-scheduling tasks to the same disk
func (at *ActiveTopology) GetDisksForPlanning(taskType TaskType, excludeNodeID string, minCapacity int64) []*DiskInfo {
	at.mutex.RLock()
	defer at.mutex.RUnlock()

	var available []*DiskInfo

	for _, disk := range at.disks {
		if disk.NodeID == excludeNodeID {
			continue // Skip excluded node
		}

		// Consider both pending and active tasks for scheduling decisions
		if at.isDiskAvailableForPlanning(disk, taskType) {
			// Check if disk can accommodate new task considering pending tasks
			planningCapacity := at.getPlanningCapacityUnsafe(disk)

			if int64(planningCapacity.VolumeSlots) >= minCapacity {
				// Create a new DiskInfo with planning information
				diskCopy := DiskInfo{
					NodeID:     disk.DiskInfo.NodeID,
					Address:    disk.DiskInfo.Address,
					DiskID:     disk.DiskInfo.DiskID,
					DiskType:   disk.DiskInfo.DiskType,
					DataCenter: disk.DiskInfo.DataCenter,
					Rack:       disk.DiskInfo.Rack,
					LoadCount:  len(disk.pendingTasks) + len(disk.assignedTasks),
				}

				// Create a new protobuf DiskInfo to avoid modifying the original
				diskInfoCopy := &master_pb.DiskInfo{
					DiskId:            disk.DiskInfo.DiskInfo.DiskId,
					MaxVolumeCount:    disk.DiskInfo.DiskInfo.MaxVolumeCount,
					VolumeCount:       disk.DiskInfo.DiskInfo.MaxVolumeCount - int64(planningCapacity.VolumeSlots),
					VolumeInfos:       disk.DiskInfo.DiskInfo.VolumeInfos,
					EcShardInfos:      disk.DiskInfo.DiskInfo.EcShardInfos,
					RemoteVolumeCount: disk.DiskInfo.DiskInfo.RemoteVolumeCount,
					ActiveVolumeCount: disk.DiskInfo.DiskInfo.ActiveVolumeCount,
					FreeVolumeCount:   disk.DiskInfo.DiskInfo.FreeVolumeCount,
					Tags:              append([]string(nil), disk.DiskInfo.DiskInfo.Tags...),
				}
				diskCopy.DiskInfo = diskInfoCopy

				available = append(available, &diskCopy)
			}
		}
	}

	return available
}

// CanAccommodateTask checks if a disk can accommodate a new task considering all constraints
func (at *ActiveTopology) CanAccommodateTask(nodeID string, diskID uint32, taskType TaskType, volumesNeeded int64) bool {
	at.mutex.RLock()
	defer at.mutex.RUnlock()

	diskKey := fmt.Sprintf("%s:%d", nodeID, diskID)
	disk, exists := at.disks[diskKey]
	if !exists {
		return false
	}

	// Check basic availability
	if !at.isDiskAvailable(disk, taskType) {
		return false
	}

	// Check effective capacity
	effectiveCapacity := at.getEffectiveAvailableCapacityUnsafe(disk)
	return int64(effectiveCapacity.VolumeSlots) >= volumesNeeded
}

// getPlanningCapacityUnsafe considers both pending and active tasks for planning
func (at *ActiveTopology) getPlanningCapacityUnsafe(disk *activeDisk) StorageSlotChange {
	if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
		return StorageSlotChange{}
	}

	baseAvailableVolumes := disk.DiskInfo.DiskInfo.MaxVolumeCount - disk.DiskInfo.DiskInfo.VolumeCount

	// Use the centralized helper function to calculate task storage impact
	totalImpact := at.calculateTaskStorageImpact(disk)

	// Calculate available capacity considering impact (negative impact reduces availability)
	availableVolumeSlots := baseAvailableVolumes - totalImpact.ToVolumeSlots()
	if availableVolumeSlots < 0 {
		availableVolumeSlots = 0
	}

	// Return detailed capacity information
	return StorageSlotChange{
		VolumeSlots: int32(availableVolumeSlots),
		ShardSlots:  -totalImpact.ShardSlots, // Available shard capacity (negative impact becomes positive availability)
	}
}

// isDiskAvailableForPlanning checks if disk can accept new tasks considering
// pending load. See isDiskAvailable for the cross-type policy.
func (at *ActiveTopology) isDiskAvailableForPlanning(disk *activeDisk, taskType TaskType) bool {
	totalLoad := len(disk.pendingTasks) + len(disk.assignedTasks)
	if MaxTotalTaskLoadPerDisk > 0 && totalLoad >= MaxTotalTaskLoadPerDisk {
		return false
	}
	return true
}

// calculateTaskStorageImpact is a helper function that calculates the total storage impact
// from all tasks (pending and assigned) on a given disk. This eliminates code duplication
// between multiple capacity calculation functions.
func (at *ActiveTopology) calculateTaskStorageImpact(disk *activeDisk) StorageSlotChange {
	if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
		return StorageSlotChange{}
	}

	totalImpact := StorageSlotChange{}

	// Process both pending and assigned tasks with identical logic
	taskLists := [][]*taskState{disk.pendingTasks, disk.assignedTasks}

	for _, taskList := range taskLists {
		for _, task := range taskList {
			// Calculate impact for all source locations
			for _, source := range task.Sources {
				if source.SourceServer == disk.NodeID && source.SourceDisk == disk.DiskID {
					totalImpact.AddInPlace(source.StorageChange)
				}
			}

			// Calculate impact for all destination locations
			for _, dest := range task.Destinations {
				if dest.TargetServer == disk.NodeID && dest.TargetDisk == disk.DiskID {
					totalImpact.AddInPlace(dest.StorageChange)
				}
			}
		}
	}

	return totalImpact
}

// getEffectiveCapacityUnsafe returns effective capacity impact without locking (for internal use)
// Returns StorageSlotChange representing the net impact from all tasks
func (at *ActiveTopology) getEffectiveCapacityUnsafe(disk *activeDisk) StorageSlotChange {
	return at.calculateTaskStorageImpact(disk)
}

// getEffectiveAvailableCapacityUnsafe returns detailed available capacity as StorageSlotChange
func (at *ActiveTopology) getEffectiveAvailableCapacityUnsafe(disk *activeDisk) StorageSlotChange {
	if disk.DiskInfo == nil || disk.DiskInfo.DiskInfo == nil {
		return StorageSlotChange{}
	}

	baseAvailable := disk.DiskInfo.DiskInfo.MaxVolumeCount - disk.DiskInfo.DiskInfo.VolumeCount
	if baseAvailable <= 0 &&
		disk.DiskInfo.DiskInfo.MaxVolumeCount == 0 &&
		disk.DiskInfo.DiskInfo.VolumeCount == 0 &&
		len(disk.DiskInfo.DiskInfo.VolumeInfos) == 0 &&
		len(disk.DiskInfo.DiskInfo.EcShardInfos) == 0 {
		// Some empty volume servers can report max_volume_counts=0 before
		// publishing concrete slot limits. Keep one provisional slot so EC
		// detection still sees the disk for placement planning.
		baseAvailable = 1
	}
	netImpact := at.getEffectiveCapacityUnsafe(disk)

	// Calculate available volume slots (negative impact reduces availability)
	availableVolumeSlots := baseAvailable - netImpact.ToVolumeSlots()
	if availableVolumeSlots < 0 {
		availableVolumeSlots = 0
	}

	// Return detailed capacity information
	return StorageSlotChange{
		VolumeSlots: int32(availableVolumeSlots),
		ShardSlots:  -netImpact.ShardSlots, // Available shard capacity (negative impact becomes positive availability)
	}
}
