package main

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"sync/atomic"
	"time"

	mqtt "github.com/eclipse/paho.mqtt.golang"
)

// ── CRC-16 / Modbus ───────────────────────────────────────────────────────────

// crc16 computes the Modbus RTU CRC-16 (poly 0xA001, initial value 0xFFFF).
// The result must be appended as [low_byte, high_byte].
func crc16(data []byte) uint16 {
	crc := uint16(0xFFFF)
	for _, b := range data {
		crc ^= uint16(b)
		for range 8 {
			if crc&1 != 0 {
				crc = (crc >> 1) ^ 0xA001
			} else {
				crc >>= 1
			}
		}
	}
	return crc
}

// ── Frame building ────────────────────────────────────────────────────────────

// buildFC4Frame builds a Modbus RTU FC4 (Read Input Registers) request frame.
//
//	[slave, 0x04, regAddr_hi, regAddr_lo, count_hi, count_lo, crc_lo, crc_hi]
//
// regAddr and count are in register (word) units.
func buildFC4Frame(slave byte, regAddr, count uint16) []byte {
	frame := make([]byte, 6)
	frame[0] = slave
	frame[1] = 0x04
	binary.BigEndian.PutUint16(frame[2:], regAddr)
	binary.BigEndian.PutUint16(frame[4:], count)
	crc := crc16(frame)
	return append(frame, byte(crc), byte(crc>>8)) // little-endian CRC
}

// ── MQTT payload ──────────────────────────────────────────────────────────────

type requestPayload struct {
	Data struct {
		ModbusFrame struct {
			Value []int  `json:"value"`
			Unit  string `json:"unit"`
		} `json:"modbus_frame"`
	} `json:"data"`
	Timestamp struct {
		Incarnation int64 `json:"incarnation"`
		SystemTime  int64 `json:"system_time"`
	} `json:"timestamp"`
}

// ── Requester ─────────────────────────────────────────────────────────────────

// Requester publishes Modbus FC4 read requests to the MQTT broker.
// Topic pattern: <topicPrefix>/<segment>/0x<slave_hex>
type Requester struct {
	client      mqtt.Client
	topicPrefix string
	incarnation atomic.Int64
	startTime   time.Time
}

func NewRequester(client mqtt.Client, topicPrefix string) *Requester {
	return &Requester{
		client:      client,
		topicPrefix: topicPrefix,
		startTime:   time.Now(),
	}
}

// ReadInputRegisters sends an FC4 read request for `count` registers (words)
// starting at `regAddr` on the given slave within the named segment.
// count=2 reads one float32 value; use a multiple of 2 for more values.
func (r *Requester) ReadInputRegisters(segment string, slave byte, regAddr, count uint16) error {
	frame := buildFC4Frame(slave, regAddr, count)

	var p requestPayload
	p.Data.ModbusFrame.Unit = "RAW"
	p.Data.ModbusFrame.Value = bytesToInts(frame)
	p.Timestamp.Incarnation = r.incarnation.Add(1)
	p.Timestamp.SystemTime = time.Since(r.startTime).Nanoseconds()

	payload, err := json.Marshal(p)
	if err != nil {
		return fmt.Errorf("marshalling request: %w", err)
	}

	topic := fmt.Sprintf("%s/%s/0x%02x", r.topicPrefix, segment, slave)
	token := r.client.Publish(topic, 0, false, payload)
	token.Wait()
	return token.Error()
}

func bytesToInts(b []byte) []int {
	ints := make([]int, len(b))
	for i, v := range b {
		ints[i] = int(v)
	}
	return ints
}
