package rules

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

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

// --- Trigger: filter_grab (event-driven) -----------------------------------
//
// Fires on every autobrr.grab event. Config supports an optional
// `filter_id` to scope to one filter; otherwise fires for any grab.

type FilterGrabTrigger struct{}

func (FilterGrabTrigger) Type() string          { return "filter_grab" }
func (FilterGrabTrigger) EventTopics() []string { return []string{string(eventbus.TopicAutobrrGrab)} }
func (FilterGrabTrigger) PeriodicEvaluate(_ context.Context, _ map[string]any) ([]notifications.Notification, error) {
	return nil, nil
}
func (FilterGrabTrigger) MatchEvent(_ context.Context, cfg map[string]any, _ string, payload any) (notifications.Notification, bool) {
	g, ok := payload.(webhooks.AutobrrGrabEvent)
	if !ok {
		return notifications.Notification{}, false
	}
	if wantFilter, _ := cfg["filter_id"].(string); wantFilter != "" && wantFilter != g.FilterID {
		return notifications.Notification{}, false
	}
	return notifications.Notification{
		Title:    "Grab: " + g.FilterName,
		Body:     g.ReleaseName,
		Severity: notifications.SeverityInfo,
		URL:      "/automation/filters/" + g.FilterID,
		Data: map[string]any{
			"filter_id":   g.FilterID,
			"filter_name": g.FilterName,
			"release":     g.ReleaseName,
			"info_hash":   g.InfoHash,
		},
	}, true
}

// --- Trigger: torrent_completed (event-driven) -----------------------------
//
// Fires on torrent.completed. Config supports optional `min_size_bytes`
// and `category` filters.

type TorrentCompletedTrigger struct{}

func (TorrentCompletedTrigger) Type() string { return "torrent_completed" }
func (TorrentCompletedTrigger) EventTopics() []string {
	return []string{string(eventbus.TopicTorrentCompleted)}
}
func (TorrentCompletedTrigger) PeriodicEvaluate(_ context.Context, _ map[string]any) ([]notifications.Notification, error) {
	return nil, nil
}
func (TorrentCompletedTrigger) MatchEvent(_ context.Context, cfg map[string]any, _ string, payload any) (notifications.Notification, bool) {
	q, ok := payload.(webhooks.QbitEvent)
	if !ok {
		return notifications.Notification{}, false
	}
	if cat, _ := cfg["category"].(string); cat != "" && !strings.EqualFold(cat, q.Category) {
		return notifications.Notification{}, false
	}
	return notifications.Notification{
		Title:    "Torrent completed",
		Body:     q.Name,
		Severity: notifications.SeverityInfo,
		URL:      "/torrents/" + q.InfoHash,
		Data: map[string]any{
			"info_hash": q.InfoHash,
			"name":      q.Name,
			"category":  q.Category,
		},
	}, true
}

// --- Trigger: tracker_scrape_error (event-driven) --------------------------
//
// Fires on tracker.status_changed when classification is auth/structural.
// Network failures don't escalate without a threshold; Phase 7's intelligence
// layer adds the auto-escalation.

type TrackerScrapeErrorTrigger struct{}

func (TrackerScrapeErrorTrigger) Type() string { return "tracker_scrape_error" }
func (TrackerScrapeErrorTrigger) EventTopics() []string {
	return []string{string(eventbus.TopicTrackerStatusChanged)}
}
func (TrackerScrapeErrorTrigger) PeriodicEvaluate(_ context.Context, _ map[string]any) ([]notifications.Notification, error) {
	return nil, nil
}
func (TrackerScrapeErrorTrigger) MatchEvent(_ context.Context, cfg map[string]any, _ string, payload any) (notifications.Notification, bool) {
	// Two payload shapes: webhooks.QbitEvent.Kind=tracker_changed OR a
	// structured tracker status event published by Phase 1's classifier.
	switch p := payload.(type) {
	case webhooks.QbitEvent:
		return notifications.Notification{
			Title:    "Tracker status changed",
			Body:     fmt.Sprintf("%s · %s", p.Name, p.Tracker),
			Severity: notifications.SeverityWarning,
			URL:      "/torrents/" + p.InfoHash,
		}, true
	case map[string]any:
		kind, _ := p["classification"].(string)
		if kind != "auth" && kind != "structural" {
			return notifications.Notification{}, false
		}
		trackerID, _ := p["tracker_id"].(string)
		if want, _ := cfg["tracker_id"].(string); want != "" && want != trackerID {
			return notifications.Notification{}, false
		}
		msg, _ := p["message"].(string)
		return notifications.Notification{
			Title:    "Tracker scrape: " + kind,
			Body:     trackerID + ": " + msg,
			Severity: notifications.SeverityCritical,
			URL:      "/trackers/" + trackerID,
		}, true
	}
	return notifications.Notification{}, false
}

// --- Trigger: ratio_threshold (periodic, SQL) ------------------------------
//
// Reads the latest ratio_snapshots per tracker; fires when real_ratio
// crosses below the configured threshold. Cooldown prevents storms while
// the ratio remains under the line.

type RatioThresholdTrigger struct {
	DB *sql.DB
}

func (RatioThresholdTrigger) Type() string          { return "ratio_threshold" }
func (RatioThresholdTrigger) EventTopics() []string { return nil }
func (RatioThresholdTrigger) MatchEvent(_ context.Context, _ map[string]any, _ string, _ any) (notifications.Notification, bool) {
	return notifications.Notification{}, false
}
func (t RatioThresholdTrigger) PeriodicEvaluate(ctx context.Context, cfg map[string]any) ([]notifications.Notification, error) {
	if t.DB == nil {
		return nil, errors.New("ratio_threshold: db nil")
	}
	below, _ := cfg["below"].(float64)
	if below <= 0 {
		return nil, errors.New("ratio_threshold: 'below' (float, >0) required")
	}
	trackerFilter, _ := cfg["tracker_id"].(string)

	q := `
		WITH latest AS (
		  SELECT tracker_id, real_ratio,
		         ROW_NUMBER() OVER (PARTITION BY tracker_id ORDER BY timestamp DESC) AS rn
		  FROM ratio_snapshots WHERE simulation_id IS NULL
		)
		SELECT tracker_id, real_ratio FROM latest WHERE rn = 1 AND real_ratio < ?
	`
	args := []any{below}
	if trackerFilter != "" {
		q += ` AND tracker_id = ?`
		args = append(args, trackerFilter)
	}
	rows, err := t.DB.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []notifications.Notification
	for rows.Next() {
		var id string
		var r sql.NullFloat64
		if err := rows.Scan(&id, &r); err != nil {
			return nil, err
		}
		if !r.Valid {
			continue
		}
		out = append(out, notifications.Notification{
			Title:    fmt.Sprintf("Ratio low: %s", id),
			Body:     fmt.Sprintf("real_ratio %.3f is below threshold %.3f", r.Float64, below),
			Severity: notifications.SeverityWarning,
			URL:      "/trackers/" + id,
			Data: map[string]any{
				"tracker_id": id,
				"ratio":      r.Float64,
				"threshold":  below,
			},
		})
	}
	return out, rows.Err()
}

// --- Trigger: custom_query (periodic, SQL) ---------------------------------
//
// Fires when an operator-supplied SELECT returns at least one row. The
// query receives no parameters; if it needs values they must be inlined
// (operator's responsibility, with the obvious caveats — see DECISIONS.md D34).

type CustomQueryTrigger struct {
	DB *sql.DB
}

func (CustomQueryTrigger) Type() string          { return "custom_query" }
func (CustomQueryTrigger) EventTopics() []string { return nil }
func (CustomQueryTrigger) MatchEvent(_ context.Context, _ map[string]any, _ string, _ any) (notifications.Notification, bool) {
	return notifications.Notification{}, false
}
func (t CustomQueryTrigger) PeriodicEvaluate(ctx context.Context, cfg map[string]any) ([]notifications.Notification, error) {
	if t.DB == nil {
		return nil, errors.New("custom_query: db nil")
	}
	query, _ := cfg["query"].(string)
	if strings.TrimSpace(query) == "" {
		return nil, errors.New("custom_query: 'query' required")
	}
	if !looksLikeSelect(query) {
		return nil, errors.New("custom_query: only SELECT statements allowed")
	}
	title, _ := cfg["title"].(string)
	if title == "" {
		title = "Custom query fired"
	}
	body, _ := cfg["body"].(string)

	rctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	rows, err := t.DB.QueryContext(rctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	if rows.Next() {
		return []notifications.Notification{{
			Title:    title,
			Body:     body,
			Severity: notifications.SeverityWarning,
		}}, nil
	}
	return nil, rows.Err()
}

func looksLikeSelect(q string) bool {
	trimmed := strings.TrimSpace(q)
	// Only allow leading SELECT/WITH; reject statements that begin with
	// data-mutating keywords. This is a paranoid first pass — the SQLite
	// connection should ideally be opened read-only for this trigger.
	upper := strings.ToUpper(trimmed)
	return strings.HasPrefix(upper, "SELECT ") || strings.HasPrefix(upper, "WITH ")
}
