package detect

// DetectConfig holds all tunable thresholds for the detection engine.
// All fields are filled with production defaults by DefaultConfig().
// 15F (hot-reload) may overwrite fields at runtime; the Engine reads via a
// pointer so updates take effect on the next Tick without a restart.
type DetectConfig struct {
	// DomesticFailNumerator and DomesticFailDenominator define the ISP-failure
	// fraction that triggers the suspect rule.
	// Default: 2/3 (two out of every three domestic ISPs must fail).
	DomesticFailNumerator   int // 2
	DomesticFailDenominator int // 3

	// SuspectStreakMin is the number of consecutive failing cycles required to
	// transition a node from "up" to "blocked_suspect".
	// Default: 2 cycles (10 min at the 5-min tick period).
	SuspectStreakMin int

	// TrafficDropThreshold is the minimum percentage drop in online-connection
	// count over the 15-minute window that activates the traffic-warning rule.
	// Default: 80.0 %.
	TrafficDropThreshold float64

	// TrafficBaselineMin is the minimum current online-connection count for
	// the traffic-warning relaxation to apply.  Below this threshold the node
	// may simply be idle rather than experiencing user flight.
	// Default: 20 connections.
	TrafficBaselineMin int

	// ConfirmedStreakMin is the number of consecutive cycles that a node must
	// spend in "blocked_suspect" before being promoted to "blocked_confirmed".
	// Default: 6 cycles (30 min).
	ConfirmedStreakMin int

	// RecoverStreakMin is the number of consecutive passing cycles while in
	// "blocked_suspect" required to recover the node back to "up".
	// Default: 2 cycles (10 min).
	RecoverStreakMin int

	// SuspectWeight is the routing weight applied when a node first enters
	// "blocked_suspect", reducing traffic directed to it.
	// Default: 10.
	SuspectWeight int
}

// DefaultConfig returns a DetectConfig pre-filled with production defaults.
func DefaultConfig() DetectConfig {
	return DetectConfig{
		DomesticFailNumerator:   2,
		DomesticFailDenominator: 3,
		SuspectStreakMin:        2,
		TrafficDropThreshold:    80.0,
		TrafficBaselineMin:      20,
		ConfirmedStreakMin:       6,
		RecoverStreakMin:        2,
		SuspectWeight:           10,
	}
}

// isSuspectTriggered reports whether the domestic ISP-failure rate meets or
// exceeds the configured fraction (default ≥ 2/3).
//
// Integer arithmetic avoids floating-point rounding:
//
//	fail/total >= num/den  ↔  fail × den >= total × num
func (cfg *DetectConfig) isSuspectTriggered(sig NodeSignal) bool {
	if sig.DomesticTotalISPs == 0 {
		return false // no domestic probe data this cycle; cannot judge
	}
	return sig.DomesticFailISPs*cfg.DomesticFailDenominator >=
		sig.DomesticTotalISPs*cfg.DomesticFailNumerator
}

// isFault reports whether the node appears to have a local outage rather than
// a domestic censorship event.
//
// Condition: domestic probes failing AND overseas probes also failing.
// When both sides are down the most likely cause is a node-level failure
// (hardware, network, crashed process) rather than GFW interference.
//
// The fault rule takes precedence over all other rules and must be evaluated
// first in the processing loop.
func isFault(sig NodeSignal) bool {
	return sig.DomesticFailISPs > 0 && sig.OverseasHasData && !sig.OverseasOK
}

// effectiveSuspectStreakMin returns the cycle threshold required to enter
// "blocked_suspect", relaxed to 1 when the traffic-warning rule is active.
func (cfg *DetectConfig) effectiveSuspectStreakMin(sig NodeSignal) int {
	if cfg.isTrafficWarning(sig) {
		return 1
	}
	return cfg.SuspectStreakMin
}

// isTrafficWarning reports whether the 15-minute traffic-drop signal exceeds
// the threshold and the baseline is large enough to be meaningful.
func (cfg *DetectConfig) isTrafficWarning(sig NodeSignal) bool {
	return sig.TrafficDropPct >= cfg.TrafficDropThreshold &&
		sig.TrafficBaseline >= cfg.TrafficBaselineMin
}
