package rules

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"github.com/rs/zerolog"

	"github.com/operator/command-center/internal/eventbus"
	"github.com/operator/command-center/internal/notifications"
)

// Engine wires trigger evaluation to the dispatcher. One per process.
type Engine struct {
	db         *sql.DB
	bus        *eventbus.Bus
	dispatcher *notifications.Dispatcher
	logger     zerolog.Logger

	mu       sync.RWMutex
	triggers map[string]Trigger // keyed by Trigger.Type()

	stop context.CancelFunc
	wg   sync.WaitGroup

	tickInterval time.Duration
}

// NewEngine constructs the engine. Triggers are registered with Register()
// before Start().
func NewEngine(db *sql.DB, bus *eventbus.Bus, dispatcher *notifications.Dispatcher, logger zerolog.Logger) *Engine {
	return &Engine{
		db:           db,
		bus:          bus,
		dispatcher:   dispatcher,
		logger:       logger.With().Str("component", "rules").Logger(),
		triggers:     map[string]Trigger{},
		tickInterval: 60 * time.Second,
	}
}

// Register adds a trigger implementation. Idempotent.
func (e *Engine) Register(t Trigger) {
	e.mu.Lock()
	defer e.mu.Unlock()
	e.triggers[t.Type()] = t
}

// Start launches the event-driven subscribers and the periodic ticker.
func (e *Engine) Start(parent context.Context) {
	ctx, cancel := context.WithCancel(parent)
	e.stop = cancel

	// Per-trigger event subscription. One goroutine per (trigger, topic).
	e.mu.RLock()
	defer e.mu.RUnlock()
	subscribedTopics := map[string]bool{}
	for _, t := range e.triggers {
		for _, topic := range t.EventTopics() {
			key := t.Type() + ":" + topic
			if subscribedTopics[key] {
				continue
			}
			subscribedTopics[key] = true
			e.wg.Add(1)
			go e.runSubscriber(ctx, t, eventbus.Topic(topic))
		}
	}

	// One periodic ticker goroutine handles all triggers that have
	// PeriodicEvaluate semantics. Triggers self-report empty results when
	// they don't need to fire.
	e.wg.Add(1)
	go e.runPeriodic(ctx)
}

// Stop signals all goroutines and waits.
func (e *Engine) Stop() {
	if e.stop != nil {
		e.stop()
	}
	e.wg.Wait()
}

func (e *Engine) runSubscriber(ctx context.Context, t Trigger, topic eventbus.Topic) {
	defer e.wg.Done()
	sub := e.bus.Subscribe(topic)
	defer sub.Close()
	for {
		select {
		case <-ctx.Done():
			return
		case ev, ok := <-sub.Chan():
			if !ok {
				return
			}
			e.handleEvent(ctx, t, ev)
		}
	}
}

func (e *Engine) handleEvent(ctx context.Context, t Trigger, ev eventbus.Event) {
	rules, err := e.matchingRules(ctx, t.Type())
	if err != nil {
		e.logger.Warn().Err(err).Msg("matchingRules query failed")
		return
	}
	for _, r := range rules {
		notif, matched := t.MatchEvent(ctx, r.TriggerConfig, string(ev.Topic), ev.Payload)
		if !matched {
			continue
		}
		e.fire(ctx, r, notif)
	}
}

func (e *Engine) runPeriodic(ctx context.Context) {
	defer e.wg.Done()
	ticker := time.NewTicker(e.tickInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			e.runPeriodicOnce(ctx)
		}
	}
}

func (e *Engine) runPeriodicOnce(ctx context.Context) {
	e.mu.RLock()
	triggers := make([]Trigger, 0, len(e.triggers))
	for _, t := range e.triggers {
		triggers = append(triggers, t)
	}
	e.mu.RUnlock()

	for _, t := range triggers {
		rules, err := e.matchingRules(ctx, t.Type())
		if err != nil {
			continue
		}
		for _, r := range rules {
			notifs, err := t.PeriodicEvaluate(ctx, r.TriggerConfig)
			if err != nil {
				e.logger.Warn().Err(err).Str("rule", r.Name).Str("type", t.Type()).Msg("periodic evaluate failed")
				continue
			}
			for _, n := range notifs {
				e.fire(ctx, r, n)
			}
		}
	}
}

// fire dispatches a notification through the rule's configured channels,
// respecting the rule's cooldown. The dispatcher always writes to the
// dashboard channel regardless of the rule's channel list (per
// notifications.Dispatcher contract).
func (e *Engine) fire(ctx context.Context, r matchedRule, n notifications.Notification) {
	now := time.Now()
	if r.LastFiredAt != nil && r.CooldownSeconds > 0 {
		if now.Sub(*r.LastFiredAt) < time.Duration(r.CooldownSeconds)*time.Second {
			e.logger.Debug().Str("rule", r.Name).Msg("rule in cooldown; skipping")
			return
		}
	}
	n.RuleID = r.ID
	n.RuleName = r.Name
	if n.Timestamp.IsZero() {
		n.Timestamp = now
	}
	e.dispatcher.Dispatch(ctx, n, r.Channels, r.ChannelConfig)
	_, _ = e.db.ExecContext(ctx,
		`UPDATE notification_rules SET last_fired_at = ? WHERE id = ?`,
		now.Unix(), r.ID)
}

// matchedRule is the in-memory row for a single rule evaluation.
type matchedRule struct {
	ID              int64
	Name            string
	TriggerConfig   map[string]any
	Channels        []string
	ChannelConfig   map[string]map[string]any
	CooldownSeconds int
	LastFiredAt     *time.Time
}

// matchingRules returns the enabled rules whose trigger_type matches.
func (e *Engine) matchingRules(ctx context.Context, triggerType string) ([]matchedRule, error) {
	rows, err := e.db.QueryContext(ctx, `
		SELECT id, name, trigger_config_json, channels_json,
		       cooldown_seconds, last_fired_at
		FROM notification_rules
		WHERE enabled = 1 AND trigger_type = ?
	`, triggerType)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []matchedRule
	for rows.Next() {
		var (
			r          matchedRule
			cfgJSON    string
			chanJSON   string
			lastFiredI sql.NullInt64
		)
		if err := rows.Scan(&r.ID, &r.Name, &cfgJSON, &chanJSON, &r.CooldownSeconds, &lastFiredI); err != nil {
			return nil, err
		}
		var wrapper struct {
			Trigger struct {
				Config map[string]any `json:"config"`
			} `json:"trigger"`
			ChannelConfig map[string]map[string]any `json:"channel_config,omitempty"`
		}
		if err := json.Unmarshal([]byte(cfgJSON), &wrapper); err != nil {
			return nil, fmt.Errorf("rules: parse trigger config for %s: %w", r.Name, err)
		}
		r.TriggerConfig = wrapper.Trigger.Config
		r.ChannelConfig = wrapper.ChannelConfig
		if err := json.Unmarshal([]byte(chanJSON), &r.Channels); err != nil {
			return nil, err
		}
		if lastFiredI.Valid {
			t := time.Unix(lastFiredI.Int64, 0)
			r.LastFiredAt = &t
		}
		out = append(out, r)
	}
	return out, rows.Err()
}
