// Package cycle orchestrates one full sentry monitoring cycle: discover
// tables, snapshot data + app health, persist, evaluate rules, suppress,
// translate, and notify. All dependencies are injected as small interfaces
// so cycles are unit-testable with fakes.
package cycle

import (
	"context"
	"sync"
	"time"

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

// HistoryFinder matches sentry/internal/rules.HistoryFinder. Declared locally
// so the cycle package does not need to import rules purely for a method
// signature. Satisfied structurally by *store.LRU and any compatible mock.
// Exported so wiring code in cmd/sentry can build adapters that satisfy
// RuleEngine without pulling in the rules package's own HistoryFinder type.
type HistoryFinder interface {
	FindClosest(target time.Time, epoch int64) (types.Snapshot, bool)
}

// TableDiscoverer returns the list of tables to snapshot.
type TableDiscoverer interface {
	DiscoverTables(ctx context.Context) ([]string, error)
}

// SnapshotCollector runs COUNT(*) + MAX() per table, degrading to info_schema
// estimates for tables present in slowTables.
type SnapshotCollector interface {
	Snapshot(ctx context.Context, tables []string, slowTables map[string]bool) ([]types.TableRow, error)
}

// AppScraper fetches the CRM /internal/sentry-metrics endpoint.
type AppScraper interface {
	Scrape(ctx context.Context) (*types.AppHealth, error)
}

// EpochTracker reports the epoch for a given time.
type EpochTracker interface {
	EpochAt(at time.Time) int64
}

// SnapshotStore is the in-memory LRU cache of recent snapshots.
type SnapshotStore interface {
	Add(s types.Snapshot)
	HistoryFinder
}

// SnapshotPersister uploads a snapshot to durable storage (S3).
type SnapshotPersister interface {
	PutSnapshot(ctx context.Context, snap types.Snapshot) error
}

// RuleEngine evaluates rules against the current snapshot using history lookups.
type RuleEngine interface {
	Evaluate(current types.Snapshot, history HistoryFinder) []types.RuleResult
}

// Suppressor filters rule results through dedupe/cooldown logic.
type Suppressor interface {
	Filter(snapshotTs time.Time, results []types.RuleResult) []types.RuleResult
}

// LLMTranslator converts a rule result into natural language. Errors fall
// back to the template formatter.
type LLMTranslator interface {
	Translate(ctx context.Context, r types.RuleResult) (string, error)
}

// TemplateFormatter is the deterministic fallback renderer.
type TemplateFormatter func(r types.RuleResult) string

// Notifier delivers the final message text to the user-facing channel.
type Notifier interface {
	SendText(ctx context.Context, text string) error
}

// Logger is an optional structured logger. Nil is acceptable.
type Logger interface {
	Printf(format string, args ...any)
}

// Cycle ties all sentry subsystems together. One Cycle per sentry process.
type Cycle struct {
	Version    string
	Discoverer TableDiscoverer
	Collector  SnapshotCollector
	Scraper    AppScraper
	Epoch      EpochTracker
	Store      SnapshotStore
	Persister  SnapshotPersister
	Engine     RuleEngine
	Suppressor Suppressor
	LLM        LLMTranslator
	Template   TemplateFormatter
	Notifier   Notifier
	Logger     Logger

	// EnvLabel is an optional environment tag (e.g. "prod", "staging") that
	// gets prefixed to every notification sent to the Notifier. Used so
	// human readers of the ops channel can tell at a glance which cluster
	// fired an alert. Empty string = no prefix.
	EnvLabel string

	// NotifyOnlyCRIT, when true, causes Run() to skip sending WARN-level
	// rule results to the Notifier. The results are still logged and still
	// pass through suppression; they simply do not reach Lark. Used to cut
	// noise in the ops channel when the operator only wants to be paged on
	// CRIT. WARN is still archived in logs for post-hoc review.
	NotifyOnlyCRIT bool

	// slowTables is a mutable set of tables that degraded to info_schema
	// estimates in a prior cycle. In this task it lives in-memory only — S3
	// persistence of slow_tables.json is a follow-up.
	slowTables map[string]bool
}

// NewCycle constructs a Cycle from a populated config struct. None of the
// interface fields may be nil except Logger and LLM (LLM nil means always use
// Template; Logger nil means silent).
func NewCycle(opts Cycle) *Cycle {
	c := opts
	if c.slowTables == nil {
		c.slowTables = make(map[string]bool)
	}
	return &c
}

// Run executes one full cycle end-to-end. It returns an error only when a
// fatal pre-snapshot step fails (table discovery or snapshot collection).
// Partial failures during persist, notify, or app scrape are logged and the
// cycle continues.
func (c *Cycle) Run(ctx context.Context) error {
	// Step 1: discover tables.
	tables, err := c.Discoverer.DiscoverTables(ctx)
	if err != nil {
		c.logf("cycle: discover failed: %v", err)
		return err
	}

	// Steps 2 + 3 in parallel: snapshot tables and scrape app.
	var (
		rows   []types.TableRow
		rowErr error
		app    *types.AppHealth
		appErr error
		wg     sync.WaitGroup
	)
	wg.Add(2)
	go func() {
		defer wg.Done()
		rows, rowErr = c.Collector.Snapshot(ctx, tables, c.slowTables)
	}()
	go func() {
		defer wg.Done()
		app, appErr = c.Scraper.Scrape(ctx)
	}()
	wg.Wait()

	if rowErr != nil {
		c.logf("cycle: snapshot failed: %v", rowErr)
		return rowErr
	}
	if appErr != nil || app == nil {
		// Soft-fail: continue with just data metrics.
		if appErr != nil {
			c.logf("cycle: app scrape failed: %v", appErr)
		}
		app = &types.AppHealth{}
	}

	// Steps 4 + 5: compute epoch and assemble snapshot.
	now := time.Now().UTC()
	snap := types.Snapshot{
		SnapshotTs: now,
		EpochID:    c.Epoch.EpochAt(now),
		Version:    c.Version,
		Tables:     rows,
		App:        *app,
	}

	// Feedback loop: track any tables that degraded this cycle so the next
	// cycle skips count(*) and uses info_schema estimates directly.
	for _, row := range rows {
		if row.Source == types.SourceInfoSchemaEst {
			c.slowTables[row.Name] = true
		}
	}

	// Step 6: push into in-memory LRU.
	c.Store.Add(snap)

	// Step 7: persist to S3 (soft-fail).
	if err := c.Persister.PutSnapshot(ctx, snap); err != nil {
		c.logf("cycle: persist failed: %v", err)
	}

	// Step 8: evaluate rules against history.
	results := c.Engine.Evaluate(snap, c.Store)

	// Step 9: suppression.
	surviving := c.Suppressor.Filter(snap.SnapshotTs, results)

	// Step 10: translate + notify each surviving result.
	//
	// Filtering:
	//   - If NotifyOnlyCRIT is true, WARN-level results are still logged
	//     but not sent to the Notifier. CRIT always goes through.
	//   - If EnvLabel is non-empty, every notification text is prefixed
	//     with "[envlabel] " so ops-channel readers see which cluster the
	//     alert came from.
	var notified int
	for _, r := range surviving {
		if c.NotifyOnlyCRIT && r.Level != "CRIT" {
			c.logf("cycle: skip notify (level=%s, NotifyOnlyCRIT=true) rule=%s entity=%s",
				r.Level, r.RuleID, r.Entity)
			continue
		}
		text := c.translate(ctx, r)
		if c.EnvLabel != "" {
			text = "[" + c.EnvLabel + "] " + text
		}
		if err := c.Notifier.SendText(ctx, text); err != nil {
			c.logf("cycle: notify failed for %s: %v", r.RuleID, err)
			continue
		}
		notified++
	}

	c.logf("cycle: ok tables=%d fires=%d surviving=%d notified=%d",
		len(rows), len(results), len(surviving), notified)
	return nil
}

// translate attempts LLM translation first and falls back to the template
// formatter on error or empty output.
func (c *Cycle) translate(ctx context.Context, r types.RuleResult) string {
	if c.LLM == nil {
		return c.Template(r)
	}
	text, err := c.LLM.Translate(ctx, r)
	if err != nil || text == "" {
		c.logf("cycle: llm failed for %s: %v - using template fallback", r.RuleID, err)
		return c.Template(r)
	}
	return text
}

func (c *Cycle) logf(format string, args ...any) {
	if c.Logger != nil {
		c.Logger.Printf(format, args...)
	}
}
