// Package modbus implements a Modbus TCP data source. Each configured device is
// polled over its own TCP connection; registers are exposed as signals named
// "device:register". Reads use the holding/input/coil/discrete function codes;
// writable holding registers and coils accept Write.
package modbus

import (
	"context"
	"fmt"
	"strings"
	"time"

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

const defaultPollInterval = time.Second

// deviceClient bundles a parsed device with its wire client and register lookup.
type deviceClient struct {
	dev    Device
	cli    *client
	byName map[string]Register
}

// Modbus is a datasource.DataSource backed by one or more Modbus TCP devices.
type Modbus struct {
	pollInterval time.Duration
	devices      map[string]*deviceClient // device name → client
	signals      map[string]signalRef     // "device:register" → ref
}

type signalRef struct {
	device string
	reg    Register
}

// New builds a Modbus source from config. It does not dial; connections are
// established lazily on first poll/write.
func New(cfg Config) (*Modbus, error) {
	poll := time.Duration(cfg.PollIntervalMs) * time.Millisecond
	if poll <= 0 {
		poll = defaultPollInterval
	}
	m := &Modbus{
		pollInterval: poll,
		devices:      make(map[string]*deviceClient),
		signals:      make(map[string]signalRef),
	}
	for _, dev := range cfg.Devices {
		if dev.Name == "" || dev.Address == "" {
			return nil, fmt.Errorf("modbus: device needs name and address")
		}
		if _, dup := m.devices[dev.Name]; dup {
			return nil, fmt.Errorf("modbus: duplicate device %q", dev.Name)
		}
		addr := dev.Address
		if !strings.Contains(addr, ":") {
			addr += ":502"
		}
		dc := &deviceClient{
			dev:    dev,
			cli:    newClient(addr, time.Duration(dev.TimeoutMs)*time.Millisecond),
			byName: make(map[string]Register),
		}
		for _, reg := range dev.Registers {
			if reg.Name == "" {
				return nil, fmt.Errorf("modbus: device %q has a register with no name", dev.Name)
			}
			if _, err := reg.wordCount(); !reg.isBool() && err != nil {
				return nil, err
			}
			if _, dup := dc.byName[reg.Name]; dup {
				return nil, fmt.Errorf("modbus: device %q duplicate register %q", dev.Name, reg.Name)
			}
			dc.byName[reg.Name] = reg
			m.signals[dev.Name+":"+reg.Name] = signalRef{device: dev.Name, reg: reg}
		}
		m.devices[dev.Name] = dc
	}
	return m, nil
}

// Name implements datasource.DataSource.
func (m *Modbus) Name() string { return "modbus" }

// Connect is a no-op; TCP connections are dialled lazily per device.
func (m *Modbus) Connect(_ context.Context) error { return nil }

// ListSignals returns metadata for every configured register.
func (m *Modbus) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
	out := make([]datasource.Metadata, 0, len(m.signals))
	for _, ref := range m.signals {
		out = append(out, ref.reg.metadata(ref.device))
	}
	return out, nil
}

// GetMetadata returns metadata for one signal.
func (m *Modbus) GetMetadata(_ context.Context, signal string) (datasource.Metadata, error) {
	ref, ok := m.signals[signal]
	if !ok {
		return datasource.Metadata{}, datasource.ErrNotFound
	}
	return ref.reg.metadata(ref.device), nil
}

// readSignal performs one synchronous read of a register's current value.
func (m *Modbus) readSignal(ref signalRef) (datasource.Value, error) {
	dc := m.devices[ref.device]
	reg := ref.reg
	now := time.Now()

	if reg.isBool() {
		fc := byte(fcReadCoils)
		if reg.kind() == kindDiscrete {
			fc = fcReadDiscrete
		}
		bits, err := dc.cli.readBits(dc.dev.UnitID, fc, reg.Address, 1)
		if err != nil {
			return datasource.Value{}, err
		}
		return datasource.Value{Timestamp: now, Data: bits[0], Quality: datasource.QualityGood}, nil
	}

	count, err := reg.wordCount()
	if err != nil {
		return datasource.Value{}, err
	}
	fc := byte(fcReadHolding)
	if reg.kind() == kindInput {
		fc = fcReadInput
	}
	words, err := dc.cli.readRegisters(dc.dev.UnitID, fc, reg.Address, uint16(count))
	if err != nil {
		return datasource.Value{}, err
	}
	data, err := reg.decode(words)
	if err != nil {
		return datasource.Value{}, err
	}
	return datasource.Value{Timestamp: now, Data: data, Quality: datasource.QualityGood}, nil
}

// Subscribe polls the register at the configured interval and pushes values
// into ch. On a read error a QualityBad value is emitted and polling continues.
func (m *Modbus) Subscribe(ctx context.Context, signal string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
	ref, ok := m.signals[signal]
	if !ok {
		return nil, datasource.ErrNotFound
	}

	ctx, cancel := context.WithCancel(ctx)
	go func() {
		ticker := time.NewTicker(m.pollInterval)
		defer ticker.Stop()
		emit := func() {
			v, err := m.readSignal(ref)
			if err != nil {
				v = datasource.Value{Timestamp: time.Now(), Quality: datasource.QualityBad}
			}
			select {
			case ch <- v:
			case <-ctx.Done():
			}
		}
		emit() // first reading without waiting a full interval
		for {
			select {
			case <-ticker.C:
				emit()
			case <-ctx.Done():
				return
			}
		}
	}()
	return datasource.CancelFunc(cancel), nil
}

// Write sets a writable holding register or coil.
func (m *Modbus) Write(_ context.Context, signal string, value any) error {
	ref, ok := m.signals[signal]
	if !ok {
		return datasource.ErrNotFound
	}
	reg := ref.reg
	if !reg.writable() {
		return datasource.ErrNotWritable
	}
	dc := m.devices[ref.device]

	if reg.isBool() {
		on, err := toBool(value)
		if err != nil {
			return err
		}
		return dc.cli.writeSingleCoil(dc.dev.UnitID, reg.Address, on)
	}

	f, err := toFloat(value)
	if err != nil {
		return err
	}
	words, err := reg.encode(f)
	if err != nil {
		return err
	}
	if len(words) == 1 {
		return dc.cli.writeSingleRegister(dc.dev.UnitID, reg.Address, words[0])
	}
	return dc.cli.writeMultipleRegisters(dc.dev.UnitID, reg.Address, words)
}

// History is unavailable for Modbus devices.
func (m *Modbus) History(_ context.Context, _ string, _, _ time.Time, _ int) ([]datasource.Value, error) {
	return nil, datasource.ErrHistoryUnavailable
}

// Close tears down every device connection.
func (m *Modbus) Close() {
	for _, dc := range m.devices {
		dc.cli.close()
	}
}

func toFloat(v any) (float64, error) {
	switch x := v.(type) {
	case float64:
		return x, nil
	case float32:
		return float64(x), nil
	case int:
		return float64(x), nil
	case int64:
		return float64(x), nil
	case bool:
		if x {
			return 1, nil
		}
		return 0, nil
	default:
		return 0, fmt.Errorf("modbus: cannot write value of type %T", v)
	}
}

func toBool(v any) (bool, error) {
	switch x := v.(type) {
	case bool:
		return x, nil
	case float64:
		return x != 0, nil
	case int:
		return x != 0, nil
	case int64:
		return x != 0, nil
	default:
		return false, fmt.Errorf("modbus: cannot write bool value of type %T", v)
	}
}
