package connector_inbound

import (
	"database/sql"
	"time"

	connector_inbound_routing "github.com/pingcap-inc/nexuscrm-connectors/features/connector/inboundrouting"
)

const (
	adaptiveBudgetHighProcessingRatio = 0.75
	adaptiveBudgetHighDBInUseRatio    = 0.75
	adaptiveBudgetSlowIngressLatency  = 250 * time.Millisecond
)

type AdaptiveBudgetAllocator interface {
	Allocate(AdaptiveBudgetInput) AdaptiveBudgetDecision
}

type AdaptiveBudgetInput struct {
	Requested           int
	ConfiguredMaxRows   int
	ActiveProcessing    int
	StaleProcessing     int
	Reserved            int
	AvailableBeforeTrim int
	DBPool              sql.DBStats
	QueueClasses        []string
	AdmissionLatency    time.Duration
	EnqueueLatency      time.Duration
}

type AdaptiveBudgetDecision struct {
	Reason              string
	Signals             []string
	Requested           int
	ConfiguredMaxRows   int
	ActiveProcessing    int
	StaleProcessing     int
	Reserved            int
	AvailableBeforeTrim int
	MaxClaims           int
	QueueClasses        []string
}

type DefaultAdaptiveBudgetAllocator struct{}

func (DefaultAdaptiveBudgetAllocator) Allocate(input AdaptiveBudgetInput) AdaptiveBudgetDecision {
	decision := AdaptiveBudgetDecision{
		Reason:              "configured_capacity",
		Requested:           input.Requested,
		ConfiguredMaxRows:   input.ConfiguredMaxRows,
		ActiveProcessing:    input.ActiveProcessing,
		StaleProcessing:     input.StaleProcessing,
		Reserved:            input.Reserved,
		AvailableBeforeTrim: input.AvailableBeforeTrim,
		QueueClasses:        append([]string(nil), input.QueueClasses...),
	}
	if input.ConfiguredMaxRows <= 0 {
		decision.Reason = "disabled"
		decision.MaxClaims = input.Requested
		return decision
	}
	maxClaims := input.Requested
	if maxClaims <= 0 || maxClaims > input.ConfiguredMaxRows {
		maxClaims = input.ConfiguredMaxRows
	}
	if input.AvailableBeforeTrim < maxClaims {
		maxClaims = input.AvailableBeforeTrim
	}
	if maxClaims <= 0 {
		decision.Reason = "processing_capacity_full"
		decision.MaxClaims = 0
		return decision
	}

	downshift := false
	if input.ConfiguredMaxRows >= defaultClaimLimiterMaxProcessingRows &&
		input.ActiveProcessing > 0 &&
		float64(input.ActiveProcessing)/float64(input.ConfiguredMaxRows) >= adaptiveBudgetHighProcessingRatio {
		maxClaims = minPositive(maxClaims, ceilDiv(maxClaims, 2))
		decision.Signals = append(decision.Signals, "processing_high")
		downshift = true
	}
	if input.DBPool.MaxOpenConnections > 0 &&
		input.DBPool.Idle == 0 &&
		float64(input.DBPool.InUse)/float64(input.DBPool.MaxOpenConnections) >= adaptiveBudgetHighDBInUseRatio {
		maxClaims = minPositive(maxClaims, ceilDiv(maxClaims, 2))
		decision.Signals = append(decision.Signals, "db_pool_high")
		downshift = true
	}
	if input.AdmissionLatency >= adaptiveBudgetSlowIngressLatency {
		maxClaims = minPositive(maxClaims, ceilDiv(maxClaims, 2))
		decision.Signals = append(decision.Signals, "admission_latency_high")
		downshift = true
	}
	if input.EnqueueLatency >= adaptiveBudgetSlowIngressLatency {
		maxClaims = minPositive(maxClaims, ceilDiv(maxClaims, 2))
		decision.Signals = append(decision.Signals, "enqueue_latency_high")
		downshift = true
	}
	if input.Reserved > 0 && input.AvailableBeforeTrim < input.Requested {
		decision.Signals = append(decision.Signals, "queue_reservation_pressure")
	}
	if containsOnlyBulkQueue(input.QueueClasses) && downshift && maxClaims > 1 {
		maxClaims = minPositive(maxClaims, ceilDiv(maxClaims, 2))
		decision.Signals = append(decision.Signals, "bulk_queue_pressure")
	}
	if downshift {
		decision.Reason = "adaptive_downshift"
	}
	decision.MaxClaims = maxClaims
	return decision
}

func minPositive(a, b int) int {
	if a <= 0 {
		return b
	}
	if b <= 0 {
		return a
	}
	if b < a {
		return b
	}
	return a
}

func ceilDiv(value, divisor int) int {
	if divisor <= 0 {
		return value
	}
	if value <= 0 {
		return 0
	}
	return (value + divisor - 1) / divisor
}

func containsOnlyBulkQueue(queueClasses []string) bool {
	if len(queueClasses) == 0 {
		return false
	}
	for _, raw := range queueClasses {
		queueClass, err := connector_inbound_routing.NormalizeQueueClass(raw)
		if err != nil || queueClass != connector_inbound_routing.QueueClassBulk {
			return false
		}
	}
	return true
}
