// Package pva implements an EPICS PV Access (PVA) client in pure Go.
//
// PVA is the modern EPICS network protocol introduced in EPICS 7, replacing
// Channel Access (CA) for structured/typed data.  This package implements
// the client side of the PVA TCP protocol sufficient for GET and MONITOR
// operations on NTScalar and NTScalarArray normative types.
//
// Reference: EPICS PVAccess Protocol Specification r1.1
// https://epics-pvdata.sourceforge.net/pvAccess.html
package pva

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
)

// ---- Message flags ----------------------------------------------------

const (
	flagApp        byte = 0x00 // application message (vs control)
	flagControl    byte = 0x01 // control message
	flagSegFirst   byte = 0x08 // first segment
	flagSegLast    byte = 0x10 // last segment
	flagSegMid     byte = 0x18 // middle segment
	flagFromServer byte = 0x40 // direction: server→client
	flagBigEndian  byte = 0x80 // byte order: big-endian (we use little-endian)
)

// ---- Application message command codes --------------------------------

const (
	CmdBeacon          byte = 0x00
	CmdConnectionValid byte = 0x01
	CmdEcho            byte = 0x02
	CmdSearchRequest   byte = 0x03
	CmdSearchResponse  byte = 0x04
	CmdAuthNZ          byte = 0x05 // authentication/authorisation
	CmdAclChange       byte = 0x06
	CmdCreateChannel   byte = 0x07
	CmdDestroyChannel  byte = 0x08
	CmdConnectionReq   byte = 0x09
	CmdGet             byte = 0x0A
	CmdPut             byte = 0x0B
	CmdPutGet          byte = 0x0C
	CmdMonitor         byte = 0x0D
	CmdArray           byte = 0x0E
	CmdDestroyReq      byte = 0x0F
	CmdProcess         byte = 0x10
	CmdGetField        byte = 0x11
	CmdMessage         byte = 0x12 // server status/warning
	CmdMultipleData    byte = 0x13
	CmdRpcCall         byte = 0x14
	CmdCancelRequest   byte = 0x15
	CmdOriginTag       byte = 0x16
)

// ---- Control message sub-commands -------------------------------------

const (
	CtrlSetMarker    byte = 0x00
	CtrlAckMarker    byte = 0x01
	CtrlSetByteOrder byte = 0x02
	CtrlEchoRequest  byte = 0x03
	CtrlEchoResponse byte = 0x04
)

// ---- Status codes -----------------------------------------------------

const (
	StatusOK     byte = 0xFF // special "OK" short encoding
	StatusOKFull byte = 0x00 // full OK message (type=OK, msg="")
	StatusWarn   byte = 0x01
	StatusError  byte = 0x02
	StatusFatal  byte = 0x03
)

// ---- Request sub-command bits -----------------------------------------

const (
	SubCmdInit     byte = 0x08 // initialise request (send FieldDesc)
	SubCmdGet      byte = 0x40 // GET sub-command
	SubCmdPipeline byte = 0x80 // pipeline (for monitor)
	SubCmdDEstroy  byte = 0x10 // destroy request
	SubCmdProcess  byte = 0x04
)

// ---- Header -----------------------------------------------------------

// PVAHeader is the 8-byte fixed header on every PVA message.
//
//	byte  0   : magic 0xCA
//	byte  1   : protocol version (0x01)
//	byte  2   : flags (see flag* constants)
//	byte  3   : command code
//	bytes 4–7 : payload size (uint32, matches byte-order flag)
type PVAHeader struct {
	Version byte
	Flags   byte
	Command byte
	Size    uint32
}

const magic byte = 0xCA
const headerSize = 8

func (h PVAHeader) isLittleEndian() bool { return h.Flags&flagBigEndian == 0 }
func (h PVAHeader) isFromServer() bool   { return h.Flags&flagFromServer != 0 }
func (h PVAHeader) isControl() bool      { return h.Flags&flagControl != 0 }

// byteOrder returns the binary.ByteOrder matching the header flags.
func (h PVAHeader) byteOrder() binary.ByteOrder {
	if h.isLittleEndian() {
		return binary.LittleEndian
	}
	return binary.BigEndian
}

// ReadHeader reads an 8-byte PVA message header from r.
func ReadHeader(r io.Reader) (PVAHeader, error) {
	var raw [headerSize]byte
	if _, err := io.ReadFull(r, raw[:]); err != nil {
		return PVAHeader{}, err
	}
	if raw[0] != magic {
		return PVAHeader{}, fmt.Errorf("pva: bad magic 0x%02X (expected 0xCA)", raw[0])
	}
	h := PVAHeader{
		Version: raw[1],
		Flags:   raw[2],
		Command: raw[3],
	}
	bo := h.byteOrder()
	h.Size = bo.Uint32(raw[4:8])
	return h, nil
}

// WriteHeader encodes h and writes the 8-byte header to w.
// Always writes in little-endian (client sends LE after SetByteOrder).
func WriteHeader(w io.Writer, h PVAHeader) error {
	raw := [headerSize]byte{
		magic,
		h.Version,
		h.Flags,
		h.Command,
	}
	binary.LittleEndian.PutUint32(raw[4:8], h.Size)
	_, err := w.Write(raw[:])
	return err
}

// BuildMessage assembles a complete PVA message (header + payload).
func BuildMessage(cmd byte, flags byte, payload []byte) []byte {
	var buf bytes.Buffer
	_ = WriteHeader(&buf, PVAHeader{
		Version: 1,
		Flags:   flags & ^flagFromServer, // clear server bit — this is client
		Command: cmd,
		Size:    uint32(len(payload)),
	})
	buf.Write(payload)
	return buf.Bytes()
}

// ---- Status decoding ---------------------------------------------------

// Status is a decoded PVA status message embedded in many response types.
type Status struct {
	Type    byte
	Message string
	Stack   string
}

// OK reports whether the status is success.
func (s Status) OK() bool { return s.Type == StatusOKFull || s.Type == StatusOK }

// Error implements the error interface so Status can be returned as error.
func (s Status) Error() string {
	if s.OK() {
		return ""
	}
	return fmt.Sprintf("pva status %d: %s", s.Type, s.Message)
}

// ReadStatus reads a PVA status from r.
// The short form (0xFF = OK) is handled transparently.
func ReadStatus(r io.Reader, bo binary.ByteOrder) (Status, error) {
	var b [1]byte
	if _, err := io.ReadFull(r, b[:]); err != nil {
		return Status{}, err
	}
	if b[0] == StatusOK {
		return Status{Type: StatusOKFull}, nil
	}
	typ := b[0]
	msg, err := readPVAString(r, bo)
	if err != nil {
		return Status{}, err
	}
	stack, err := readPVAString(r, bo)
	if err != nil {
		return Status{}, err
	}
	return Status{Type: typ, Message: msg, Stack: stack}, nil
}

// readPVAString reads a PVA length-prefixed string.
// PVA uses compact size encoding (same as pvdata package).
func readPVAString(r io.Reader, _ binary.ByteOrder) (string, error) {
	// delegate to pvdata compact-size reader
	var b [1]byte
	if _, err := io.ReadFull(r, b[:]); err != nil {
		return "", err
	}
	n := int(b[0])
	if n == 0xFF {
		var v int32
		if err := binary.Read(r, binary.LittleEndian, &v); err != nil {
			return "", err
		}
		n = int(v)
	}
	if n <= 0 {
		return "", nil
	}
	buf := make([]byte, n)
	if _, err := io.ReadFull(r, buf); err != nil {
		return "", err
	}
	return string(buf), nil
}
