// Package servervar provides a small persistent key/value data source for
// "server variables": named scalar values that the server-side control-logic
// engine writes (e.g. the state of a sequence) and that interface panels can
// read live. Panels may read any variable; writes from panels are gated to
// control-logic editors in the WebSocket write handler.
package servervar

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"sort"
	"sync"
	"time"

	"github.com/uopi/uopi/internal/datasource"
)

const fileName = "servervars.json"

type variable struct {
	value float64
	ts    time.Time
}

// Source is the "srv" data source. Variables are created on first write and
// persisted so their last value survives a restart.
type Source struct {
	path string

	mu     sync.RWMutex
	vars   map[string]*variable
	subs   map[string]map[int]chan<- datasource.Value
	nextID int
}

// New opens (or initialises) the server-variable store under storageDir.
func New(storageDir string) (*Source, error) {
	s := &Source{
		path: filepath.Join(storageDir, fileName),
		vars: map[string]*variable{},
		subs: map[string]map[int]chan<- datasource.Value{},
	}
	if err := s.load(); err != nil {
		return nil, err
	}
	return s, nil
}

func (s *Source) load() error {
	data, err := os.ReadFile(s.path)
	if os.IsNotExist(err) {
		return nil
	}
	if err != nil {
		return err
	}
	var raw map[string]float64
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	now := time.Now()
	for name, v := range raw {
		s.vars[name] = &variable{value: v, ts: now}
	}
	return nil
}

// saveLocked persists the current values atomically. Caller holds s.mu.
func (s *Source) saveLocked() {
	raw := make(map[string]float64, len(s.vars))
	for name, v := range s.vars {
		raw[name] = v.value
	}
	data, err := json.MarshalIndent(raw, "", "  ")
	if err != nil {
		return
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o644); err != nil {
		return
	}
	_ = os.Rename(tmp, s.path)
}

func meta(name string) datasource.Metadata {
	return datasource.Metadata{
		Name:        name,
		Type:        datasource.TypeFloat64,
		Description: "Server variable",
		Writable:    true,
	}
}

// Name implements datasource.DataSource.
func (s *Source) Name() string { return "srv" }

// Connect is a no-op — the store is opened in New.
func (s *Source) Connect(_ context.Context) error { return nil }

// ListSignals returns metadata for every defined server variable.
func (s *Source) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	names := make([]string, 0, len(s.vars))
	for name := range s.vars {
		names = append(names, name)
	}
	sort.Strings(names)
	out := make([]datasource.Metadata, 0, len(names))
	for _, name := range names {
		out = append(out, meta(name))
	}
	return out, nil
}

// GetMetadata returns metadata for a single variable. Unknown names still report
// writable metadata so that control logic / authorised panels may create them.
func (s *Source) GetMetadata(_ context.Context, signal string) (datasource.Metadata, error) {
	return meta(signal), nil
}

// Subscribe registers ch for updates and immediately delivers the current value.
func (s *Source) Subscribe(ctx context.Context, signal string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
	s.mu.Lock()
	if s.subs[signal] == nil {
		s.subs[signal] = map[int]chan<- datasource.Value{}
	}
	id := s.nextID
	s.nextID++
	s.subs[signal][id] = ch
	cur, ok := s.vars[signal]
	var first datasource.Value
	if ok {
		first = datasource.Value{Timestamp: cur.ts, Data: cur.value, Quality: datasource.QualityGood}
	}
	s.mu.Unlock()

	if ok {
		go func() {
			select {
			case ch <- first:
			case <-ctx.Done():
			}
		}()
	}

	return func() {
		s.mu.Lock()
		if m := s.subs[signal]; m != nil {
			delete(m, id)
			if len(m) == 0 {
				delete(s.subs, signal)
			}
		}
		s.mu.Unlock()
	}, nil
}

func toFloat64(v any) float64 {
	switch x := v.(type) {
	case float64:
		return x
	case float32:
		return float64(x)
	case int:
		return float64(x)
	case int64:
		return float64(x)
	case bool:
		if x {
			return 1
		}
		return 0
	case json.Number:
		f, _ := x.Float64()
		return f
	default:
		return 0
	}
}

// Write sets a variable (creating it if needed), persists, and fans the new value
// out to all subscribers.
func (s *Source) Write(_ context.Context, signal string, value any) error {
	v := toFloat64(value)

	s.mu.Lock()
	now := time.Now()
	s.vars[signal] = &variable{value: v, ts: now}
	s.saveLocked()
	subs := make([]chan<- datasource.Value, 0, len(s.subs[signal]))
	for _, ch := range s.subs[signal] {
		subs = append(subs, ch)
	}
	s.mu.Unlock()

	upd := datasource.Value{Timestamp: now, Data: v, Quality: datasource.QualityGood}
	for _, ch := range subs {
		select {
		case ch <- upd:
		default:
		}
	}
	return nil
}

// History is not supported.
func (s *Source) History(_ context.Context, _ string, _, _ time.Time, _ int) ([]datasource.Value, error) {
	return nil, datasource.ErrHistoryUnavailable
}
