package synthetic

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sync"
	"time"

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

const definitionsFile = "synthetic.json"

// signalState holds everything needed to run one synthetic signal.
type signalState struct {
	def SignalDef
	rg  *runtimeGraph // compiled DAG; op-node state persists across evaluations

	// cancel stops the goroutine driving this signal.
	cancel context.CancelFunc
}

// Synthetic is a DataSource that computes values from upstream signals via
// configurable DSP pipelines.
type Synthetic struct {
	brk       *broker.Broker
	storePath string
	log       *slog.Logger

	mu      sync.RWMutex
	signals map[string]*signalState

	// root context provided by Connect; used to start per-signal goroutines.
	ctx context.Context
}

// New creates a Synthetic data source. storePath is the directory where
// synthetic.json is stored. brk is used to subscribe to upstream signals.
func New(storePath string, brk *broker.Broker, log *slog.Logger) *Synthetic {
	return &Synthetic{
		brk:       brk,
		storePath: storePath,
		log:       log,
		signals:   make(map[string]*signalState),
	}
}

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

// Connect loads the definitions file and starts all signal goroutines.
func (s *Synthetic) Connect(ctx context.Context) error {
	s.ctx = ctx

	defs, err := s.loadDefs()
	if err != nil {
		return fmt.Errorf("synthetic: load definitions: %w", err)
	}

	for _, def := range defs {
		if err := s.startSignal(def); err != nil {
			s.log.Warn("synthetic: failed to start signal", "name", def.Name, "err", err)
		}
	}
	return nil
}

// ListSignals returns metadata for all defined synthetic signals.
func (s *Synthetic) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	out := make([]datasource.Metadata, 0, len(s.signals))
	for _, st := range s.signals {
		out = append(out, defToMetadata(st.def, outTypeOf(st)))
	}
	return out, nil
}

// FilteredMetadata returns metadata for every defined synthetic signal for
// which keep returns true. It lets the API layer apply per-caller visibility
// rules (which depend on SignalDef fields not present in datasource.Metadata).
func (s *Synthetic) FilteredMetadata(keep func(SignalDef) bool) []datasource.Metadata {
	s.mu.RLock()
	defer s.mu.RUnlock()

	out := make([]datasource.Metadata, 0, len(s.signals))
	for _, st := range s.signals {
		if keep(st.def) {
			out = append(out, defToMetadata(st.def, outTypeOf(st)))
		}
	}
	return out
}

// GetMetadata returns metadata for a single named signal.
func (s *Synthetic) GetMetadata(_ context.Context, signal string) (datasource.Metadata, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	st, ok := s.signals[signal]
	if !ok {
		return datasource.Metadata{}, datasource.ErrNotFound
	}
	return defToMetadata(st.def, outTypeOf(st)), nil
}

// Subscribe registers ch to receive computed values for the named signal.
// Values are pushed whenever an upstream signal changes.
func (s *Synthetic) Subscribe(ctx context.Context, signal string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
	s.mu.RLock()
	st, ok := s.signals[signal]
	s.mu.RUnlock()
	if !ok {
		return nil, datasource.ErrNotFound
	}

	// Collect the source node references for this signal's DAG.
	refs := st.rg.sourceRefs()
	if len(refs) == 0 {
		return nil, fmt.Errorf("synthetic: signal %q has no upstream inputs", signal)
	}
	// Source node ids, index-aligned with refs, so updates map to graph inputs.
	srcIDs := make([]string, len(st.rg.sources))
	for i, s := range st.rg.sources {
		srcIDs[i] = s.id
	}

	ctx, cancel := context.WithCancel(ctx)

	go func() {
		defer cancel()

		// Latest value and timestamp per source node id.
		latest := make(map[string]dsp.Sample, len(refs))
		latestTs := make([]time.Time, len(refs))
		ready := make([]bool, len(refs))

		// Subscribe to every upstream ref via the broker.
		updateCh := make(chan indexedUpdate, 32*len(refs))

		unsubs := make([]func(), 0, len(refs))
		for i, ref := range refs {
			idx := i // capture
			perCh := make(chan broker.Update, 16)
			unsub, err := s.brk.Subscribe(ref, perCh)
			if err != nil {
				s.log.Warn("synthetic: upstream subscribe failed",
					"signal", signal, "upstream", ref, "err", err)
				// Continue; the slot will stay at zero.
			} else {
				unsubs = append(unsubs, unsub)
				go func() {
					for {
						select {
						case u, ok := <-perCh:
							if !ok {
								return
							}
							val := toSample(u.Value.Data)
							select {
							case updateCh <- indexedUpdate{idx: idx, val: val, ts: u.Value.Timestamp}:
							default:
							}
						case <-ctx.Done():
							return
						}
					}
				}()
			}
		}

		defer func() {
			for _, unsub := range unsubs {
				unsub()
			}
		}()

		for {
			select {
			case <-ctx.Done():
				return

			case upd := <-updateCh:
				latest[srcIDs[upd.idx]] = upd.val
				latestTs[upd.idx] = upd.ts
				ready[upd.idx] = true

				// Only compute once we have at least one value for every input.
				allReady := true
				for _, r := range ready {
					if !r {
						allReady = false
						break
					}
				}
				if !allReady {
					continue
				}

				// The output is computed from the latest value of every input, so
				// its timestamp is the most recent contributing sample time. Using
				// the triggering update's timestamp instead would drag the output
				// back in time whenever a slow/stale input fired, producing
				// non-monotonic or duplicated timestamps on plots.
				outTs := latestTs[0]
				for _, ts := range latestTs[1:] {
					if ts.After(outTs) {
						outTs = ts
					}
				}

				// Evaluate the DAG.
				s.mu.RLock()
				cur, stillExists := s.signals[signal]
				s.mu.RUnlock()
				if !stillExists {
					return
				}

				result, err := cur.rg.evalSample(latest)
				if err != nil {
					s.log.Warn("synthetic: pipeline error", "signal", signal, "err", err)
					continue
				}

				v := datasource.Value{
					Timestamp: outTs,
					Data:      result.AsAny(),
					Quality:   datasource.QualityGood,
				}
				select {
				case ch <- v:
				case <-ctx.Done():
					return
				}
			}
		}
	}()

	return datasource.CancelFunc(cancel), nil
}

// Write is not supported for synthetic signals.
func (s *Synthetic) Write(_ context.Context, _ string, _ any) error {
	return datasource.ErrNotWritable
}

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

// AddSignal adds a new synthetic signal definition at runtime and persists it.
func (s *Synthetic) AddSignal(def SignalDef) error {
	if def.Name == "" {
		return errors.New("signal name must not be empty")
	}
	if def.Version < 1 {
		def.Version = 1
	}

	rg, err := compileGraph(def)
	if err != nil {
		return fmt.Errorf("compile graph: %w", err)
	}

	s.mu.Lock()
	if _, exists := s.signals[def.Name]; exists {
		s.mu.Unlock()
		return fmt.Errorf("signal %q already exists", def.Name)
	}

	st := &signalState{def: def, rg: rg}
	s.signals[def.Name] = st
	s.mu.Unlock()

	if err := s.saveDefs(); err != nil {
		// Roll back the in-memory addition.
		s.mu.Lock()
		delete(s.signals, def.Name)
		s.mu.Unlock()
		return fmt.Errorf("persist definitions: %w", err)
	}

	return nil
}

// RemoveSignal removes a synthetic signal definition at runtime and persists.
func (s *Synthetic) RemoveSignal(name string) error {
	s.mu.Lock()
	st, ok := s.signals[name]
	if !ok {
		s.mu.Unlock()
		return datasource.ErrNotFound
	}
	// Stop the goroutine if it is running.
	if st.cancel != nil {
		st.cancel()
	}
	delete(s.signals, name)
	s.mu.Unlock()

	if err := s.saveDefs(); err != nil {
		return fmt.Errorf("persist definitions: %w", err)
	}
	return nil
}

// GetSignal returns the definition of a single named signal.
func (s *Synthetic) GetSignal(name string) (SignalDef, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	st, ok := s.signals[name]
	if !ok {
		return SignalDef{}, datasource.ErrNotFound
	}
	return st.def, nil
}

// UpdateSignal replaces the pipeline of an existing synthetic signal at runtime
// and persists the change. The signal must already exist.
func (s *Synthetic) UpdateSignal(def SignalDef) error {
	if def.Name == "" {
		return errors.New("signal name must not be empty")
	}

	rg, err := compileGraph(def)
	if err != nil {
		return fmt.Errorf("compile graph: %w", err)
	}

	s.mu.Lock()
	old, exists := s.signals[def.Name]
	if !exists {
		s.mu.Unlock()
		return datasource.ErrNotFound
	}
	// Preserve the superseded revision as a backup and bump the version.
	oldDef := old.def
	if oldDef.Version < 1 {
		oldDef.Version = 1
	}
	if err := s.backupVersion(oldDef); err != nil {
		s.mu.Unlock()
		return fmt.Errorf("back up revision: %w", err)
	}
	def.Version = oldDef.Version + 1
	if old.cancel != nil {
		old.cancel()
	}
	s.signals[def.Name] = &signalState{def: def, rg: rg}
	s.mu.Unlock()

	if err := s.saveDefs(); err != nil {
		return fmt.Errorf("persist definitions: %w", err)
	}
	return nil
}

// GetDefs returns a copy of all current signal definitions (for the REST API).
func (s *Synthetic) GetDefs() []SignalDef {
	s.mu.RLock()
	defer s.mu.RUnlock()

	out := make([]SignalDef, 0, len(s.signals))
	for _, st := range s.signals {
		out = append(out, st.def)
	}
	return out
}

// ── internal helpers ──────────────────────────────────────────────────────────

func (s *Synthetic) defsFilePath() string {
	return filepath.Join(s.storePath, definitionsFile)
}

func (s *Synthetic) loadDefs() ([]SignalDef, error) {
	path := s.defsFilePath()
	data, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		// Create an empty definitions file.
		if werr := os.WriteFile(path, []byte("[]\n"), 0o644); werr != nil {
			s.log.Warn("synthetic: could not create empty definitions file", "path", path, "err", werr)
		}
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	var defs []SignalDef
	if err := json.Unmarshal(data, &defs); err != nil {
		return nil, fmt.Errorf("parse %s: %w", path, err)
	}
	return defs, nil
}

func (s *Synthetic) saveDefs() error {
	s.mu.RLock()
	defs := make([]SignalDef, 0, len(s.signals))
	for _, st := range s.signals {
		defs = append(defs, st.def)
	}
	s.mu.RUnlock()

	data, err := json.MarshalIndent(defs, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(s.defsFilePath(), data, 0o644)
}

// startSignal compiles the DAG for def and registers the signalState.
// The actual goroutines are started lazily by Subscribe.
func (s *Synthetic) startSignal(def SignalDef) error {
	rg, err := compileGraph(def)
	if err != nil {
		return fmt.Errorf("compile graph for %q: %w", def.Name, err)
	}

	s.mu.Lock()
	s.signals[def.Name] = &signalState{def: def, rg: rg}
	s.mu.Unlock()

	s.log.Info("synthetic: signal registered", "name", def.Name)
	return nil
}

// defToMetadata converts a SignalDef into a datasource.Metadata. outType is the
// compiled graph's best-effort output type; an array output is reported as a
// waveform (TypeFloat64Array) so widgets can pick a compatible view.
func defToMetadata(def SignalDef, outType dsp.ValType) datasource.Metadata {
	dt := datasource.TypeFloat64
	if outType == dsp.ValArray {
		dt = datasource.TypeFloat64Array
	}
	return datasource.Metadata{
		Name:        def.Name,
		Type:        dt,
		Unit:        def.Meta.Unit,
		Description: def.Meta.Description,
		DisplayLow:  def.Meta.DisplayLow,
		DisplayHigh: def.Meta.DisplayHigh,
		Writable:    def.Meta.Writable,
	}
}

// outTypeOf returns the compiled output type for a signal state, or unknown.
func outTypeOf(st *signalState) dsp.ValType {
	if st == nil || st.rg == nil {
		return dsp.ValUnknown
	}
	return st.rg.outType
}

// TraceNode is a single node's computed value in a debug trace.
type TraceNode struct {
	Value  any    `json:"value"`
	Type   string `json:"type"` // "scalar" | "array"
	Approx bool   `json:"approx,omitempty"`
}

// TraceResult is the per-node outcome of a single-shot evaluation of an
// (unsaved) synthetic graph, used by the editor's live/debug overlay.
type TraceResult struct {
	Nodes map[string]TraceNode `json:"nodes"`
	Error string               `json:"error,omitempty"`
}

// Trace compiles def and evaluates it once with fresh state, returning the value
// computed for every node. Source node values are obtained via read(ds, name)
// (typically a broker ReadNow snapshot); a source that cannot be read defaults to
// scalar 0. Stateful ops are flagged Approx since their true running value
// depends on accumulated state this single-shot pass does not have. A per-node
// evaluation error is returned in Error with the partial values still populated.
func (s *Synthetic) Trace(def SignalDef, read func(ds, name string) (any, error)) (TraceResult, error) {
	rg, err := compileGraph(def)
	if err != nil {
		return TraceResult{}, err
	}
	sourceVals := make(map[string]dsp.Sample, len(rg.sources))
	for _, src := range rg.sources {
		raw, rerr := read(src.ref.DS, src.ref.Name)
		if rerr != nil || raw == nil {
			sourceVals[src.id] = dsp.Scalar(0)
			continue
		}
		sourceVals[src.id] = toSample(raw)
	}
	vals, evalErr := rg.evalSampleTrace(sourceVals)

	opType := make(map[string]string, len(rg.order))
	for _, n := range rg.order {
		if n.kind == "op" && n.op != nil {
			opType[n.id] = n.op.Type()
		}
	}
	res := TraceResult{Nodes: make(map[string]TraceNode, len(vals))}
	for id, sample := range vals {
		tn := TraceNode{Value: sample.AsAny(), Type: "scalar"}
		if sample.IsArray {
			tn.Type = "array"
		}
		if ot, ok := opType[id]; ok && dsp.IsStateful(ot) {
			tn.Approx = true
		}
		res.Nodes[id] = tn
	}
	if evalErr != nil {
		res.Error = evalErr.Error()
	}
	return res, nil
}

// toSample coerces a datasource.Value.Data into a dsp.Sample: arrays become
// array Samples (waveforms), everything else a scalar Sample.
func toSample(v any) dsp.Sample {
	switch val := v.(type) {
	case []float64:
		return dsp.Array(val)
	case []float32:
		out := make([]float64, len(val))
		for i, e := range val {
			out[i] = float64(e)
		}
		return dsp.Array(out)
	case []int:
		out := make([]float64, len(val))
		for i, e := range val {
			out[i] = float64(e)
		}
		return dsp.Array(out)
	default:
		return dsp.Scalar(toFloat64(v))
	}
}

// toFloat64 coerces any numeric scalar value from a datasource.Value.Data to float64.
func toFloat64(v any) float64 {
	switch val := v.(type) {
	case float64:
		return val
	case float32:
		return float64(val)
	case int64:
		return float64(val)
	case int32:
		return float64(val)
	case int:
		return float64(val)
	case bool:
		if val {
			return 1
		}
		return 0
	default:
		return 0
	}
}

// indexedUpdate carries a value from one upstream goroutine to the pipeline runner.
type indexedUpdate struct {
	idx int
	val dsp.Sample
	ts  time.Time
}
