package rules

import (
	"sort"
	"time"

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

// Thresholds for data.* rules. See spec §3.4.1.
const (
	RowSpikeWarnPct       = 0.50
	RowSpikeCritPct       = 1.00
	RowSpikeWarnAbsMin    = 5000
	RowSpikeCritAbs       = 200000
	RowDropWarnPct        = -0.20
	RowDropCritPct        = -0.50
	OutboxBacklogWarn     = 50000
	OutboxBacklogCrit     = 200000
	OutboxFailedSurgeWarn = 500
)

// AllDataRules returns all data-volume rules in deterministic (ID-sorted) order.
//
// NOTE: data.write_stall was removed intentionally. Production traffic is
// bursty (off-hours gaps are normal) and the rule produced too many false
// positives against tables like _System_OutboxEvent and _System_AuditLog
// during quiet windows. Row-count spike/drop + outbox backlog + outbox
// failed surge cover the real failure modes without the noise.
func AllDataRules() []Rule {
	rules := []Rule{
		rowSpikeWarnRule{},
		rowSpikeCritRule{},
		rowDropWarnRule{},
		rowDropCritRule{},
		outboxBacklogWarnRule{},
		outboxBacklogCritRule{},
		outboxFailedSurgeRule{},
	}
	sort.Slice(rules, func(i, j int) bool { return rules[i].ID() < rules[j].ID() })
	return rules
}

// --- helpers ---------------------------------------------------------------

// findTable returns the first TableRow in tables whose Name matches.
func findTable(tables []types.TableRow, name string) (types.TableRow, bool) {
	for _, t := range tables {
		if t.Name == name {
			return t, true
		}
	}
	return types.TableRow{}, false
}

// tolerance returns the multiplier applied to percentage thresholds when
// the underlying source is an estimate (doubled to absorb TiDB stats noise).
func tolerance(src types.Source) float64 {
	if src == types.SourceInfoSchemaEst {
		return 2.0
	}
	return 1.0
}

// pctDelta returns (current - baseline) / baseline; 0 if baseline == 0.
func pctDelta(current, baseline int64) float64 {
	if baseline == 0 {
		return 0
	}
	return float64(current-baseline) / float64(baseline)
}

// rowDeltaContext returns the standard context for row-delta rules.
func rowDeltaContext(name string, current, baseline int64, src types.Source) map[string]any {
	return map[string]any{
		"table":       name,
		"current":     current,
		"baseline_1h": baseline,
		"delta":       current - baseline,
		"delta_pct":   pctDelta(current, baseline),
		"source":      string(src),
	}
}

// eachTableWithBaseline iterates current tables and invokes fn with the matching
// baseline row from the 1h-ago snapshot. If no historical snapshot is available,
// fn is not called (cold-start skip).
func eachTableWithBaseline(
	current types.Snapshot,
	history HistoryFinder,
	fn func(cur, base types.TableRow),
) {
	target := current.SnapshotTs.Add(-1 * time.Hour)
	baseSnap, ok := history.FindClosest(target, current.EpochID)
	if !ok {
		return
	}
	for _, cur := range current.Tables {
		base, ok := findTable(baseSnap.Tables, cur.Name)
		if !ok {
			continue
		}
		fn(cur, base)
	}
}

// --- row spike WARN -------------------------------------------------------

type rowSpikeWarnRule struct{}

func (rowSpikeWarnRule) ID() string { return "data.row_spike_warn" }

func (r rowSpikeWarnRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	var out []types.RuleResult
	eachTableWithBaseline(current, history, func(cur, base types.TableRow) {
		delta := cur.RowCount - base.RowCount
		if delta < RowSpikeWarnAbsMin {
			return
		}
		pct := pctDelta(cur.RowCount, base.RowCount)
		threshold := RowSpikeWarnPct * tolerance(cur.Source)
		if pct > threshold {
			out = append(out, types.RuleResult{
				RuleID:   r.ID(),
				Entity:   cur.Name,
				Level:    "WARN",
				Breached: true,
				Context:  rowDeltaContext(cur.Name, cur.RowCount, base.RowCount, cur.Source),
			})
		}
	})
	return out
}

// --- row spike CRIT -------------------------------------------------------

type rowSpikeCritRule struct{}

func (rowSpikeCritRule) ID() string { return "data.row_spike_crit" }

func (r rowSpikeCritRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	var out []types.RuleResult
	eachTableWithBaseline(current, history, func(cur, base types.TableRow) {
		delta := cur.RowCount - base.RowCount
		pct := pctDelta(cur.RowCount, base.RowCount)
		threshold := RowSpikeCritPct * tolerance(cur.Source)
		if pct > threshold || delta > RowSpikeCritAbs {
			out = append(out, types.RuleResult{
				RuleID:   r.ID(),
				Entity:   cur.Name,
				Level:    "CRIT",
				Breached: true,
				Context:  rowDeltaContext(cur.Name, cur.RowCount, base.RowCount, cur.Source),
			})
		}
	})
	return out
}

// --- row drop WARN --------------------------------------------------------

type rowDropWarnRule struct{}

func (rowDropWarnRule) ID() string { return "data.row_drop_warn" }

func (r rowDropWarnRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	var out []types.RuleResult
	eachTableWithBaseline(current, history, func(cur, base types.TableRow) {
		pct := pctDelta(cur.RowCount, base.RowCount)
		threshold := RowDropWarnPct * tolerance(cur.Source)
		if pct < threshold {
			out = append(out, types.RuleResult{
				RuleID:   r.ID(),
				Entity:   cur.Name,
				Level:    "WARN",
				Breached: true,
				Context:  rowDeltaContext(cur.Name, cur.RowCount, base.RowCount, cur.Source),
			})
		}
	})
	return out
}

// --- row drop CRIT --------------------------------------------------------

type rowDropCritRule struct{}

func (rowDropCritRule) ID() string { return "data.row_drop_crit" }

func (r rowDropCritRule) Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult {
	var out []types.RuleResult
	eachTableWithBaseline(current, history, func(cur, base types.TableRow) {
		pct := pctDelta(cur.RowCount, base.RowCount)
		threshold := RowDropCritPct * tolerance(cur.Source)
		if pct < threshold {
			out = append(out, types.RuleResult{
				RuleID:   r.ID(),
				Entity:   cur.Name,
				Level:    "CRIT",
				Breached: true,
				Context:  rowDeltaContext(cur.Name, cur.RowCount, base.RowCount, cur.Source),
			})
		}
	})
	return out
}

// --- outbox backlog WARN --------------------------------------------------

type outboxBacklogWarnRule struct{}

func (outboxBacklogWarnRule) ID() string { return "data.outbox_backlog_warn" }

func (r outboxBacklogWarnRule) Evaluate(current types.Snapshot, _ HistoryFinder) []types.RuleResult {
	pending := current.App.Outbox.Pending
	if pending <= OutboxBacklogWarn {
		return nil
	}
	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "WARN",
		Breached: true,
		Context: map[string]any{
			"pending":   pending,
			"threshold": OutboxBacklogWarn,
		},
	}}
}

// --- outbox backlog CRIT --------------------------------------------------

type outboxBacklogCritRule struct{}

func (outboxBacklogCritRule) ID() string { return "data.outbox_backlog_crit" }

func (r outboxBacklogCritRule) Evaluate(current types.Snapshot, _ HistoryFinder) []types.RuleResult {
	pending := current.App.Outbox.Pending
	if pending <= OutboxBacklogCrit {
		return nil
	}
	return []types.RuleResult{{
		RuleID:   r.ID(),
		Entity:   "",
		Level:    "CRIT",
		Breached: true,
		Context: map[string]any{
			"pending":   pending,
			"threshold": OutboxBacklogCrit,
		},
	}}
}

// --- outbox failed surge --------------------------------------------------

type outboxFailedSurgeRule struct{}

func (outboxFailedSurgeRule) ID() string { return "data.outbox_failed_surge" }

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