package shadowpool

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
)

const (
	// zeroRentRecordIPTopN is the number of top source IPs to report in the IP distribution metric.
	zeroRentRecordIPTopN = 20
)

// zeroRentRecordIPMetricsInterval controls how often we refresh the zero-rent IP distribution metrics.
// This avoids running an expensive aggregation query on every Inventory tick.
const zeroRentRecordIPMetricsInterval = 10 * time.Minute

var (
	zeroRentRecordIPMetricsMu   sync.Mutex
	lastZeroRentRecordMetricsAt time.Time
)

// shouldUpdateZeroRentRecordIPMetrics determines whether we should refresh the zero-rent
// IP distribution metrics at the given time. It is safe for concurrent use.
func shouldUpdateZeroRentRecordIPMetrics(now time.Time) bool {
	zeroRentRecordIPMetricsMu.Lock()
	defer zeroRentRecordIPMetricsMu.Unlock()

	if lastZeroRentRecordMetricsAt.IsZero() || now.Sub(lastZeroRentRecordMetricsAt) >= zeroRentRecordIPMetricsInterval {
		lastZeroRentRecordMetricsAt = now
		return true
	}
	return false
}

// Inventory shadow pool clusters, then update metrics accordingly.
func (w *worker) Inventory(ctx context.Context) error {
	tilog.Info(ctx, "Starting Inventory operation for shadow pools")

	// List all shadow pools (both enabled and disabled for complete inventory)
	pools, err := w.poolRepo.List(ctx, &repository.ShadowPoolFilter{
		OnlyEnabled: false, // Get all pools for inventory
	})
	if err != nil {
		tilog.Error(ctx, "Failed to list shadow pools", zap.Error(err))
		return fmt.Errorf("failed to list shadow pools: %w", err)
	}

	tilog.Info(ctx, "Found shadow pools for inventory", zap.Int("count", len(pools)))

	// Build a pool-id → pool map for label lookup and update metadata metrics.
	poolMap := make(map[uint64]*entity.ShadowPool, len(pools))
	poolIDs := make([]uint64, 0, len(pools))
	for _, pool := range pools {
		poolMap[pool.ID] = pool
		poolIDs = append(poolIDs, pool.ID)

		metadataClusterCount.WithLabelValues(
			fmt.Sprintf("%d", pool.ID),
			pool.Name,
			"min_capacity",
		).Set(float64(pool.MinCapacity))

		metadataClusterCount.WithLabelValues(
			fmt.Sprintf("%d", pool.ID),
			pool.Name,
			"max_capacity",
		).Set(float64(pool.MaxCapacity))
	}

	// Single GROUP BY query replaces per-pool × per-state × per-deletion-status loops.
	if len(poolIDs) > 0 {
		counts, err := w.clusterRepo.CountShadowPoolClustersGrouped(ctx, poolIDs)
		if err != nil {
			tilog.Error(ctx, "Failed to count shadow pool clusters grouped", zap.Error(err))
		} else {
			// Reset the gauge vec so stale (pool_id, state, is_deleted) combinations are removed.
			clusterStatusCount.Reset()
			for _, c := range counts {
				pool, ok := poolMap[c.PoolID]
				if !ok {
					continue
				}
				isDeletedLabel := "false"
				if c.IsDeleted {
					isDeletedLabel = "true"
				}
				clusterStatusCount.WithLabelValues(
					fmt.Sprintf("%d", pool.ID),
					pool.Name,
					shadowStateToString(c.ShadowState),
					isDeletedLabel,
				).Set(float64(c.Count))
			}
		}
	} else {
		// No pools found; clear any previous cluster status metrics to avoid stale values.
		clusterStatusCount.Reset()
	}

	// Update zero_rent_records IP distribution metrics.
	// This aggregation can be expensive; run it only at a controlled interval.
	if shouldUpdateZeroRentRecordIPMetrics(time.Now()) {
		ipCounts, err := w.zeroRentRecordRepo.CountBySourceIP(ctx, zeroRentRecordIPTopN)
		if err != nil {
			tilog.Error(ctx, "Failed to count zero rent records by source IP", zap.Error(err))
		} else {
			// Reset the gauge vec so stale IPs (that dropped out of the top-N) are removed.
			zeroRentRecordIPCount.Reset()
			for _, ic := range ipCounts {
				zeroRentRecordIPCount.WithLabelValues(ic.SourceIP).Set(float64(ic.Count))
			}
		}
	}

	tilog.Info(ctx, "Completed Inventory operation for shadow pools")
	return nil
}

// shadowStateToString converts ShadowState enum to string for metrics labels
func shadowStateToString(state entity.ShadowState) string {
	switch state {
	case entity.ShadowStateNone:
		return "none"
	case entity.ShadowStatePreparing:
		return "preparing"
	case entity.ShadowStatePrepared:
		return "prepared"
	case entity.ShadowStateRenting:
		return "renting"
	case entity.ShadowStateRented:
		return "rented"
	case entity.ShadowStateFreeing:
		return "freeing"
	case entity.ShadowStateFreed:
		return "freed"
	case entity.ShadowStateClaiming:
		return "claiming"
	case entity.ShadowStateClaimed:
		return "claimed"
	case entity.ShadowStateTakingOver:
		return "taking_over"
	case entity.ShadowStateTakenOver:
		return "taken_over"
	default:
		return "unknown"
	}
}
