//go:build windows

package cat

import (
	"context"
	"fmt"
	"net"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

// TCI is a native backend for Expert Electronics' TCI protocol (SunSDR2/MB1/
// ColibriNANO via ExpertSDR2/EESDR, and TCI-compatible apps). TCI is a text
// protocol over a WebSocket: the server streams state ("vfo:0,0,14100000;",
// "modulation:0,cw;", "trx:0,true;") and accepts the same commands to control
// the rig. We keep the pushed state cached so ReadState is instant, like Flex.
//
// Pure Go (gorilla/websocket, no CGO). Default port 40001.
type TCI struct {
	host string
	port int

	digitalDefault string // surfaced when the rig reports a digital mode (FT8/…)
	spotsEnabled   bool   // mirror cluster spots onto the TCI panorama

	mu         sync.Mutex // guards conn + writes + state
	conn       *websocket.Conn
	dialCancel context.CancelFunc // cancels an in-flight Connect dial (Interrupt/Stop)
	ready      bool

	// Cached state pushed by the radio.
	device string
	freqA  int64 // VFO A (RX) frequency, Hz  (vfo:0,0)
	freqB  int64 // VFO B (TX in split), Hz    (vfo:0,1)
	mode   string
	split  bool
	tx     bool

	lastSig string // last logged state signature (log only on change)
}

const tciDefaultPort = 40001

// NewTCI builds a TCI backend for the given host/port. digitalDefault is the
// mode surfaced when the radio reports a generic digital modulation; spots turns
// on mirroring OpsLog's cluster spots onto the TCI panorama.
func NewTCI(host string, port int, digitalDefault string, spots bool) *TCI {
	if port <= 0 || port > 65535 {
		port = tciDefaultPort
	}
	return &TCI{host: strings.TrimSpace(host), port: port, digitalDefault: strings.TrimSpace(digitalDefault), spotsEnabled: spots}
}

func (t *TCI) Name() string { return "tci" }

// Connect opens the WebSocket and starts the reader goroutine. The reader keeps
// our cached state current from the radio's push messages.
func (t *TCI) Connect() error {
	t.mu.Lock()
	already := t.conn != nil
	host, port := t.host, t.port
	t.mu.Unlock()
	if already {
		return nil
	}
	if host == "" {
		return fmt.Errorf("tci: no host configured")
	}
	url := fmt.Sprintf("ws://%s", net.JoinHostPort(host, strconv.Itoa(port)))
	// Cancellable dial so Interrupt() (Stop / Settings "Save & Close") aborts it at
	// once instead of waiting out a dead server's 5 s handshake timeout.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	t.mu.Lock()
	t.dialCancel = cancel
	t.mu.Unlock()
	dialer := websocket.Dialer{HandshakeTimeout: 5 * time.Second}
	conn, _, err := dialer.DialContext(ctx, url, nil)
	cancel()
	t.mu.Lock()
	t.dialCancel = nil
	t.mu.Unlock()
	if err != nil {
		return fmt.Errorf("tci: connect %s: %w", url, err)
	}
	t.mu.Lock()
	t.conn = conn
	t.ready = false
	t.mu.Unlock()
	debugLog.Printf("TCI: connected to %s", url)
	go t.reader(conn)
	if t.spotsEnabled {
		_ = t.send("spot_clear;") // drop any leftover spots from a previous session
	}
	return nil
}

// SendSpot mirrors a cluster spot onto the TCI panorama (implements Spotter).
// The radio replaces a spot that has the same callsign, so re-spotting updates
// it in place. No-op when spot mirroring is disabled.
func (t *TCI) SendSpot(s SpotInfo) error {
	if !t.spotsEnabled {
		return nil
	}
	call := strings.TrimSpace(s.Callsign)
	if call == "" || s.FreqHz <= 0 {
		return nil
	}
	color := strings.TrimSpace(s.Color)
	if color == "" {
		color = "#FFFFA500" // opaque orange default
	}
	color = "0x" + strings.TrimPrefix(color, "#")
	mode := strings.ToLower(strings.TrimSpace(s.Mode))
	// Commas/semicolons would break TCI's comma-separated argument parsing.
	text := strings.NewReplacer(",", " ", ";", " ").Replace(s.Comment)
	return t.send(fmt.Sprintf("spot:%s,%s,%d,%s,%s;", call, mode, s.FreqHz, color, text))
}

// Disconnect closes the WebSocket; the reader goroutine then exits.
func (t *TCI) Disconnect() {
	t.mu.Lock()
	c := t.conn
	t.conn = nil
	t.ready = false
	t.mu.Unlock()
	if c != nil {
		_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
		_ = c.Close()
	}
}

// Interrupt aborts an in-flight Connect dial so Stop()/Start() don't block on a
// dead server's handshake timeout. Satisfies the Manager's interruptible
// interface. Safe from another goroutine; a no-op when not dialing.
func (t *TCI) Interrupt() {
	t.mu.Lock()
	cancel := t.dialCancel
	c := t.conn
	t.mu.Unlock()
	if cancel != nil {
		cancel()
	}
	if c != nil {
		_ = c.Close()
	}
}

// ReadState returns the cached state pushed by the radio.
func (t *TCI) ReadState() (RigState, error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if t.conn == nil {
		return RigState{}, fmt.Errorf("tci: not connected")
	}
	st := RigState{Connected: t.ready, Rig: t.device}
	if !t.ready {
		return st, nil
	}
	// ADIF convention: FreqHz is the TX freq. In split, TX is VFO B.
	if t.split && t.freqB > 0 {
		st.FreqHz = t.freqB
		st.RxFreqHz = t.freqA
		st.Split = true
	} else {
		st.FreqHz = t.freqA
	}
	st.Mode = tciModeToADIF(t.mode, t.digitalDefault)
	if st.FreqHz > 0 {
		st.Band = BandFromHz(st.FreqHz)
	}
	sig := fmt.Sprintf("%d/%d/%v/%s", st.FreqHz, st.RxFreqHz, st.Split, st.Mode)
	if sig != t.lastSig {
		t.lastSig = sig
		debugLog.Printf("TCI: state tx=%d rx=%d split=%v mode=%s", st.FreqHz, st.RxFreqHz, st.Split, st.Mode)
	}
	return st, nil
}

// SetFrequency tunes VFO A (the main/RX VFO).
func (t *TCI) SetFrequency(hz int64) error {
	return t.send(fmt.Sprintf("vfo:0,0,%d;", hz))
}

// SetMode maps an ADIF mode to a TCI modulation and sets it. USB vs LSB is
// chosen from the current VFO-A frequency (< 10 MHz → LSB).
func (t *TCI) SetMode(mode string) error {
	t.mu.Lock()
	freq := t.freqA
	t.mu.Unlock()
	m := adifToTCIMode(mode, freq)
	if m == "" {
		return nil
	}
	return t.send(fmt.Sprintf("modulation:0,%s;", m))
}

// SetPTT keys or unkeys the transmitter (VFO 0).
func (t *TCI) SetPTT(on bool) error {
	return t.send(fmt.Sprintf("trx:0,%t;", on))
}

// send writes a command to the WebSocket (one writer at a time).
func (t *TCI) send(cmd string) error {
	t.mu.Lock()
	c := t.conn
	t.mu.Unlock()
	if c == nil {
		return fmt.Errorf("tci: not connected")
	}
	_ = c.SetWriteDeadline(time.Now().Add(3 * time.Second))
	if err := c.WriteMessage(websocket.TextMessage, []byte(cmd)); err != nil {
		debugLog.Printf("TCI: send %q failed: %v", cmd, err)
		return err
	}
	debugLog.Printf("TCI: → %s", cmd)
	return nil
}

// reader drains push messages and keeps the cached state current until the
// connection closes.
func (t *TCI) reader(conn *websocket.Conn) {
	for {
		_, data, err := conn.ReadMessage()
		if err != nil {
			break
		}
		// A frame may carry several ";"-terminated commands.
		for _, cmd := range strings.Split(string(data), ";") {
			t.handle(strings.TrimSpace(cmd))
		}
	}
	t.mu.Lock()
	if t.conn == conn {
		t.conn = nil
		t.ready = false
	}
	t.mu.Unlock()
	debugLog.Printf("TCI: reader ended")
}

// handle parses one "command:args" message and updates the cache.
func (t *TCI) handle(msg string) {
	if msg == "" {
		return
	}
	name, args := msg, ""
	if i := strings.IndexByte(msg, ':'); i >= 0 {
		name, args = msg[:i], msg[i+1:]
	}
	f := strings.Split(args, ",")
	get := func(i int) string {
		if i < len(f) {
			return strings.TrimSpace(f[i])
		}
		return ""
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	switch strings.ToLower(name) {
	case "device":
		t.device = strings.TrimSpace(args)
	case "ready", "start":
		t.ready = true
	case "stop":
		t.ready = false
	case "vfo":
		// vfo:<rx>,<channel>,<freq>
		if get(0) == "0" {
			hz, _ := strconv.ParseInt(get(2), 10, 64)
			if hz > 0 {
				t.ready = true // receiving live state → treat as ready even without an explicit "ready;"
				switch get(1) {
				case "0":
					t.freqA = hz
				case "1":
					t.freqB = hz
				}
			}
		}
	case "modulation":
		if get(0) == "0" {
			t.mode = strings.ToLower(get(1))
		}
	case "split_enable":
		if get(0) == "0" {
			t.split = get(1) == "true"
		}
	case "trx":
		if get(0) == "0" {
			t.tx = get(1) == "true"
		}
	}
}

// tciModeToADIF converts a TCI modulation to an ADIF mode. Generic digital
// modulations surface the operator's chosen digital default (FT8/FT4/RTTY…).
func tciModeToADIF(m, digitalDefault string) string {
	switch strings.ToLower(strings.TrimSpace(m)) {
	case "usb", "lsb", "dsb":
		return "SSB"
	case "cw":
		return "CW"
	case "am", "sam":
		return "AM"
	case "nfm", "wfm", "fm":
		return "FM"
	case "digu", "digl":
		if digitalDefault != "" {
			return strings.ToUpper(digitalDefault)
		}
		return "DATA"
	case "drm":
		return "DIGITALVOICE"
	case "":
		return ""
	default:
		return strings.ToUpper(m)
	}
}

// adifToTCIMode maps an ADIF mode to a TCI modulation. USB/LSB is chosen from
// the frequency (< 10 MHz → LSB) as usual. Digital modes → digu.
func adifToTCIMode(mode string, freqHz int64) string {
	switch strings.ToUpper(strings.TrimSpace(mode)) {
	case "SSB", "USB", "LSB":
		if freqHz > 0 && freqHz < 10_000_000 {
			return "lsb"
		}
		return "usb"
	case "CW", "CWR", "CW-R":
		return "cw"
	case "AM":
		return "am"
	case "FM", "NFM":
		return "nfm"
	case "RTTY":
		return "digl"
	case "":
		return ""
	default:
		// FT8/FT4/PSK/DATA/JT… → upper-sideband digital.
		return "digu"
	}
}
