package cluster

import (
	"context"
	"encoding/json"
	"log"
	"os"
	"runtime"
	"sync"
	"time"

	"switchboard-core/store"
)

// ConnCounter provides WebSocket connection count (satisfied by events.Hub).
type ConnCounter interface {
	ConnCount() int
}

// RegistryConfig holds tunable cluster registry parameters.
type RegistryConfig struct {
	HeartbeatInterval time.Duration
	StaleThreshold    time.Duration
}

// Registry manages node self-registration, heartbeat, and stale sweep.
// Follows the workflow.Scanner pattern: stopCh + wg + ticker.
type Registry struct {
	nodeID    string
	endpoint  string
	store     store.ClusterStore
	hub       ConnCounter
	cfg       RegistryConfig
	startTime time.Time
	stopCh    chan struct{}
	wg        sync.WaitGroup

	// Optional callbacks — set after construction via setters.
	sandboxStats   func() (exec, errors uint64, avgMs float64)
	triggerFires   func() int64
	extensionCount func() int
}

// SetSandboxStats registers a callback for sandbox execution counters.
func (r *Registry) SetSandboxStats(fn func() (uint64, uint64, float64)) {
	r.sandboxStats = fn
}

// SetTriggerFireCount registers a callback for trigger fire count.
func (r *Registry) SetTriggerFireCount(fn func() int64) {
	r.triggerFires = fn
}

// SetExtensionCount registers a callback for active extension count.
func (r *Registry) SetExtensionCount(fn func() int) {
	r.extensionCount = fn
}

// NewRegistry creates a cluster registry instance.
func NewRegistry(nodeID, endpoint string, s store.ClusterStore, hub ConnCounter, cfg RegistryConfig) *Registry {
	return &Registry{
		nodeID:    nodeID,
		endpoint:  endpoint,
		store:     s,
		hub:       hub,
		cfg:       cfg,
		startTime: time.Now(),
		stopCh:    make(chan struct{}),
	}
}

// NodeID returns this node's identifier.
func (r *Registry) NodeID() string { return r.nodeID }

// Start registers the node and launches the heartbeat goroutine.
func (r *Registry) Start() error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := r.store.Register(ctx, r.nodeID, r.endpoint); err != nil {
		return err
	}

	r.wg.Add(1)
	go func() {
		defer r.wg.Done()
		ticker := time.NewTicker(r.cfg.HeartbeatInterval)
		defer ticker.Stop()

		for {
			select {
			case <-ticker.C:
				r.tick()
			case <-r.stopCh:
				return
			}
		}
	}()
	log.Printf("[cluster] started (node=%s, heartbeat=%s, stale=%s)",
		r.nodeID, r.cfg.HeartbeatInterval, r.cfg.StaleThreshold)
	return nil
}

// Stop deregisters the node and waits for the heartbeat goroutine to exit.
func (r *Registry) Stop() {
	close(r.stopCh)
	r.wg.Wait()

	// Best-effort deregister so peers don't have to wait for stale sweep.
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	if err := r.store.Deregister(ctx, r.nodeID); err != nil {
		log.Printf("[cluster] deregister failed: %v", err)
	}
	log.Printf("[cluster] stopped (node=%s)", r.nodeID)
}

// tick runs one heartbeat + sweep cycle.
func (r *Registry) tick() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	stats := r.collectStats()

	rows, err := r.store.Heartbeat(ctx, r.nodeID, stats)
	if err != nil {
		log.Printf("[cluster] heartbeat error: %v", err)
		return
	}
	if rows == 0 {
		// Self-eviction: this node was swept by a peer.
		// Kubernetes will restart the process → re-register.
		log.Printf("[cluster] FATAL: node %s evicted from registry — shutting down", r.nodeID)
		os.Exit(1)
	}

	swept, err := r.store.SweepStale(ctx, r.cfg.StaleThreshold)
	if err != nil {
		log.Printf("[cluster] sweep error: %v", err)
		return
	}
	if swept > 0 {
		log.Printf("[cluster] swept %d stale node(s)", swept)
	}
}

// collectStats gathers runtime metrics for the stats JSONB column.
func (r *Registry) collectStats() json.RawMessage {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)

	stats := map[string]any{
		"goroutines":    runtime.NumGoroutine(),
		"heap_alloc":    m.HeapAlloc,
		"heap_sys":      m.HeapSys,
		"stack_in_use":  m.StackInuse,
		"gc_cycles":     m.NumGC,
		"gc_pause_ns":   m.PauseNs[(m.NumGC+255)%256],
		"gc_cpu_pct":    m.GCCPUFraction * 100,
		"uptime_sec":    time.Since(r.startTime).Seconds(),
		"ws_clients":    r.hub.ConnCount(),
	}

	if r.extensionCount != nil {
		stats["extensions_loaded"] = r.extensionCount()
	}
	if r.sandboxStats != nil {
		exec, errors, avgMs := r.sandboxStats()
		stats["starlark_exec_total"] = exec
		stats["starlark_errors_total"] = errors
		stats["starlark_avg_duration_ms"] = avgMs
	}
	if r.triggerFires != nil {
		stats["trigger_fires_total"] = r.triggerFires()
	}

	data, _ := json.Marshal(stats)
	return data
}
