package rules

import (
	"strings"
	"sync"
	"time"

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

// EpochWindow reports whether a given instant falls inside a post-reset
// suppression window. sentry/internal/epoch.Tracker satisfies it.
type EpochWindow interface {
	InSuppressionWindow(at time.Time) bool
}

// firedEntry records the last time a given {RuleID, Entity} pair fired, and
// at what level, for dedup/escalation logic.
type firedEntry struct {
	level string
	at    time.Time
}

// Suppressor filters rule results according to dedup, cold-start, and epoch
// suppression rules. Call Filter exactly once per cycle, in order.
type Suppressor struct {
	coldStartCycles int
	cyclesSeen      int
	window          EpochWindow
	dedupTTL        time.Duration
	lastFired       map[string]firedEntry // key = ruleID + "|" + entity
	mu              sync.Mutex
}

// NewSuppressor constructs a Suppressor. coldStartCycles is typically 2,
// dedupTTL 60*time.Minute.
func NewSuppressor(window EpochWindow, coldStartCycles int, dedupTTL time.Duration) *Suppressor {
	return &Suppressor{
		coldStartCycles: coldStartCycles,
		window:          window,
		dedupTTL:        dedupTTL,
		lastFired:       make(map[string]firedEntry),
	}
}

// isDBBackedRule reports whether the rule should be dropped while the epoch
// suppression window is active. data.* rules are DB-backed by definition;
// app.outbox_stall is also DB-backed and must be treated as a data rule here.
func isDBBackedRule(ruleID string) bool {
	if strings.HasPrefix(ruleID, "data.") {
		return true
	}
	if ruleID == "app.outbox_stall" {
		return true
	}
	return false
}

// Filter accepts a slice of results just produced for the given snapshot and
// returns the subset that should actually be alerted. It also records fired
// alerts so subsequent calls can dedup them. Must be called exactly once per
// cycle, in order.
func (s *Suppressor) Filter(snapshotTs time.Time, results []types.RuleResult) []types.RuleResult {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.cyclesSeen++
	if s.cyclesSeen <= s.coldStartCycles {
		return nil
	}

	inEpochWindow := s.window != nil && s.window.InSuppressionWindow(snapshotTs)

	out := make([]types.RuleResult, 0, len(results))
	for _, r := range results {
		if inEpochWindow && isDBBackedRule(r.RuleID) {
			continue
		}

		key := r.RuleID + "|" + r.Entity
		prev, seen := s.lastFired[key]
		switch {
		case !seen:
			out = append(out, r)
			s.lastFired[key] = firedEntry{level: r.Level, at: snapshotTs}
		case snapshotTs.After(prev.at.Add(s.dedupTTL)):
			out = append(out, r)
			s.lastFired[key] = firedEntry{level: r.Level, at: snapshotTs}
		case prev.level == "WARN" && r.Level == "CRIT":
			// Escalation bypass: allow through once, reset cooldown.
			out = append(out, r)
			s.lastFired[key] = firedEntry{level: r.Level, at: snapshotTs}
		default:
			// Drop.
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}
