package modbus

import (
	"fmt"
	"strings"

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

// Config is the [datasource.modbus] section. Devices share no connection state;
// each is polled independently over its own TCP socket.
type Config struct {
	Enabled bool `toml:"enabled"`
	// PollIntervalMs is the default polling period for every register that does
	// not override it. Zero → 1000 ms.
	PollIntervalMs int      `toml:"poll_interval_ms"`
	Devices        []Device `toml:"devices"`
}

// Device is one Modbus TCP slave. Address is "host:port" (default port 502 is
// appended if absent). UnitID is the Modbus unit/slave identifier (0–255).
type Device struct {
	Name      string     `toml:"name"`
	Address   string     `toml:"address"`
	UnitID    uint8      `toml:"unit_id"`
	TimeoutMs int        `toml:"timeout_ms"`
	Registers []Register `toml:"registers"`
}

// Register describes one logical signal mapped onto a Modbus address.
//
//   - Kind selects the address space / function code:
//     "holding" (FC03/06/10), "input" (FC04, read-only),
//     "coil" (FC01/05, bool), "discrete" (FC02, read-only bool).
//   - Encoding selects how holding/input words are decoded:
//     "uint16", "int16", "uint32", "int32", "float32", "float64".
//     Ignored for coil/discrete (always bool).
//   - WordOrder is "big" (default, high word first) or "little" for the
//     multi-word encodings.
//   - Scale/Offset transform the raw numeric value: value*Scale + Offset.
//     Scale 0 is treated as 1.
type Register struct {
	Name        string  `toml:"name"`
	Kind        string  `toml:"kind"`
	Address     uint16  `toml:"address"`
	Encoding    string  `toml:"encoding"`
	WordOrder   string  `toml:"word_order"`
	Unit        string  `toml:"unit"`
	Scale       float64 `toml:"scale"`
	Offset      float64 `toml:"offset"`
	Min         float64 `toml:"min"`
	Max         float64 `toml:"max"`
	Writable    bool    `toml:"writable"`
	Description string  `toml:"description"`
}

// register kinds.
const (
	kindHolding  = "holding"
	kindInput    = "input"
	kindCoil     = "coil"
	kindDiscrete = "discrete"
)

// isBool reports whether the register addresses a single-bit space.
func (r Register) isBool() bool {
	return r.kind() == kindCoil || r.kind() == kindDiscrete
}

func (r Register) kind() string {
	if r.Kind == "" {
		return kindHolding
	}
	return strings.ToLower(r.Kind)
}

func (r Register) encoding() string {
	if r.Encoding == "" {
		return "uint16"
	}
	return strings.ToLower(r.Encoding)
}

func (r Register) littleWordOrder() bool {
	return strings.ToLower(r.WordOrder) == "little"
}

func (r Register) scale() float64 {
	if r.Scale == 0 {
		return 1
	}
	return r.Scale
}

// wordCount returns the number of 16-bit registers the encoding occupies.
func (r Register) wordCount() (int, error) {
	switch r.encoding() {
	case "uint16", "int16":
		return 1, nil
	case "uint32", "int32", "float32":
		return 2, nil
	case "float64":
		return 4, nil
	default:
		return 0, fmt.Errorf("modbus: unknown encoding %q", r.Encoding)
	}
}

// dataType maps the register to a datasource value type.
func (r Register) dataType() datasource.DataType {
	if r.isBool() {
		return datasource.TypeBool
	}
	// Integer encodings with no fractional scaling stay integers; anything
	// scaled, offset, or float-encoded becomes a float64.
	switch r.encoding() {
	case "float32", "float64":
		return datasource.TypeFloat64
	default:
		if r.scale() == 1 && r.Offset == 0 {
			return datasource.TypeInt64
		}
		return datasource.TypeFloat64
	}
}

// writable reports whether writes are permitted. Input registers and discrete
// inputs are read-only regardless of the Writable flag.
func (r Register) writable() bool {
	switch r.kind() {
	case kindInput, kindDiscrete:
		return false
	default:
		return r.Writable
	}
}

// metadata builds the datasource.Metadata for this register under signal name
// "device:register".
func (r Register) metadata(device string) datasource.Metadata {
	return datasource.Metadata{
		Name:        device + ":" + r.Name,
		Type:        r.dataType(),
		Unit:        r.Unit,
		Description: r.Description,
		DisplayLow:  r.Min,
		DisplayHigh: r.Max,
		DriveLow:    r.Min,
		DriveHigh:   r.Max,
		Writable:    r.writable(),
	}
}
