// Package pva provides an EPICS PV Access data source for uopi using the
// pure-Go gopva library.
package pva

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"strings"
	"sync"
	"time"

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

// PVA is the PV Access data source.
type PVA struct {
	client *gopva.Client

	mu       sync.RWMutex
	metadata map[string]datasource.Metadata
}

// New creates a new PV Access data source.
// Server addresses are read from the EPICS_PVA_ADDR_LIST environment variable;
// addrList overrides that when non-empty.
func New(addrList []string) datasource.DataSource {
	if len(addrList) > 0 {
		// Inject addresses into the environment so gopva.NewClient picks them up.
		_ = os.Setenv("EPICS_PVA_ADDR_LIST", strings.Join(addrList, " "))
	}
	return &PVA{
		metadata: make(map[string]datasource.Metadata),
	}
}

// Name implements datasource.DataSource.
func (p *PVA) Name() string { return "pva" }

// Connect creates the PVA client.
func (p *PVA) Connect(_ context.Context) error {
	cl, err := gopva.NewClient()
	if err != nil {
		return fmt.Errorf("pva: connect: %w", err)
	}
	p.client = cl
	slog.Info("pva: client started")
	return nil
}

// -------------------------------------------------------------------------- //
// Subscribe                                                                    //
// -------------------------------------------------------------------------- //

// Subscribe registers ch to receive live PVA value updates for signal.
func (p *PVA) Subscribe(ctx context.Context, signal string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
	slog.Info("pva: subscribing", "pv", signal)

	monCh := p.client.Monitor(ctx, signal)

	// Wait for the first event to confirm the subscription is live.
	// We do this in the goroutine to avoid blocking the caller.
	done := make(chan struct{})
	go func() {
		defer close(done)
		for evt := range monCh {
			if evt.Err != nil {
				slog.Error("pva: monitor error", "pv", signal, "err", evt.Err)
				select {
				case ch <- datasource.Value{
					Timestamp: time.Now(),
					Data:      nil,
					Quality:   datasource.QualityBad,
				}:
				default:
				}
				return
			}
			// Cache metadata from the first value.
			p.updateMetadata(signal, evt.Value)

			val := structToValue(evt.Value)
			select {
			case ch <- val:
			default:
			}
		}
	}()

	cancel := func() {
		// cancelling the context passed to Monitor stops it; the channel will be closed.
		// The goroutine will exit when monCh is closed.
		<-done
	}
	return datasource.CancelFunc(cancel), nil
}

// -------------------------------------------------------------------------- //
// GetMetadata                                                                  //
// -------------------------------------------------------------------------- //

// GetMetadata returns metadata for signal. It performs a one-shot GET if
// metadata has not been cached yet.
func (p *PVA) GetMetadata(ctx context.Context, signal string) (datasource.Metadata, error) {
	p.mu.RLock()
	if m, ok := p.metadata[signal]; ok {
		p.mu.RUnlock()
		return m, nil
	}
	p.mu.RUnlock()

	tctx, cancel := context.WithTimeout(ctx, 15*time.Second)
	defer cancel()

	sv, err := p.client.Get(tctx, signal)
	if err != nil {
		return datasource.Metadata{}, fmt.Errorf("pva: GetMetadata %q: %w", signal, err)
	}

	p.updateMetadata(signal, sv)

	p.mu.RLock()
	m := p.metadata[signal]
	p.mu.RUnlock()
	return m, nil
}

// updateMetadata derives and caches metadata from a StructValue.
func (p *PVA) updateMetadata(signal string, sv pvdata.StructValue) {
	m := structToMeta(signal, sv)
	p.mu.Lock()
	p.metadata[signal] = m
	p.mu.Unlock()
}

// -------------------------------------------------------------------------- //
// ListSignals                                                                  //
// -------------------------------------------------------------------------- //

// ListSignals returns metadata for all signals with cached information.
func (p *PVA) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
	p.mu.RLock()
	defer p.mu.RUnlock()
	out := make([]datasource.Metadata, 0, len(p.metadata))
	for _, m := range p.metadata {
		out = append(out, m)
	}
	return out, nil
}

// -------------------------------------------------------------------------- //
// Write                                                                        //
// -------------------------------------------------------------------------- //

// Write is not yet implemented for PVA.
func (p *PVA) Write(_ context.Context, _ string, _ any) error {
	return fmt.Errorf("pva: write not yet implemented")
}

// -------------------------------------------------------------------------- //
// History                                                                      //
// -------------------------------------------------------------------------- //

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

// -------------------------------------------------------------------------- //
// Helpers                                                                      //
// -------------------------------------------------------------------------- //

// structToValue converts a PVA StructValue to a datasource.Value.
// It follows NTScalar/NTScalarArray conventions: looks for a "value" field
// and an optional "alarm" struct for quality, and "timeStamp" for the timestamp.
func structToValue(sv pvdata.StructValue) datasource.Value {
	val := fieldByName(sv, "value")
	ts := extractTimestamp(sv)
	quality := extractQuality(sv)
	severity, status := extractSeverityStatus(sv)

	var data any
	if val != nil {
		data = normaliseValue(val)
	}

	return datasource.Value{
		Timestamp: ts,
		Data:      data,
		Quality:   quality,
		Severity:  severity,
		Status:    status,
	}
}

// structToMeta derives datasource.Metadata from a PVA StructValue.
func structToMeta(name string, sv pvdata.StructValue) datasource.Metadata {
	m := datasource.Metadata{
		Name:     name,
		Writable: true, // PVA channels are assumed writable until proven otherwise
	}

	val := fieldByName(sv, "value")
	if val == nil && len(sv.Fields) > 0 {
		val = sv.Fields[0].Value
	}

	if val != nil {
		switch val.(type) {
		case float64, float32:
			m.Type = datasource.TypeFloat64
		case int64, int32, int16, int8, uint64, uint32, uint16, uint8:
			m.Type = datasource.TypeInt64
		case string:
			m.Type = datasource.TypeString
		case bool:
			m.Type = datasource.TypeBool
		case []float64, []float32:
			m.Type = datasource.TypeFloat64Array
		default:
			m.Type = datasource.TypeFloat64
		}
	}

	// Extract display limits from NTScalar display structure.
	if disp := structByName(sv, "display"); disp != nil {
		if v := fieldByName(*disp, "limitLow"); v != nil {
			m.DisplayLow, _ = toFloat64(v)
		}
		if v := fieldByName(*disp, "limitHigh"); v != nil {
			m.DisplayHigh, _ = toFloat64(v)
		}
		if v := fieldByName(*disp, "units"); v != nil {
			m.Unit, _ = v.(string)
		}
	}

	// Extract control limits from NTScalar control structure.
	if ctrl := structByName(sv, "control"); ctrl != nil {
		if v := fieldByName(*ctrl, "limitLow"); v != nil {
			m.DriveLow, _ = toFloat64(v)
		}
		if v := fieldByName(*ctrl, "limitHigh"); v != nil {
			m.DriveHigh, _ = toFloat64(v)
		}
	}

	return m
}

// normaliseValue converts any PVA scalar/array value to datasource-compatible types.
func normaliseValue(v any) any {
	switch x := v.(type) {
	case float64:
		return x
	case float32:
		return float64(x)
	case int64:
		return x
	case int32:
		return int64(x)
	case int16:
		return int64(x)
	case int8:
		return int64(x)
	case uint64:
		return int64(x)
	case uint32:
		return int64(x)
	case uint16:
		return int64(x)
	case uint8:
		return int64(x)
	case bool:
		return x
	case string:
		return x
	case []float64:
		return x
	case []float32:
		out := make([]float64, len(x))
		for i, f := range x {
			out[i] = float64(f)
		}
		return out
	default:
		return fmt.Sprintf("%v", v)
	}
}

func extractTimestamp(sv pvdata.StructValue) time.Time {
	ts := structByName(sv, "timeStamp")
	if ts == nil {
		return time.Now()
	}
	sec := int64(0)
	nsec := int32(0)
	if v := fieldByName(*ts, "secondsPastEpoch"); v != nil {
		sec, _ = v.(int64)
	}
	if v := fieldByName(*ts, "nanoseconds"); v != nil {
		nsec, _ = v.(int32)
	}
	if sec == 0 {
		return time.Now()
	}
	// EPICS epoch: 1990-01-01 00:00:00 UTC = Unix 631152000
	const epicsEpoch = 631152000
	return time.Unix(sec+epicsEpoch, int64(nsec)).UTC()
}

func extractQuality(sv pvdata.StructValue) datasource.Quality {
	alarm := structByName(sv, "alarm")
	if alarm == nil {
		return datasource.QualityGood
	}
	sev := fieldByName(*alarm, "severity")
	if sev == nil {
		return datasource.QualityGood
	}
	s, _ := sev.(int32)
	switch s {
	case 1:
		return datasource.QualityUncertain
	case 2, 3:
		return datasource.QualityBad
	default:
		return datasource.QualityGood
	}
}

// extractSeverityStatus pulls the raw EPICS alarm severity and status from the
// NTScalar "alarm" struct. Returns (0, 0) when no alarm struct is present.
func extractSeverityStatus(sv pvdata.StructValue) (severity, status int) {
	alarm := structByName(sv, "alarm")
	if alarm == nil {
		return 0, 0
	}
	if v := fieldByName(*alarm, "severity"); v != nil {
		if s, ok := v.(int32); ok {
			severity = int(s)
		}
	}
	if v := fieldByName(*alarm, "status"); v != nil {
		if s, ok := v.(int32); ok {
			status = int(s)
		}
	}
	return severity, status
}

func toFloat64(v any) (float64, bool) {
	switch x := v.(type) {
	case float64:
		return x, true
	case float32:
		return float64(x), true
	case int64:
		return float64(x), true
	case int32:
		return float64(x), true
	default:
		return 0, false
	}
}

func fieldByName(sv pvdata.StructValue, name string) any {
	for _, f := range sv.Fields {
		if f.Name == name {
			return f.Value
		}
	}
	return nil
}

func structByName(sv pvdata.StructValue, name string) *pvdata.StructValue {
	for _, f := range sv.Fields {
		if f.Name == name {
			if s, ok := f.Value.(pvdata.StructValue); ok {
				return &s
			}
		}
	}
	return nil
}
