package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/common/types"
	celref "github.com/google/cel-go/common/types/ref"

	"github.com/dbledeez/panel/controller/internal/db"
	"github.com/dbledeez/panel/controller/internal/events"
	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)

// Event-driven scheduler triggers, layered on top of the cron scheduler in
// scheduler.go. A registered "event" schedule subscribes to the event bus,
// evaluates a user-supplied CEL expression against each incoming event,
// and fires the action when the condition is true — optionally only after
// the condition has been continuously true for a sustained duration.
//
// CEL context (all fields always present; use zero values where irrelevant):
//
//	event_type        string   "app_state" | "instance_state" | "player" | "log"
//	instance_id       string
//	agent_id          string
//	players_online    int      0 when the event isn't an AppStateUpdate
//	players_max       int
//	uptime_seconds    int
//	instance_status   string   e.g. "running" "crashed"
//	exit_code         int
//	player_kind       string   "KIND_JOIN" etc.
//	player_name       string
//	player_id         string
//	player_detail     string
//	log_line          string
//	log_stream        string   "stdout" | "stderr"
//
// Examples:
//
//	players_online == 0                                      (with sustained=1800 → idle 30m → stop)
//	event_type == "instance_state" && instance_status == "crashed"
//	event_type == "player" && player_kind == "KIND_JOIN"
//	event_type == "log" && log_line.contains("OutOfMemoryError")

// eventTriggers layers on top of scheduleEngine. It owns the CEL env + a
// sustain-state tracker keyed by schedule_id.
type eventTriggers struct {
	log    interface{} // *slog.Logger, kept as any to avoid extra package overhead; we only log via the outer engine
	engine *scheduleEngine
	bus    *events.Bus

	celEnv *cel.Env

	mu       sync.Mutex
	programs map[string]cel.Program // schedule_id → compiled CEL program
	firstSeen map[string]time.Time   // schedule_id → when condition first became true (for sustain)
}

func newEventTriggers(engine *scheduleEngine, bus *events.Bus) (*eventTriggers, error) {
	env, err := cel.NewEnv(
		cel.Variable("event_type", cel.StringType),
		cel.Variable("instance_id", cel.StringType),
		cel.Variable("agent_id", cel.StringType),
		cel.Variable("players_online", cel.IntType),
		cel.Variable("players_max", cel.IntType),
		cel.Variable("uptime_seconds", cel.IntType),
		cel.Variable("instance_status", cel.StringType),
		cel.Variable("exit_code", cel.IntType),
		cel.Variable("player_kind", cel.StringType),
		cel.Variable("player_name", cel.StringType),
		cel.Variable("player_id", cel.StringType),
		cel.Variable("player_detail", cel.StringType),
		cel.Variable("log_line", cel.StringType),
		cel.Variable("log_stream", cel.StringType),
	)
	if err != nil {
		return nil, fmt.Errorf("new cel env: %w", err)
	}
	return &eventTriggers{
		engine:    engine,
		bus:       bus,
		celEnv:    env,
		programs:  map[string]cel.Program{},
		firstSeen: map[string]time.Time{},
	}, nil
}

// Register compiles + stores a CEL program for the schedule. Ignored if the
// schedule isn't an event trigger or is disabled.
func (e *eventTriggers) Register(r db.ScheduleRow) error {
	if r.TriggerKind != "event" || !r.Enabled {
		e.Unregister(r.ID)
		return nil
	}
	if r.EventSpec == "" {
		return fmt.Errorf("event trigger requires event_spec (CEL expression)")
	}
	ast, issues := e.celEnv.Compile(r.EventSpec)
	if issues != nil && issues.Err() != nil {
		return fmt.Errorf("cel compile: %w", issues.Err())
	}
	if !ast.OutputType().IsExactType(cel.BoolType) {
		return fmt.Errorf("cel expression must return bool, got %s", ast.OutputType())
	}
	prog, err := e.celEnv.Program(ast)
	if err != nil {
		return fmt.Errorf("cel program: %w", err)
	}
	e.mu.Lock()
	e.programs[r.ID] = prog
	delete(e.firstSeen, r.ID) // fresh tracking window
	e.mu.Unlock()
	return nil
}

// Unregister removes the compiled program + any sustain state.
func (e *eventTriggers) Unregister(scheduleID string) {
	e.mu.Lock()
	delete(e.programs, scheduleID)
	delete(e.firstSeen, scheduleID)
	e.mu.Unlock()
}

// Start subscribes to the bus and evaluates all registered event programs
// against every incoming event. Runs until ctx is cancelled.
func (e *eventTriggers) Start(ctx context.Context) {
	sub := e.bus.Subscribe(events.Filter{})
	go func() {
		defer sub.Close()
		for {
			select {
			case <-ctx.Done():
				return
			case ev, ok := <-sub.Events():
				if !ok {
					return
				}
				e.dispatch(ev)
			}
		}
	}()
}

func (e *eventTriggers) dispatch(ev *panelv1.Event) {
	input := eventToCEL(ev)
	targetInstance := inputInstanceID(input)

	e.mu.Lock()
	progs := make(map[string]cel.Program, len(e.programs))
	for id, p := range e.programs {
		progs[id] = p
	}
	e.mu.Unlock()

	for scheduleID, prog := range progs {
		// Look up the schedule to get sustained_seconds + instance filter.
		row := e.engine.scheduleByID(scheduleID)
		if row == nil || !row.Enabled {
			continue
		}
		if row.InstanceID != "" && row.InstanceID != targetInstance && targetInstance != "" {
			continue
		}
		out, _, err := prog.Eval(input)
		if err != nil {
			e.engine.record(scheduleID, "error", "cel eval: "+err.Error())
			continue
		}
		boolRef, ok := out.(types.Bool)
		if !ok {
			e.engine.record(scheduleID, "error", "cel result not bool")
			continue
		}
		condTrue := bool(boolRef)
		if e.sustainAndCheck(scheduleID, condTrue, int(row.EventSustainedSeconds)) {
			e.engine.fire(*row)
		}
	}
}

// sustainAndCheck returns true if the action should fire right now. Tracks
// the first-time-true per schedule. If the schedule has sustained=0 the
// action fires on every true evaluation.
func (e *eventTriggers) sustainAndCheck(id string, condition bool, sustainSec int) bool {
	e.mu.Lock()
	defer e.mu.Unlock()
	if !condition {
		delete(e.firstSeen, id)
		return false
	}
	if sustainSec <= 0 {
		return true
	}
	first, ok := e.firstSeen[id]
	if !ok {
		e.firstSeen[id] = time.Now()
		return false
	}
	if time.Since(first) >= time.Duration(sustainSec)*time.Second {
		delete(e.firstSeen, id) // reset so next episode tracks fresh
		return true
	}
	return false
}

// ---- CEL input assembly ----

func eventToCEL(ev *panelv1.Event) map[string]any {
	out := map[string]any{
		"event_type":       "",
		"instance_id":      "",
		"agent_id":         ev.AgentId,
		"players_online":   int64(0),
		"players_max":      int64(0),
		"uptime_seconds":   int64(0),
		"instance_status":  "",
		"exit_code":        int64(0),
		"player_kind":      "",
		"player_name":      "",
		"player_id":        "",
		"player_detail":    "",
		"log_line":         "",
		"log_stream":       "",
	}
	switch p := ev.Payload.(type) {
	case *panelv1.Event_AppState:
		out["event_type"] = "app_state"
		out["instance_id"] = p.AppState.InstanceId
		out["players_online"] = int64(p.AppState.PlayersOnline)
		out["players_max"] = int64(p.AppState.PlayersMax)
		out["uptime_seconds"] = int64(p.AppState.UptimeSeconds)
	case *panelv1.Event_InstanceState:
		out["event_type"] = "instance_state"
		out["instance_id"] = p.InstanceState.InstanceId
		out["instance_status"] = statusToDB(p.InstanceState.Status)
		out["exit_code"] = int64(p.InstanceState.ExitCode)
	case *panelv1.Event_Player:
		out["event_type"] = "player"
		out["instance_id"] = p.Player.InstanceId
		out["player_kind"] = p.Player.Kind.String()
		out["player_name"] = p.Player.PlayerName
		out["player_id"] = p.Player.PlayerId
		out["player_detail"] = p.Player.Detail
	case *panelv1.Event_Log:
		out["event_type"] = "log"
		out["instance_id"] = p.Log.InstanceId
		out["log_line"] = p.Log.Line
		out["log_stream"] = p.Log.Stream
	}
	return out
}

func inputInstanceID(in map[string]any) string {
	if s, ok := in["instance_id"].(string); ok {
		return s
	}
	return ""
}

// Keep celref imported — we use types.Bool above; some code paths may
// extend to other ref.Val consumers when sustained_for is added as a CEL
// function natively.
var _ celref.Val = types.Bool(false)
