package triggers

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"go.starlark.net/starlark"

	"switchboard-core/events"
	"switchboard-core/models"
	"switchboard-core/sandbox"
)

// wireEventTrigger subscribes to the bus pattern for an event trigger.
func (e *Engine) wireEventTrigger(t *models.Trigger) {
	if t.EventPattern == "" {
		return
	}

	triggerID := t.ID
	packageID := t.PackageID
	entryPoint := t.EntryPoint
	pattern := t.EventPattern

	unsub := e.bus.Subscribe(pattern, func(ev events.Event) {
		// Fire asynchronously — never block the event bus
		go e.fireEventTrigger(triggerID, packageID, entryPoint, ev)
	})

	e.mu.Lock()
	e.unsubs[triggerID] = unsub
	e.mu.Unlock()
}

// fireEventTrigger invokes the Starlark handler for an event trigger.
func (e *Engine) fireEventTrigger(triggerID, packageID, entryPoint string, ev events.Event) {
	start := time.Now()
	ctx := e.ctx
	if ctx == nil {
		ctx = context.Background()
	}

	// Re-check trigger is still enabled
	trigger, err := e.stores.Triggers.GetByID(ctx, triggerID)
	if err != nil || trigger == nil || !trigger.Enabled {
		return
	}

	// Load package
	pkg, err := e.stores.Packages.Get(ctx, packageID)
	if err != nil || pkg == nil || pkg.Status != models.PackageStatusActive {
		return
	}

	// Check triggers.register permission
	if !e.hasPermission(ctx, packageID, models.ExtPermTriggersRegister) {
		return
	}

	// Build Starlark context dict
	ctxDict := starlark.NewDict(6)
	_ = ctxDict.SetKey(starlark.String("trigger_type"), starlark.String("event"))
	_ = ctxDict.SetKey(starlark.String("trigger_id"), starlark.String(triggerID))
	_ = ctxDict.SetKey(starlark.String("event_label"), starlark.String(ev.Label))
	_ = ctxDict.SetKey(starlark.String("event_ts"), starlark.MakeInt64(ev.Ts))
	if ev.Room != "" {
		_ = ctxDict.SetKey(starlark.String("event_room"), starlark.String(ev.Room))
	}
	if len(ev.Payload) > 0 {
		var payloadMap map[string]any
		if json.Unmarshal(ev.Payload, &payloadMap) == nil {
			_ = ctxDict.SetKey(starlark.String("event_payload"), mapToStarlark(payloadMap))
		} else {
			_ = ctxDict.SetKey(starlark.String("event_payload"), starlark.String(string(ev.Payload)))
		}
	}

	// Call entry point with full sandbox context
	_, output, callErr := e.runner.CallEntryPoint(ctx, pkg, entryPoint,
		starlark.Tuple{ctxDict}, nil, nil)

	duration := int(time.Since(start).Milliseconds())
	errStr := ""
	if callErr != nil {
		errStr = callErr.Error()
		log.Printf("  ⚠️  trigger[event] %s/%s error: %v", packageID, entryPoint, callErr)
	}

	// Update fire state
	_ = e.stores.Triggers.UpdateFireState(ctx, triggerID, start, errStr)

	// Log execution
	e.logExecution(triggerID, "", start, duration, callErr == nil, errStr, output)
	e.publishEvent("trigger.fired", triggerID)
}

// hasPermission checks if a package has a specific granted permission.
func (e *Engine) hasPermission(ctx context.Context, packageID, permission string) bool {
	if e.stores.ExtPermissions == nil {
		return false
	}
	granted, err := e.stores.ExtPermissions.GrantedForPackage(ctx, packageID)
	if err != nil {
		return false
	}
	for _, p := range granted {
		if p == permission {
			return true
		}
	}
	return false
}

// mapToStarlark converts a map[string]any to a Starlark dict.
func mapToStarlark(m map[string]any) *starlark.Dict {
	d := starlark.NewDict(len(m))
	for k, v := range m {
		_ = d.SetKey(starlark.String(k), goToStarlark(v))
	}
	return d
}

// goToStarlark converts a Go value to a Starlark value.
func goToStarlark(v any) starlark.Value {
	switch val := v.(type) {
	case nil:
		return starlark.None
	case bool:
		return starlark.Bool(val)
	case int:
		return starlark.MakeInt(val)
	case int64:
		return starlark.MakeInt64(val)
	case float64:
		return starlark.Float(val)
	case string:
		return starlark.String(val)
	case map[string]any:
		return mapToStarlark(val)
	case []any:
		elems := make([]starlark.Value, len(val))
		for i, e := range val {
			elems[i] = goToStarlark(e)
		}
		return starlark.NewList(elems)
	default:
		return starlark.String(fmt.Sprintf("%v", val))
	}
}

// RunContext builds a sandbox.RunContext for a trigger invocation.
func triggerRunContext(userID, teamID string) *sandbox.RunContext {
	if userID == "" {
		return nil
	}
	return &sandbox.RunContext{
		UserID: userID,
		TeamID: teamID,
	}
}
