// Package astm implements ASTM E1381 low-level transport — the framing,
// checksum and ENQ/ACK handshake that wraps every ASTM E1394 record stream.
//
// Content parsing (E1394 records: H/P/O/R/L) is intentionally NOT done here;
// the agent forwards the assembled record text to the cloud and LabCore's
// AstmRecordParser does the content work.
package astm

import (
	"errors"
	"fmt"
	"io"
	"net"
	"strconv"
	"strings"
	"time"
)

// Control characters (ASTM E1381).
const (
	ENQ = 0x05
	ACK = 0x06
	NAK = 0x15
	STX = 0x02
	ETX = 0x03
	ETB = 0x17
	EOT = 0x04
	CR  = 0x0D
	LF  = 0x0A
)

// Tunables — generous defaults; the analyzer should never be left waiting.
var (
	ReadTimeout    = 15 * time.Second
	HandshakeRetry = 3
)

// Session represents one ASTM transmission: ENQ → frame(s) → EOT.
type Session struct {
	// Records holds the assembled ASTM E1394 record text (records joined by
	// CR, ETB-split frames already glued back together).
	Records string
	// PeerAddr is the remote address, when relevant (TCP).
	PeerAddr string
	// StartedAt is when the ENQ arrived.
	StartedAt time.Time
}

// Receive runs the receiver side of the ASTM E1381 protocol against a
// byte stream (TCP conn or serial port). It blocks until EOT is received or
// the connection drops. Returns the assembled session.
//
// Wire-level behavior:
//
//	<- ENQ                              establishment
//	-> ACK
//	<- STX FN text ETB|ETX C1 C2 CR LF  one or more frames
//	-> ACK (per frame)
//	<- EOT                              termination
//
// Frames carry up to one record terminated by CR; an intermediate ETB
// indicates a record continues in the next frame. Frame numbers cycle 1..7,0.
func Receive(rw io.ReadWriter) (*Session, error) {
	s := &Session{StartedAt: time.Now()}
	br := newByteReader(rw)

	// Wait for ENQ.
	b, err := br.readByte()
	if err != nil {
		return nil, fmt.Errorf("waiting for ENQ: %w", err)
	}
	if b == EOT {
		// Some devices send EOT immediately after a previous session — accept and continue waiting.
		b, err = br.readByte()
		if err != nil {
			return nil, fmt.Errorf("waiting for ENQ after stray EOT: %w", err)
		}
	}
	if b != ENQ {
		return nil, fmt.Errorf("expected ENQ (0x05), got 0x%02X", b)
	}
	if _, err := rw.Write([]byte{ACK}); err != nil {
		return nil, fmt.Errorf("send ACK to ENQ: %w", err)
	}

	// Read frames until EOT.
	var carry strings.Builder // accumulates a record across ETB-split frames
	for {
		b, err := br.readByte()
		if err != nil {
			return nil, fmt.Errorf("waiting for STX/EOT: %w", err)
		}
		switch b {
		case EOT:
			return s, nil
		case ENQ:
			// New establishment within the session — re-ACK.
			if _, err := rw.Write([]byte{ACK}); err != nil {
				return nil, fmt.Errorf("re-ACK: %w", err)
			}
			continue
		case STX:
			// fallthrough to frame parse below
		default:
			// Skip stray control or framing whitespace.
			continue
		}

		frame, isFinal, err := readFrame(br)
		if err != nil {
			// Bad frame — NAK and let the sender retransmit.
			_, _ = rw.Write([]byte{NAK})
			return nil, fmt.Errorf("read frame: %w", err)
		}
		// frame is the FN+text+ETB/ETX, with checksum already verified.
		// Strip the leading FN char.
		if len(frame) < 1 {
			_, _ = rw.Write([]byte{NAK})
			continue
		}
		text := frame[1 : len(frame)-1] // drop FN and trailing ETB/ETX
		carry.WriteString(text)
		if isFinal {
			// A full record (or set of records, the device may emit one frame
			// per record). Append a CR if the carry doesn't end with one.
			rec := carry.String()
			carry.Reset()
			if !strings.HasSuffix(rec, "\r") {
				rec += "\r"
			}
			s.Records += rec
		}
		if _, err := rw.Write([]byte{ACK}); err != nil {
			return nil, fmt.Errorf("ACK frame: %w", err)
		}
	}
}

// readFrame reads bytes up to ETB or ETX, validates the checksum, and returns
// the frame body (FN + text + terminator). isFinal=true means ETX (record
// complete), false means ETB (more frames continue this record).
func readFrame(br *byteReader) (body string, isFinal bool, err error) {
	var buf []byte
	for {
		b, err := br.readByte()
		if err != nil {
			return "", false, err
		}
		buf = append(buf, b)
		if b == ETX || b == ETB {
			break
		}
		if len(buf) > 64*1024 {
			return "", false, errors.New("frame exceeds 64KB")
		}
	}
	// Two-char ASCII hex checksum follows.
	hi, err := br.readByte()
	if err != nil {
		return "", false, fmt.Errorf("checksum hi: %w", err)
	}
	lo, err := br.readByte()
	if err != nil {
		return "", false, fmt.Errorf("checksum lo: %w", err)
	}
	want, err := strconv.ParseUint(string([]byte{hi, lo}), 16, 8)
	if err != nil {
		return "", false, fmt.Errorf("parse checksum %q: %w", string([]byte{hi, lo}), err)
	}
	got := checksum(buf)
	if byte(want) != got {
		return "", false, fmt.Errorf("checksum mismatch want=%02X got=%02X", want, got)
	}
	// Optional CR LF after checksum — consume and discard if present.
	if pk, err := br.peek(); err == nil && pk == CR {
		_, _ = br.readByte()
		if pk2, err := br.peek(); err == nil && pk2 == LF {
			_, _ = br.readByte()
		}
	}
	return string(buf), buf[len(buf)-1] == ETX, nil
}

// checksum is the sum of bytes from FN through ETB/ETX inclusive, mod 256.
func checksum(body []byte) byte {
	var sum int
	for _, b := range body {
		sum += int(b)
	}
	return byte(sum % 256)
}

// byteReader wraps an io.Reader, supporting one-byte peek and read-deadlines
// when the underlying reader is a net.Conn.
type byteReader struct {
	r    io.Reader
	peek []byte
}

func newByteReader(r io.Reader) *byteReader { return &byteReader{r: r} }

func (b *byteReader) readByte() (byte, error) {
	if len(b.peek) > 0 {
		x := b.peek[0]
		b.peek = b.peek[1:]
		return x, nil
	}
	if c, ok := b.r.(net.Conn); ok {
		_ = c.SetReadDeadline(time.Now().Add(ReadTimeout))
	}
	buf := make([]byte, 1)
	if _, err := io.ReadFull(b.r, buf); err != nil {
		return 0, err
	}
	return buf[0], nil
}

func (b *byteReader) peek() (byte, error) {
	if len(b.peek) > 0 {
		return b.peek[0], nil
	}
	x, err := b.readByte()
	if err != nil {
		return 0, err
	}
	b.peek = append(b.peek, x)
	return x, nil
}
