package rules

import (
	"sort"
	"time"

	"github.com/nexuscrm/sentry/internal/types"
)

// Thresholds for app.* rules. See spec §3.4.2 (tuned-loose values).
const (
	GoroutineLeakDelta = 1000
	HeapGrowthPct      = 1.00 // +100% over 2h
	DBPoolWaitDelta    = 500
	PanicSurgeDelta    = 20
	HTTP5xxWarnFrac    = 0.03
	HTTP5xxCritFrac    = 0.10
	OutboxStallCycles  = 3
)

// AllAppRules returns all app-health rules in deterministic (ID-sorted) order.
func AllAppRules() []Rule {
	rules := []Rule{
		goroutineLeakRule{},
		heapGrowthRule{},
		dbPoolWaitRule{},
		panicSurgeRule{},
		http5xxWarnRule{},
		http5xxCritRule{},
		outboxStallRule{},
	}
	sort.Slice(rules, func(i, j int) bool { return rules[i].ID() < rules[j].ID() })
	return rules
}

// --- goroutine_leak -------------------------------------------------------

type goroutineLeakRule struct{}

func (goroutineLeakRule) ID() string { return "app.goroutine_leak" }

func (r goroutineLeakRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	target := current.SnapshotTs.Add(-1 * time.Hour)
	base, ok := history.FindClosest(target, current.EpochID)
	if !ok {
		return nil
	}
	cur := current.App.Runtime.Goroutines
	hist := base.App.Runtime.Goroutines
	delta := cur - hist
	if delta <= GoroutineLeakDelta {
		return nil
	}
	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "WARN",
		Breached: true,
		Context: map[string]any{
			"current":     cur,
			"baseline_1h": hist,
			"delta":       delta,
			"threshold":   GoroutineLeakDelta,
		},
	}}
}

// --- heap_growth ----------------------------------------------------------

type heapGrowthRule struct{}

func (heapGrowthRule) ID() string { return "app.heap_growth" }

func (r heapGrowthRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	target := current.SnapshotTs.Add(-2 * time.Hour)
	base, ok := history.FindClosest(target, current.EpochID)
	if !ok {
		return nil
	}
	cur := current.App.Runtime.HeapInuseMB
	hist := base.App.Runtime.HeapInuseMB
	if hist == 0 {
		// Avoid divide-by-zero and treat uninitialized baseline as skip.
		return nil
	}
	pct := float64(int64(cur)-int64(hist)) / float64(hist)
	if pct <= HeapGrowthPct {
		return nil
	}
	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "WARN",
		Breached: true,
		Context: map[string]any{
			"current_mb":     cur,
			"baseline_2h_mb": hist,
			"delta_pct":      pct,
			"threshold_pct":  HeapGrowthPct,
		},
	}}
}

// --- db_pool_wait ---------------------------------------------------------

type dbPoolWaitRule struct{}

func (dbPoolWaitRule) ID() string { return "app.db_pool_wait" }

func (r dbPoolWaitRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	target := current.SnapshotTs.Add(-10 * time.Minute)
	base, ok := history.FindClosest(target, current.EpochID)
	if !ok {
		return nil
	}
	cur := current.App.DBPool.WaitCount
	hist := base.App.DBPool.WaitCount
	if cur < hist {
		// Counter reset (CRM process restart) — use fresh baseline, skip.
		return nil
	}
	delta := cur - hist
	if delta <= DBPoolWaitDelta {
		return nil
	}
	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "CRIT",
		Breached: true,
		Context: map[string]any{
			"current":      cur,
			"baseline_10m": hist,
			"delta":        delta,
			"threshold":    DBPoolWaitDelta,
		},
	}}
}

// --- panic_surge ----------------------------------------------------------

type panicSurgeRule struct{}

func (panicSurgeRule) ID() string { return "app.panic_surge" }

func (r panicSurgeRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	target := current.SnapshotTs.Add(-10 * time.Minute)
	base, ok := history.FindClosest(target, current.EpochID)
	if !ok {
		return nil
	}
	cur := current.App.HTTP.PanicCount
	hist := base.App.HTTP.PanicCount
	if cur < hist {
		return nil
	}
	delta := cur - hist
	if delta <= PanicSurgeDelta {
		return nil
	}
	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "CRIT",
		Breached: true,
		Context: map[string]any{
			"current":      cur,
			"baseline_10m": hist,
			"delta":        delta,
			"threshold":    PanicSurgeDelta,
		},
	}}
}

// --- http 5xx fraction ----------------------------------------------------

// http5xxFraction returns the 5xx-of-requests fraction over the last 10m window.
// Returns (fraction, reqDelta, errDelta, true) on success; ok=false when the
// computation should be skipped (cold start, counter reset, zero requests).
func http5xxFraction(current types.Snapshot, history HistoryFinder) (float64, int64, int64, bool) {
	target := current.SnapshotTs.Add(-10 * time.Minute)
	base, ok := history.FindClosest(target, current.EpochID)
	if !ok {
		return 0, 0, 0, false
	}
	curReq := current.App.HTTP.RequestsTotal
	histReq := base.App.HTTP.RequestsTotal
	curErr := current.App.HTTP.Error5xxCount
	histErr := base.App.HTTP.Error5xxCount
	// Counter reset on either monotonic counter — skip.
	if curReq < histReq || curErr < histErr {
		return 0, 0, 0, false
	}
	reqDelta := curReq - histReq
	errDelta := curErr - histErr
	if reqDelta == 0 {
		// No new requests — nothing to compute.
		return 0, 0, 0, false
	}
	frac := float64(errDelta) / float64(reqDelta)
	if frac > 1.0 {
		// Impossible under monotonic counters; defensive skip.
		return 0, 0, 0, false
	}
	return frac, reqDelta, errDelta, true
}

func http5xxContext(frac float64, reqDelta, errDelta int64, threshold float64) map[string]any {
	return map[string]any{
		"fraction":  frac,
		"req_delta": reqDelta,
		"err_delta": errDelta,
		"threshold": threshold,
	}
}

type http5xxWarnRule struct{}

func (http5xxWarnRule) ID() string { return "app.http_5xx_warn" }

func (r http5xxWarnRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	frac, reqDelta, errDelta, ok := http5xxFraction(current, history)
	if !ok || frac <= HTTP5xxWarnFrac {
		return nil
	}
	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "WARN",
		Breached: true,
		Context:  http5xxContext(frac, reqDelta, errDelta, HTTP5xxWarnFrac),
	}}
}

type http5xxCritRule struct{}

func (http5xxCritRule) ID() string { return "app.http_5xx_crit" }

func (r http5xxCritRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	frac, reqDelta, errDelta, ok := http5xxFraction(current, history)
	if !ok || frac <= HTTP5xxCritFrac {
		return nil
	}
	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "CRIT",
		Breached: true,
		Context:  http5xxContext(frac, reqDelta, errDelta, HTTP5xxCritFrac),
	}}
}

// --- outbox_stall ---------------------------------------------------------

type outboxStallRule struct{}

func (outboxStallRule) ID() string { return "app.outbox_stall" }

// Evaluate fires when Outbox.Pending > 0 across the current snapshot plus
// 3 prior same-epoch snapshots at ~10m / ~20m / ~30m ago, and the pending
// sequence is non-decreasing (any dip means drain progress happened and
// resets the stall).
func (r outboxStallRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	// Collect three distinct history snapshots via FindClosest at each target.
	offsets := []time.Duration{
		-30 * time.Minute,
		-20 * time.Minute,
		-10 * time.Minute,
	}
	seen := make(map[time.Time]bool)
	hist := make([]types.Snapshot, 0, OutboxStallCycles)
	for _, off := range offsets {
		target := current.SnapshotTs.Add(off)
		s, ok := history.FindClosest(target, current.EpochID)
		if !ok {
			return nil
		}
		if seen[s.SnapshotTs] {
			// Same snapshot returned for multiple buckets → insufficient distinct history.
			return nil
		}
		seen[s.SnapshotTs] = true
		hist = append(hist, s)
	}

	// Build pending sequence oldest → newest, ending with current.
	seq := []int64{
		hist[0].App.Outbox.Pending,
		hist[1].App.Outbox.Pending,
		hist[2].App.Outbox.Pending,
		current.App.Outbox.Pending,
	}

	// Guard: all must be > 0 (drained at any point resets the stall).
	for _, p := range seq {
		if p <= 0 {
			return nil
		}
	}

	// Guard: non-decreasing (a dip means progress happened).
	for i := 1; i < len(seq); i++ {
		if seq[i] < seq[i-1] {
			return nil
		}
	}

	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "CRIT",
		Breached: true,
		Context: map[string]any{
			"pending_seq": seq,
			"cycles":      OutboxStallCycles,
		},
	}}
}
