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
	DecodeGrid  string // 4-char grid, CQ decodes only — the exchange carries none
	DeltaFreqHz int64  // audio offset within the passband (Hz)
	SNR         int    // reported signal-to-noise (dB)
	IsCQ        bool   // the decode was a CQ call
}

// maxFwdHeader bounds how far into a packet the WSJT-X magic may sit behind a
// forwarder's header. The one seen in the field ("127.0.0.1:2237|") is 15 bytes;
// 64 leaves room for a longer address without ever scanning a real payload.
const maxFwdHeader = 64

// stripForwarderHeader removes the origin header a UDP relay prepends.
//
// A relay that re-broadcasts WSJT-X traffic has to say where each datagram came
// from, and it does so as plain text in front of the payload:
//
//	"127.0.0.1:2237|" + <the original, untouched WSJT-X packet>
//
// The magic then sits 15 bytes in, every packet fails on "bad magic", and an
// operator running MSHV behind such a relay gets nothing at all. There is no
// need for a separate service type: what follows the header IS a WSJT-X packet,
// so the whole parser and everything downstream apply unchanged.
//
// Deliberately narrow. The magic must appear within maxFwdHeader bytes AND
// everything before it must be printable ASCII — a truncated or corrupt packet
// that happens to contain those four bytes somewhere is not resurrected into a
// QSO. Anything else is returned untouched, and still fails as it did.
func stripForwarderHeader(pkt []byte) []byte {
	if len(pkt) < 4 {
		return pkt
	}
	if binary.BigEndian.Uint32(pkt) == wsjtMagic {
		return pkt // no header — the overwhelmingly common case
	}
	limit := len(pkt) - 4
	if limit > maxFwdHeader {
		limit = maxFwdHeader
	}
	for i := 1; i <= limit; i++ {
		if binary.BigEndian.Uint32(pkt[i:]) != wsjtMagic {
			continue
		}
		for _, b := range pkt[:i] {
			if b < 0x20 || b >= 0x7f {
				return pkt // not a text header — leave it alone
			}
		}
		return pkt[i:]
	}
	return pkt
}

// 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")
	}
	// A relay (W&P and friends) puts its own origin header in front — skip it so
	// the packet parses exactly as if it had arrived from WSJT-X directly.
	pkt = stripForwarderHeader(pkt)
	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, grid := wsjtSender(msg)
		if call == "" {
			return WSJTEvent{}, false, nil // free-text / telemetry / unparseable → ignore
		}
		ev.IsDecode = true
		ev.DecodeCall = call
		ev.IsCQ = isCQ
		ev.DecodeGrid = grid
		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,
// whether it was a CQ, and the grid when the message carries one. Grammar:
//
//	CQ [modifier] <de_call> [grid]      → de_call, isCQ=true, grid
//	<to_call> <de_call> [report|…]      → de_call, isCQ=false
//
// Only a CQ carries a grid: the standard exchange puts a signal report in that
// third slot, never a locator.
//
// Returns "" for free-text / telemetry / hashed-call messages we can't resolve.
func wsjtSender(message string) (call string, isCQ bool, grid string) {
	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) {
			c := f[idx]
			// A GRID sitting in the callsign slot means the real call was
			// unparseable and the skip above went one word too far. JN36 has letters
			// and digits and passes every shape test there is, so without this the
			// station's grid gets logged, spotted and coloured as its callsign.
			if looksLikeCall(c) && !isGridField(c) {
				if idx+1 < len(f) && isGridField(f[idx+1]) {
					grid = f[idx+1]
				}
				return c, true, grid
			}
		}
		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, ""
}

// isGridField reports a 4-character Maidenhead field+square (JN36).
//
// RR73 is the reason this is not a bare pattern match: it is a sign-off, not a
// locator, yet R falls inside A–R and 73 inside 00–99, so it satisfies the
// Maidenhead shape exactly. WSJT-X never puts it in the slot after a CQ call,
// but a station sending "CQ RR73" style free text would silently plant a
// nonexistent grid in the log's grid index, and nothing downstream could tell.
func isGridField(s string) bool {
	if len(s) != 4 || s == "RR73" {
		return false
	}
	return s[0] >= 'A' && s[0] <= 'R' && s[1] >= 'A' && s[1] <= 'R' &&
		s[2] >= '0' && s[2] <= '9' && s[3] >= '0' && s[3] <= '9'
}

// 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
}
