package udp

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

// WSJT-X / JTDX / MSHV UDP protocol (WSJT-X v2 schema).
//
// Wire format:
//   uint32 magic   (0xadbccbda)
//   uint32 schema  (2 or 3)
//   uint32 type    (message id)
//   QString id     (the program's "id" — typically "WSJT-X")
//   ... type-specific payload ...
//
// QString = int32 length followed by `length` UTF-8 bytes, or -1 for nil.
// QUtf8 in newer versions; same wire format for the common case.
//
// We only care about two messages here:
//   Status   (type 1) → exposes the current DX call so HamLog can pre-fill
//   LoggedADIF (type 12) → carries the ADIF of the just-logged QSO
// Everything else (heartbeat, decodes, clears, status of other VFOs) is
// ignored.

const (
	wsjtMagic = 0xadbccbda

	wsjtMsgHeartbeat  = 0
	wsjtMsgStatus     = 1
	wsjtMsgDecode     = 2
	wsjtMsgClear      = 3
	wsjtMsgQSOLogged  = 5
	wsjtMsgLoggedADIF = 12
)

// WSJTEvent is the parsed, typed result of decoding a single packet.
// One of (DXCall, LoggedADIF, DecodeCall) is non-empty depending on the message.
type WSJTEvent struct {
	DXCall      string // current "DX Call" field in the WSJT app (Status)
	DXGrid      string // optional grid for that call (Status)
	Mode        string // FT8 / FT4 / …
	FreqHz      int64  // current dial freq when available (Status)
	LoggedADIF  string // full ADIF text when message is LoggedADIF
	ProgramID   string // "WSJT-X" / "JTDX" / "MSHV" — for diagnostics / dedup

	// Decode (type 2): the transmitting station heard on the band. FreqHz is NOT
	// set here (Decode carries only the audio offset); the caller adds the last
	// known dial frequency (from Status) to DeltaFreqHz to get the RF frequency.
	IsDecode    bool
	DecodeCall  string // the sender (DE) callsign extracted from the message text
	DeltaFreqHz int64  // audio offset within the passband (Hz)
	SNR         int    // reported signal-to-noise (dB)
	IsCQ        bool   // the decode was a CQ call
}

// ParseWSJT decodes one UDP packet. Returns ok=false for messages we
// don't care about (heartbeat, clears, etc.).
func ParseWSJT(pkt []byte) (WSJTEvent, bool, error) {
	if len(pkt) < 12 {
		return WSJTEvent{}, false, fmt.Errorf("packet too short")
	}
	r := bytes.NewReader(pkt)
	var magic, schema, mtype uint32
	if err := binary.Read(r, binary.BigEndian, &magic); err != nil {
		return WSJTEvent{}, false, err
	}
	if magic != wsjtMagic {
		return WSJTEvent{}, false, fmt.Errorf("bad magic %#x", magic)
	}
	if err := binary.Read(r, binary.BigEndian, &schema); err != nil {
		return WSJTEvent{}, false, err
	}
	_ = schema
	if err := binary.Read(r, binary.BigEndian, &mtype); err != nil {
		return WSJTEvent{}, false, err
	}
	id, err := readQString(r)
	if err != nil {
		return WSJTEvent{}, false, fmt.Errorf("read id: %w", err)
	}

	ev := WSJTEvent{ProgramID: id}
	switch mtype {
	case wsjtMsgStatus:
		// Status payload order (v2):
		//   quint64 dial_frequency
		//   QUtf8   mode
		//   QUtf8   dx_call
		//   QUtf8   report
		//   QUtf8   tx_mode
		//   bool    tx_enabled
		//   bool    transmitting
		//   bool    decoding
		//   qint32  rx_df
		//   qint32  tx_df
		//   QUtf8   de_call
		//   QUtf8   de_grid
		//   QUtf8   dx_grid
		//   ... (more fields appended in later schemas, we stop reading
		//        after dx_grid which is all we need)
		var dialHz uint64
		if err := binary.Read(r, binary.BigEndian, &dialHz); err != nil {
			return WSJTEvent{}, false, err
		}
		ev.FreqHz = int64(dialHz)
		mode, err := readQString(r)
		if err != nil {
			return WSJTEvent{}, false, err
		}
		ev.Mode = strings.ToUpper(strings.TrimSpace(mode))
		dxCall, err := readQString(r)
		if err != nil {
			return WSJTEvent{}, false, err
		}
		ev.DXCall = strings.ToUpper(strings.TrimSpace(dxCall))
		// Skip report, tx_mode (QUtf8), tx_enabled (bool), transmitting,
		// decoding, rx_df (qint32), tx_df (qint32), de_call (QUtf8),
		// de_grid (QUtf8) → then dx_grid.
		for _, name := range []string{"report", "tx_mode"} {
			if _, err := readQString(r); err != nil {
				return ev, true, fmt.Errorf("read %s: %w", name, err)
			}
		}
		// 3 booleans (each 1 byte)
		for i := 0; i < 3; i++ {
			var b uint8
			if err := binary.Read(r, binary.BigEndian, &b); err != nil {
				return ev, true, err
			}
		}
		// 2 int32
		var i32 int32
		for i := 0; i < 2; i++ {
			if err := binary.Read(r, binary.BigEndian, &i32); err != nil {
				return ev, true, err
			}
		}
		// de_call, de_grid, dx_grid
		if _, err := readQString(r); err != nil {
			return ev, true, err
		}
		if _, err := readQString(r); err != nil {
			return ev, true, err
		}
		dxGrid, err := readQString(r)
		if err != nil {
			return ev, true, err
		}
		ev.DXGrid = strings.ToUpper(strings.TrimSpace(dxGrid))
		return ev, true, nil

	case wsjtMsgDecode:
		// Decode payload (v2):
		//   bool    is_new
		//   quint32 time (ms since midnight)
		//   qint32  snr
		//   double  delta_time (seconds)
		//   quint32 delta_frequency (Hz, audio offset in the passband)
		//   QUtf8   mode
		//   QUtf8   message (the decoded text, e.g. "CQ K1ABC FN42")
		//   bool    low_confidence
		//   bool    off_air
		var b uint8
		if err := binary.Read(r, binary.BigEndian, &b); err != nil { // is_new
			return WSJTEvent{}, false, err
		}
		var t32, df uint32
		var snr int32
		if err := binary.Read(r, binary.BigEndian, &t32); err != nil { // time
			return WSJTEvent{}, false, err
		}
		if err := binary.Read(r, binary.BigEndian, &snr); err != nil {
			return WSJTEvent{}, false, err
		}
		var dt float64
		if err := binary.Read(r, binary.BigEndian, &dt); err != nil { // delta_time
			return WSJTEvent{}, false, err
		}
		if err := binary.Read(r, binary.BigEndian, &df); err != nil { // delta_frequency
			return WSJTEvent{}, false, err
		}
		mode, err := readQString(r)
		if err != nil {
			return WSJTEvent{}, false, err
		}
		msg, err := readQString(r)
		if err != nil {
			return WSJTEvent{}, false, err
		}
		call, isCQ := wsjtSender(msg)
		if call == "" {
			return WSJTEvent{}, false, nil // free-text / telemetry / unparseable → ignore
		}
		ev.IsDecode = true
		ev.DecodeCall = call
		ev.IsCQ = isCQ
		ev.DeltaFreqHz = int64(df)
		ev.SNR = int(snr)
		ev.Mode = strings.ToUpper(strings.TrimSpace(mode))
		return ev, true, nil

	case wsjtMsgLoggedADIF:
		// Payload: a single QString containing the ADIF record.
		adif, err := readQString(r)
		if err != nil {
			return WSJTEvent{}, false, err
		}
		ev.LoggedADIF = adif
		return ev, true, nil
	}
	return WSJTEvent{}, false, nil
}

// wsjtSender extracts the transmitting (DE) callsign from a WSJT-X message and
// whether it was a CQ. Grammar:
//
//	CQ [modifier] <de_call> [grid]      → de_call, isCQ=true
//	<to_call> <de_call> [report|…]      → de_call, isCQ=false
//
// Returns "" for free-text / telemetry / hashed-call messages we can't resolve.
func wsjtSender(message string) (call string, isCQ bool) {
	f := strings.Fields(strings.ToUpper(strings.TrimSpace(message)))
	if len(f) == 0 {
		return "", false
	}
	if f[0] == "CQ" {
		// Skip an optional modifier after CQ (DX / a region like NA / a zone like
		// 020) — it never looks like a callsign (no letter+digit mix).
		idx := 1
		if len(f) > 2 && !looksLikeCall(f[1]) {
			idx = 2
		}
		if idx < len(f) && looksLikeCall(f[idx]) {
			return f[idx], true
		}
		return "", true
	}
	// Standard exchange: the DE (sender) call is the second token.
	if len(f) >= 2 && looksLikeCall(f[1]) {
		return f[1], false
	}
	return "", false
}

// looksLikeCall is a loose callsign test: 3–12 chars of A–Z/0–9//, with at least
// one letter AND one digit. Rejects the fixed exchange tokens (RR73/RRR/73) that
// would otherwise pass.
func looksLikeCall(s string) bool {
	switch s {
	case "RR73", "RRR", "73":
		return false
	}
	if len(s) < 3 || len(s) > 12 {
		return false
	}
	hasL, hasD := false, false
	for i := 0; i < len(s); i++ {
		c := s[i]
		switch {
		case c >= 'A' && c <= 'Z':
			hasL = true
		case c >= '0' && c <= '9':
			hasD = true
		case c == '/':
		default:
			return false
		}
	}
	return hasL && hasD
}

// readQString reads a Qt QString as written by QDataStream: an int32 byte
// length (or -1 for null) followed by the UTF-8 bytes.
func readQString(r *bytes.Reader) (string, error) {
	var n int32
	if err := binary.Read(r, binary.BigEndian, &n); err != nil {
		return "", err
	}
	if n <= 0 {
		return "", nil
	}
	if int(n) > r.Len() {
		return "", fmt.Errorf("short string: want %d have %d", n, r.Len())
	}
	buf := make([]byte, n)
	if _, err := r.Read(buf); err != nil {
		return "", err
	}
	return string(buf), nil
}
