package udpsprotocol

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"math"
	"time"
)

// ─── Constants ───────────────────────────────────────────────────────────────

const (
	MagicUDPS uint32 = 0x53504455 // 'UDPS' little-endian

	PktData       uint8 = 0
	PktConfig     uint8 = 1
	PktACK        uint8 = 2
	PktConnect    uint8 = 3
	PktDisconnect uint8 = 4

	HeaderSize   = 17
	SigDescSize  = 136
	NoTimeSignal = uint32(0xFFFFFFFF)

	QuantNone   uint8 = 0
	QuantUint8  uint8 = 1
	QuantInt8   uint8 = 2
	QuantUint16 uint8 = 3
	QuantInt16  uint8 = 4

	// TimeMode values – must match UDPStreamerTimeMode enum in UDPStreamer.h
	TimeModePacket      uint8 = 0 // use wall-clock packet arrival time
	TimeModeFullArray   uint8 = 1 // TimeSignal has same N elements; not expanded here
	TimeModeFirstSample uint8 = 2 // TimeSignal scalar = time of element [0]
	TimeModeLastSample  uint8 = 3 // TimeSignal scalar = time of element [N-1]

	// PublishMode values – must match UDPStreamerPublishMode enum in UDPStreamer.h
	PublishModeStrict     uint8 = 0 // one packet per Synchronise() call
	PublishModeAccumulate uint8 = 1 // variable batch; DATA has [8 HRT][4 numSamples][signals...]
	PublishModeDecimate   uint8 = 2 // one packet every Ratio calls
)

// ─── Packet header (17 bytes, little-endian, packed) ─────────────────────────

type PacketHeader struct {
	Magic          uint32
	Type           uint8
	Counter        uint32
	FragmentIdx    uint16
	TotalFragments uint16
	PayloadBytes   uint32
}

// ParseHeader decodes exactly HeaderSize bytes into a PacketHeader.
func ParseHeader(b []byte) (PacketHeader, error) {
	if len(b) < HeaderSize {
		return PacketHeader{}, fmt.Errorf("header too short: %d bytes", len(b))
	}
	var h PacketHeader
	r := bytes.NewReader(b[:HeaderSize])
	if err := binary.Read(r, binary.LittleEndian, &h); err != nil {
		return PacketHeader{}, err
	}
	if h.Magic != MagicUDPS {
		return PacketHeader{}, fmt.Errorf("bad magic: 0x%08X", h.Magic)
	}
	return h, nil
}

// buildHeader serialises a PacketHeader to a 17-byte slice.
func buildHeader(h PacketHeader) []byte {
	buf := new(bytes.Buffer)
	_ = binary.Write(buf, binary.LittleEndian, h)
	return buf.Bytes()
}

// BuildConnectPacket returns a 17-byte CONNECT datagram.
func BuildConnectPacket() []byte {
	return buildHeader(PacketHeader{
		Magic:          MagicUDPS,
		Type:           PktConnect,
		Counter:        0,
		FragmentIdx:    0,
		TotalFragments: 1,
		PayloadBytes:   0,
	})
}

// BuildDisconnectPacket returns a 17-byte DISCONNECT datagram.
func BuildDisconnectPacket() []byte {
	return buildHeader(PacketHeader{
		Magic:          MagicUDPS,
		Type:           PktDisconnect,
		Counter:        0,
		FragmentIdx:    0,
		TotalFragments: 1,
		PayloadBytes:   0,
	})
}

// ─── Signal descriptor (136 bytes) ───────────────────────────────────────────

// SignalInfo holds the parsed metadata for one signal.
type SignalInfo struct {
	Name          string  `json:"name"`
	TypeCode      uint8   `json:"typeCode"`
	QuantType     uint8   `json:"quantType"`
	NumDimensions uint8   `json:"numDimensions"`
	NumRows       uint32  `json:"numRows"`
	NumCols       uint32  `json:"numCols"`
	RangeMin      float64 `json:"rangeMin"`
	RangeMax      float64 `json:"rangeMax"`
	TimeMode      uint8   `json:"timeMode"`
	SamplingRate  float64 `json:"samplingRate"`
	TimeSignalIdx uint32  `json:"timeSignalIdx"`
	Unit          string  `json:"unit"`
}

// NumElements returns the total number of scalar values in one sample of this signal.
func (s SignalInfo) NumElements() int {
	r := int(s.NumRows)
	c := int(s.NumCols)
	if r == 0 {
		r = 1
	}
	if c == 0 {
		c = 1
	}
	/* HI-2: cap at 1M to prevent integer overflow / OOM from crafted packets */
	n := r * c
	if n < 0 || n > 1024*1024 {
		return 1024 * 1024
	}
	return n
}

// rawTypeSize returns the byte size for one element of the raw (unquantised) type.
func rawTypeSize(typeCode uint8) int {
	switch typeCode {
	case 0, 1: // uint8, int8
		return 1
	case 2, 3: // uint16, int16
		return 2
	case 4, 5: // uint32, int32
		return 4
	case 6, 7: // uint64, int64
		return 8
	case 8: // float32
		return 4
	case 9: // float64
		return 8
	default:
		return 1
	}
}

// quantSize returns the byte size of one quantised element.
func quantSize(qt uint8) int {
	switch qt {
	case QuantUint8, QuantInt8:
		return 1
	case QuantUint16, QuantInt16:
		return 2
	default:
		return 0
	}
}

// readRawElement reads one element at offset and converts it to float64.
func readRawElement(b []byte, offset int, typeCode uint8) float64 {
	switch typeCode {
	case 0:
		return float64(b[offset])
	case 1:
		return float64(int8(b[offset]))
	case 2:
		return float64(binary.LittleEndian.Uint16(b[offset:]))
	case 3:
		return float64(int16(binary.LittleEndian.Uint16(b[offset:])))
	case 4:
		return float64(binary.LittleEndian.Uint32(b[offset:]))
	case 5:
		return float64(int32(binary.LittleEndian.Uint32(b[offset:])))
	case 6:
		return float64(binary.LittleEndian.Uint64(b[offset:]))
	case 7:
		return float64(int64(binary.LittleEndian.Uint64(b[offset:])))
	case 8:
		bits := binary.LittleEndian.Uint32(b[offset:])
		return float64(math.Float32frombits(bits))
	case 9:
		bits := binary.LittleEndian.Uint64(b[offset:])
		return math.Float64frombits(bits)
	default:
		return 0
	}
}

// dequantise converts a raw quantised integer to a physical float64.
func dequantise(qt uint8, raw uint16, rangeMin, rangeMax float64) float64 {
	span := rangeMax - rangeMin
	switch qt {
	case QuantUint8:
		return rangeMin + (float64(uint8(raw))/255.0)*span
	case QuantInt8:
		return rangeMin + (float64(int8(raw)+127)/254.0)*span
	case QuantUint16:
		return rangeMin + (float64(raw)/65535.0)*span
	case QuantInt16:
		return rangeMin + (float64(int16(raw)+32767)/65534.0)*span
	default:
		return 0
	}
}

// nullTermString converts a zero-padded byte slice to a Go string.
func nullTermString(b []byte) string {
	n := bytes.IndexByte(b, 0)
	if n < 0 {
		return string(b)
	}
	return string(b[:n])
}

// ─── CONFIG payload parser ────────────────────────────────────────────────────

// ParseConfig decodes a fully-reassembled CONFIG payload.
// Returns the signal list, the publishing mode byte (PublishMode*), and any error.
func ParseConfig(payload []byte) ([]SignalInfo, uint8, error) {
	if len(payload) < 4 {
		return nil, 0, fmt.Errorf("config payload too short")
	}
	numSigs := binary.LittleEndian.Uint32(payload[0:4])
	/* HI-2: validate numSigs against payload length before allocating */
	maxSigs := uint32(len(payload) / SigDescSize)
	if numSigs > maxSigs {
		return nil, 0, fmt.Errorf("config claims %d signals but payload can hold at most %d", numSigs, maxSigs)
	}
	offset := 4
	sigs := make([]SignalInfo, 0, numSigs)
	for i := uint32(0); i < numSigs; i++ {
		if offset+SigDescSize > len(payload) {
			return nil, 0, fmt.Errorf("config payload truncated at signal %d", i)
		}
		raw := payload[offset : offset+SigDescSize]
		si := SignalInfo{
			Name:          nullTermString(raw[0:64]),
			TypeCode:      raw[64],
			QuantType:     raw[65],
			NumDimensions: raw[66],
			NumRows:       binary.LittleEndian.Uint32(raw[67:71]),
			NumCols:       binary.LittleEndian.Uint32(raw[71:75]),
			RangeMin:      math.Float64frombits(binary.LittleEndian.Uint64(raw[75:83])),
			RangeMax:      math.Float64frombits(binary.LittleEndian.Uint64(raw[83:91])),
			TimeMode:      raw[91],
			SamplingRate:  math.Float64frombits(binary.LittleEndian.Uint64(raw[92:100])),
			TimeSignalIdx: binary.LittleEndian.Uint32(raw[100:104]),
			Unit:          nullTermString(raw[104:136]),
		}
		sigs = append(sigs, si)
		offset += SigDescSize
	}
	// Trailing publish-mode byte (added after signal descriptors).
	publishMode := PublishModeStrict
	if offset < len(payload) {
		publishMode = payload[offset]
	}
	return sigs, publishMode, nil
}

// ─── DATA payload parser ──────────────────────────────────────────────────────

// DataSample holds the decoded values from one DATA packet.
type DataSample struct {
	HRTTimestamp uint64
	WallTime     time.Time            // wall-clock time at UDP arrival; used as x-axis
	Values       map[string][]float64 // key = signal name, value = []float64 with NumElements entries
}

// parseElems reads n elements for sig from payload at offset, advancing offset.
// Returns the slice of float64 values and the new offset.
func parseElems(payload []byte, offset, n int, sig SignalInfo) ([]float64, int, error) {
	elems := make([]float64, n)
	if sig.QuantType == QuantNone {
		sz := rawTypeSize(sig.TypeCode)
		needed := n * sz
		if offset+needed > len(payload) {
			return nil, offset, fmt.Errorf("data payload truncated for signal %q", sig.Name)
		}
		for i := 0; i < n; i++ {
			elems[i] = readRawElement(payload, offset+i*sz, sig.TypeCode)
		}
		offset += needed
	} else {
		sz := quantSize(sig.QuantType)
		needed := n * sz
		if offset+needed > len(payload) {
			return nil, offset, fmt.Errorf("data payload truncated (quant) for signal %q", sig.Name)
		}
		for i := 0; i < n; i++ {
			var raw uint16
			if sz == 1 {
				raw = uint16(payload[offset+i])
			} else {
				raw = binary.LittleEndian.Uint16(payload[offset+i*2:])
			}
			elems[i] = dequantise(sig.QuantType, raw, sig.RangeMin, sig.RangeMax)
		}
		offset += needed
	}
	return elems, offset, nil
}

// ParseData decodes a fully-reassembled DATA payload using the provided signal config
// and publishing mode. arrivalTime is the wall-clock time at which the packet arrived.
//
// For PublishModeAccumulate the payload format is:
//
//	[8 HRT][4 numSamples][for each signal: accumulated scalars → numSamples elems; arrays → NumElements elems]
//
// The function returns one DataSample per accumulated snapshot so the hub can
// process each slot independently with its own timestamp.
func ParseData(payload []byte, sigs []SignalInfo, publishMode uint8, arrivalTime time.Time) ([]DataSample, error) {
	if len(payload) < 8 {
		return nil, fmt.Errorf("data payload too short")
	}
	hrt := binary.LittleEndian.Uint64(payload[0:8])
	offset := 8

	if publishMode == PublishModeAccumulate {
		if len(payload) < 12 {
			return nil, fmt.Errorf("accumulate data payload too short (missing numSamples)")
		}
		numSamples := int(binary.LittleEndian.Uint32(payload[8:12]))
		offset = 12
		if numSamples == 0 {
			return []DataSample{}, nil
		}
		/* HI-2: sanity-cap numSamples to prevent OOM from crafted packets */
		if numSamples < 0 || numSamples > 1024*1024 {
			return nil, fmt.Errorf("accumulate numSamples %d out of range", numSamples)
		}

		// Parse per-signal data blocks (all slots for a signal are contiguous).
		accumVals := make(map[string][]float64, len(sigs)) // scalars: numSamples values
		fixedVals := make(map[string][]float64, len(sigs)) // arrays: NumElements values

		for _, sig := range sigs {
			n := sig.NumElements()
			if n == 1 {
				// Accumulated scalar: read numSamples back-to-back elements.
				elems, newOff, err := parseElems(payload, offset, numSamples, sig)
				if err != nil {
					return nil, err
				}
				offset = newOff
				accumVals[sig.Name] = elems
			} else {
				// Fixed array (non-accumulated): one set of NumElements values.
				elems, newOff, err := parseElems(payload, offset, n, sig)
				if err != nil {
					return nil, err
				}
				offset = newOff
				fixedVals[sig.Name] = elems
			}
		}

		// Build one DataSample per slot.
		samples := make([]DataSample, numSamples)
		for k := 0; k < numSamples; k++ {
			vals := make(map[string][]float64, len(sigs))
			for sigName, av := range accumVals {
				vals[sigName] = []float64{av[k]}
			}
			for sigName, fv := range fixedVals {
				vals[sigName] = fv // shared read-only reference; hub does not modify
			}
			samples[k] = DataSample{HRTTimestamp: hrt, WallTime: arrivalTime, Values: vals}
		}
		return samples, nil
	}

	// Strict / Decimate: single snapshot, one element set per signal.
	vals := make(map[string][]float64, len(sigs))
	for _, sig := range sigs {
		n := sig.NumElements()
		elems, newOff, err := parseElems(payload, offset, n, sig)
		if err != nil {
			return nil, err
		}
		offset = newOff
		vals[sig.Name] = elems
	}
	return []DataSample{{HRTTimestamp: hrt, WallTime: arrivalTime, Values: vals}}, nil
}
