// Package stub provides a synthetic data source that emits deterministic test
// signals (sine waves, ramp, boolean toggle) without any external dependencies.
// It is used during development and for integration tests.
package stub

import (
	"context"
	"fmt"
	"math"
	"math/rand/v2"
	"sync"
	"time"

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

const updateInterval = 100 * time.Millisecond // 10 Hz default

type signalDef struct {
	meta     datasource.Metadata
	interval time.Duration // 0 → updateInterval
	fn       func(t time.Time) any
}

// Stub is a data source that emits canned signals for development and testing.
type Stub struct {
	signals map[string]signalDef

	// writable values (signal name → current value)
	mu     sync.RWMutex
	values map[string]any
}

// New returns a Stub with a predefined set of test signals.
func New() *Stub {
	s := &Stub{
		signals: make(map[string]signalDef),
		values:  make(map[string]any),
	}
	s.register(signalDef{
		meta: datasource.Metadata{
			Name: "sine_1hz", Type: datasource.TypeFloat64,
			Unit: "V", DisplayLow: -1, DisplayHigh: 1,
			Description: "1 Hz sine wave",
		},
		fn: func(t time.Time) any {
			return math.Sin(2 * math.Pi * float64(t.UnixNano()) / 1e9)
		},
	})
	s.register(signalDef{
		meta: datasource.Metadata{
			Name: "sine_01hz", Type: datasource.TypeFloat64,
			Unit: "A", DisplayLow: -1, DisplayHigh: 1,
			Description: "0.1 Hz sine wave",
		},
		fn: func(t time.Time) any {
			return math.Sin(2 * math.Pi * 0.1 * float64(t.UnixNano()) / 1e9)
		},
	})
	s.register(signalDef{
		meta: datasource.Metadata{
			Name: "ramp_10s", Type: datasource.TypeFloat64,
			Unit: "mm", DisplayLow: 0, DisplayHigh: 100,
			Description: "10-second sawtooth ramp, 0–100",
		},
		fn: func(t time.Time) any {
			return math.Mod(float64(t.UnixNano())/1e10, 100)
		},
	})
	s.register(signalDef{
		meta: datasource.Metadata{
			Name: "toggle_1hz", Type: datasource.TypeBool,
			Description: "1 Hz square wave (boolean)",
		},
		fn: func(t time.Time) any { return (t.Unix() % 2) == 0 },
	})
	s.register(signalDef{
		meta: datasource.Metadata{
			Name: "noise", Type: datasource.TypeFloat64,
			Unit: "dB", DisplayLow: -1, DisplayHigh: 1,
			Description: "White noise",
		},
		fn: func(t time.Time) any {
			// Seed a per-tick RNG from the timestamp so results are
			// reproducible within a tick but vary across ticks.
			r := rand.New(rand.NewPCG(uint64(t.UnixNano()), 0))
			return r.Float64()*2 - 1
		},
	})
	s.register(signalDef{
		meta: datasource.Metadata{
			Name: "setpoint", Type: datasource.TypeFloat64,
			Unit: "°C", DisplayLow: 0, DisplayHigh: 200,
			DriveLow: 0, DriveHigh: 200,
			Description: "Writable setpoint (stub)",
			Writable:    true,
		},
		fn: func(t time.Time) any { return 25.0 }, // default; overwritten by Write
	})
	s.values["setpoint"] = 25.0
	s.register(signalDef{
		meta: datasource.Metadata{
			Name: "counter_fast", Type: datasource.TypeFloat64,
			Description: "Fast monotonic counter (1 ms tick) for benchmarks",
		},
		interval: time.Millisecond,
		fn:       func(t time.Time) any { return float64(t.UnixMilli()) },
	})
	return s
}

// NewN creates a Stub with n dynamically generated signals named "pv_0" … "pv_{n-1}".
// Each signal is a sine wave at a distinct frequency, emitted at 10 Hz.
// Use this for stress and scale testing.
func NewN(n int) *Stub {
	s := &Stub{
		signals: make(map[string]signalDef),
		values:  make(map[string]any),
	}
	for i := range n {
		freq := 0.1 + float64(i)*0.01 // spread frequencies so signals differ
		s.register(signalDef{
			meta: datasource.Metadata{
				Name:        fmt.Sprintf("pv_%d", i),
				Type:        datasource.TypeFloat64,
				Unit:        "V",
				DisplayLow:  -1,
				DisplayHigh: 1,
				Description: fmt.Sprintf("Synthetic PV #%d (%.2f Hz sine)", i, freq),
			},
			fn: func(t time.Time) any {
				return math.Sin(2 * math.Pi * freq * float64(t.UnixNano()) / 1e9)
			},
		})
	}
	return s
}

func (s *Stub) register(d signalDef) {
	s.signals[d.meta.Name] = d
}

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

// Connect is a no-op for the stub source.
func (s *Stub) Connect(_ context.Context) error { return nil }

// ListSignals returns metadata for all built-in stub signals.
func (s *Stub) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
	out := make([]datasource.Metadata, 0, len(s.signals))
	for _, d := range s.signals {
		out = append(out, d.meta)
	}
	return out, nil
}

// GetMetadata returns the metadata for the named signal.
func (s *Stub) GetMetadata(_ context.Context, signal string) (datasource.Metadata, error) {
	d, ok := s.signals[signal]
	if !ok {
		return datasource.Metadata{}, datasource.ErrNotFound
	}
	return d.meta, nil
}

// Subscribe starts a ticker that pushes generated values into ch.
// The tick interval is the signal's configured interval (default 10 Hz).
func (s *Stub) Subscribe(ctx context.Context, signal string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
	def, ok := s.signals[signal]
	if !ok {
		return nil, datasource.ErrNotFound
	}

	interval := def.interval
	if interval == 0 {
		interval = updateInterval
	}

	ctx, cancel := context.WithCancel(ctx)
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		for {
			select {
			case t := <-ticker.C:
				var data any
				if def.meta.Writable {
					s.mu.RLock()
					data = s.values[signal]
					s.mu.RUnlock()
				} else {
					data = def.fn(t)
				}
				v := datasource.Value{Timestamp: t, Data: data, Quality: datasource.QualityGood}
				select {
				case ch <- v:
				case <-ctx.Done():
					return
				}
			case <-ctx.Done():
				return
			}
		}
	}()

	return datasource.CancelFunc(cancel), nil
}

// Write updates the current value of a writable signal.
func (s *Stub) Write(_ context.Context, signal string, value any) error {
	def, ok := s.signals[signal]
	if !ok {
		return datasource.ErrNotFound
	}
	if !def.meta.Writable {
		return datasource.ErrNotWritable
	}
	s.mu.Lock()
	s.values[signal] = value
	s.mu.Unlock()
	return nil
}

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