package kpa

// The client: one connection, strict question-and-answer, a cached status.
//
// Shaped like internal/acom and internal/spe so a third amplifier is the same
// thing to read — but the traffic is the opposite kind. Those two are told to
// stream and are then listened to; this one is asked, and answers. The
// reference is explicit that there is no flow control and that commands are
// paced by waiting for the previous reply, so nothing here ever has two
// questions outstanding.

import (
	"bufio"
	"fmt"
	"io"
	"net"
	"strings"
	"sync"
	"time"

	"go.bug.st/serial"

	"hamlog/internal/applog"
)

const (
	dialTimeout = 5 * time.Second
	ioTimeout   = 2 * time.Second
	// pollInterval is the fast cycle: forward power, SWR, and whether a fault has
	// appeared. Four times a second is enough for a bar that is read while
	// talking, and it is four round trips a second on a link with no flow
	// control — faster buys nothing and costs the set commands their latency.
	pollInterval = 250 * time.Millisecond
	// slowEvery is how many fast cycles pass between the readings that do not
	// move: mode, band, temperature, supply. Once a second.
	slowEvery = 4
)

// Status is what the panel polls.
type Status struct {
	Connected bool   `json:"connected"`
	Transport string `json:"transport"` // "serial" | "tcp"
	Model     string `json:"model,omitempty"`
	LastError string `json:"last_error,omitempty"`

	// PowerOn is the main supplies (^ON), Operate is OPERATE vs STANDBY (^OS).
	// They are different questions: an amplifier can be switched on and in
	// standby, which is the normal state between overs.
	PowerOn bool `json:"power_on"`
	Operate bool `json:"operate"`

	FwdW  int     `json:"fwd_w"`
	SWR   float64 `json:"swr"`
	VoltV float64 `json:"volt_v"`
	CurA  int     `json:"cur_a"`
	TempC int     `json:"temp_c"`
	Band  string  `json:"band,omitempty"`

	// Tuning is the ATU mid-cycle (^TP), so a panel can say so rather than
	// showing a wild SWR and a power reading nobody should act on.
	Tuning bool `json:"tuning"`

	// Fault is the current fault code and its meaning. A fault puts the
	// amplifier in STANDBY by itself, so it is the first thing to show.
	FaultCode int    `json:"fault_code"`
	FaultText string `json:"fault_text,omitempty"`
}

// Config selects the model and how to reach it.
type Config struct {
	Model     string // "KPA500" | "KPA1500"
	Transport string // "serial" | "tcp"
	ComPort   string // serial
	Baud      int    // serial: 4800…230400, set on the amplifier and not negotiated
	Host      string // tcp (KPA1500 only)
	Port      int    // tcp, default 1500
}

type Client struct {
	cfg Config

	mu     sync.Mutex // serialises the connection: one question at a time
	conn   io.ReadWriteCloser
	rd     *bufio.Reader
	skipTP bool // ^TP went unanswered once — a KPA500, no ATU; never ask again

	statusMu sync.RWMutex
	status   Status

	stop    chan struct{}
	running bool
}

// New builds a client. Nothing is opened until Start.
func New(cfg Config) *Client {
	if cfg.Baud <= 0 {
		cfg.Baud = 38400
	}
	if cfg.Port <= 0 {
		cfg.Port = 1500
	}
	if strings.TrimSpace(cfg.Model) == "" {
		cfg.Model = "KPA1500"
	}
	c := &Client{cfg: cfg, stop: make(chan struct{})}
	c.status.Transport = cfg.Transport
	c.status.Model = strings.ToUpper(strings.TrimSpace(cfg.Model))
	return c
}

func (c *Client) Start() error {
	if c.running {
		return nil
	}
	c.running = true
	go c.pollLoop()
	return nil
}

func (c *Client) Stop() {
	if !c.running {
		return
	}
	c.running = false
	close(c.stop)
	c.mu.Lock()
	c.dropLocked()
	c.mu.Unlock()
}

func (c *Client) GetStatus() Status {
	c.statusMu.RLock()
	defer c.statusMu.RUnlock()
	return c.status
}

func (c *Client) setErr(msg string) {
	c.statusMu.Lock()
	was := c.status.LastError
	c.status.Connected = false
	c.status.LastError = msg
	c.statusMu.Unlock()
	// Logged on CHANGE only: a disconnected amplifier is polled four times a
	// second, and the log is where a hardware problem is diagnosed hours later.
	if msg != "" && msg != was {
		applog.Printf("kpa: %s", msg)
	}
}

// dropLocked closes the connection. Caller holds c.mu.
func (c *Client) dropLocked() {
	if c.conn != nil {
		_ = c.conn.Close()
		c.conn = nil
		c.rd = nil
	}
}

// connectLocked opens the transport. Caller holds c.mu.
func (c *Client) connectLocked() error {
	if c.conn != nil {
		return nil
	}
	switch strings.ToLower(strings.TrimSpace(c.cfg.Transport)) {
	case "tcp":
		if strings.TrimSpace(c.cfg.Host) == "" {
			return fmt.Errorf("no address configured for the amplifier")
		}
		addr := net.JoinHostPort(c.cfg.Host, fmt.Sprint(c.cfg.Port))
		conn, err := net.DialTimeout("tcp", addr, dialTimeout)
		if err != nil {
			// Named for what it usually is. The KPA1500 accepts ONE TCP client,
			// so the common failure is not a wrong address but the Elecraft
			// utility already holding the socket — and "connection refused"
			// sends an operator looking at their network instead.
			return fmt.Errorf("cannot reach the amplifier on %s: %w (it accepts a single TCP connection — close the Elecraft utility or any other program using it)", addr, err)
		}
		c.conn = conn
	default:
		if strings.TrimSpace(c.cfg.ComPort) == "" {
			return fmt.Errorf("no serial port configured for the amplifier")
		}
		p, err := serial.Open(c.cfg.ComPort, &serial.Mode{BaudRate: c.cfg.Baud})
		if err != nil {
			return fmt.Errorf("cannot open %s: %w", c.cfg.ComPort, err)
		}
		// The KPA500 is POWER-CONTROLLED by these lines: the Elecraft utility
		// switches the amplifier on by raising them. Held asserted, once, and
		// never touched again — reconnect cycles that toggled them were
		// switching a KPA500 OFF twenty seconds after its operator pressed
		// nothing but Standby.
		_ = p.SetDTR(true)
		_ = p.SetRTS(true)
		_ = p.SetReadTimeout(ioTimeout)
		c.conn = p
	}
	c.rd = bufio.NewReader(c.conn)
	applog.Printf("kpa: connected to the %s", c.status.Model)
	return nil
}

// ask sends one command and reads its answer.
//
// The whole exchange is under the lock: with no flow control, two questions in
// flight means two answers to sort out, and the only thing distinguishing them
// is the prefix — which is exactly what payload() has to reject when it
// happens.
func (c *Client) ask(cmd string) (string, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if err := c.connectLocked(); err != nil {
		return "", err
	}
	if tc, ok := c.conn.(net.Conn); ok {
		_ = tc.SetDeadline(time.Now().Add(ioTimeout))
	}
	if _, err := c.conn.Write([]byte(cmd)); err != nil {
		c.dropLocked()
		return "", fmt.Errorf("writing %s: %w", cmd, err)
	}
	// Answers end with a semicolon and nothing else does, so the terminator is
	// the frame.
	line, err := c.rd.ReadString(';')
	if err != nil {
		// NOT dropped. A command this model simply does not know (^TP is the
		// KPA1500's ATU — a KPA500 never answers it) is silence, not a dead
		// link, and dropping here tore the connection down on every slow poll
		// cycle: two seconds of stalled commands, a reconnect, and a DTR
		// toggle the amplifier read as the off switch. Nothing arrived, so
		// nothing is left to desynchronise the next exchange. Write errors —
		// the genuinely dead link — still drop, above.
		return "", fmt.Errorf("no answer to %s: %w", cmd, err)
	}
	return strings.TrimSpace(line), nil
}

// send is a SET: written, and not answered. The reference says SET commands do
// not generally produce a response, so waiting for one would stall the poll
// loop for a whole timeout every time the operator pressed a button.
func (c *Client) send(cmd string) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if err := c.connectLocked(); err != nil {
		return err
	}
	if tc, ok := c.conn.(net.Conn); ok {
		_ = tc.SetDeadline(time.Now().Add(ioTimeout))
	}
	if _, err := c.conn.Write([]byte(cmd)); err != nil {
		c.dropLocked()
		return fmt.Errorf("writing %s: %w", cmd, err)
	}
	applog.Printf("kpa: → %s", cmd)
	return nil
}

// Operate puts the amplifier in OPERATE (true) or STANDBY (false).
//
// Worth knowing, and worth saying in the UI: from firmware 01.41 onwards,
// going to OPERATE also CLEARS the current fault — every one except
// temperature, which clears by cooling. So this button is the way out of a
// fault as well as the way into transmit.
func (c *Client) Operate(on bool) error {
	if on {
		return c.send("^OS1;")
	}
	return c.send("^OS0;")
}

// ClearFault clears the current fault without changing mode (^FLC).
func (c *Client) ClearFault() error { return c.send("^FLC;") }

// PowerOn switches the main supplies on or off (^ON1 / ^ON0).
//
// Off is a real power-down, not standby, and the way back on over the network
// is Wake-on-LAN or the front panel — so a caller should be asking the operator
// first. The sleeping microcontroller does answer ^ON while the supplies are
// off, which is why "off" is a state this can report rather than a silence.
func (c *Client) PowerOn(on bool) error {
	if on {
		return c.send("^ON1;")
	}
	return c.send("^ON0;")
}

// Tune starts an ATU tune cycle (^FT). It needs drive from the transceiver.
func (c *Client) Tune() error { return c.send("^FT;") }

// pollLoop keeps the status fresh, reconnecting as needed.
func (c *Client) pollLoop() {
	t := time.NewTicker(pollInterval)
	defer t.Stop()
	var n uint64
	for {
		select {
		case <-c.stop:
			return
		case <-t.C:
			c.pollOnce(n)
			n++
		}
	}
}

func (c *Client) pollOnce(n uint64) {
	// Forward power and SWR in ONE exchange (^WS), which is why that command
	// exists and why the two are not asked separately.
	reply, err := c.ask("^WS;")
	if err != nil {
		c.setErr(err.Error())
		return
	}
	w, swr, err := parseWS(reply)
	if err != nil {
		c.setErr(err.Error())
		return
	}

	c.statusMu.Lock()
	c.status.Connected = true
	c.status.LastError = ""
	c.status.FwdW, c.status.SWR = w, swr
	c.statusMu.Unlock()

	// The fault, every cycle: it puts the amplifier in standby by itself, and an
	// operator watching a power bar needs to know why it stopped moving.
	if reply, err := c.ask("^FL;"); err == nil {
		if code, err := parseFault(reply); err == nil {
			c.statusMu.Lock()
			was := c.status.FaultCode
			c.status.FaultCode = code
			c.status.FaultText = FaultName(code)
			c.statusMu.Unlock()
			if code != was && code != 0 {
				applog.Printf("kpa: FAULT %02X — %s", code, FaultName(code))
			}
		}
	}

	if n%slowEvery != 0 {
		return
	}
	// The readings that do not move fast. Each is optional: an older firmware or
	// a KPA500 that does not know one of these must not take the rest down with
	// it, so a failure here leaves the previous value standing.
	if reply, err := c.ask("^OS;"); err == nil {
		if v, err := parseInt(reply, "^OS"); err == nil {
			c.statusMu.Lock()
			c.status.Operate = v == 1
			c.statusMu.Unlock()
		}
	}
	if reply, err := c.ask("^ON;"); err == nil {
		if v, err := parseInt(reply, "^ON"); err == nil {
			c.statusMu.Lock()
			c.status.PowerOn = v == 1
			c.statusMu.Unlock()
		}
	}
	if reply, err := c.ask("^VI;"); err == nil {
		if v, a, err := parseVI(reply); err == nil {
			c.statusMu.Lock()
			c.status.VoltV, c.status.CurA = v, a
			c.statusMu.Unlock()
		}
	}
	if reply, err := c.ask("^TM;"); err == nil {
		if v, err := parseInt(reply, "^TM"); err == nil {
			c.statusMu.Lock()
			c.status.TempC = v
			c.statusMu.Unlock()
		}
	}
	if reply, err := c.ask("^BN;"); err == nil {
		if v, err := parseInt(reply, "^BN"); err == nil {
			c.statusMu.Lock()
			c.status.Band = BandName(v)
			c.statusMu.Unlock()
		}
	}
	if c.skipTP {
		return
	}
	if reply, err := c.ask("^TP;"); err == nil {
		if v, err := parseInt(reply, "^TP"); err == nil {
			c.statusMu.Lock()
			c.status.Tuning = v == 1
			c.statusMu.Unlock()
		}
	} else {
		// One silence is the model's answer for good: a KPA500 has no ATU and
		// will never answer ^TP — asking again every cycle cost a two-second
		// stall each time.
		c.skipTP = true
		applog.Printf("kpa: ^TP unanswered — no ATU on this model, not asking again")
	}
}

// SetBand puts the amplifier on a band by its ADIF name.
//
// The KPA takes its band from the transceiver on its own XCVR connector, but it
// also accepts ^BN — so OpsLog can simply say it on the link it is already
// using. That is worth knowing: an Acom has no such command, which is why
// following it needs a second serial port and a transceiver emulator answering
// its polls (internal/catemu). None of that applies here.
//
// Sent only when it CHANGES. Repeating the current band four times a second
// would be traffic on a link with no flow control, in exchange for nothing.
func (c *Client) SetBand(adifBand string) error {
	n, ok := bandNumber(adifBand)
	if !ok {
		// Not an error the operator should see: the KPA covers 160-6 m, and
		// tuning to 23 cm is not a fault, it is simply not this amplifier's
		// business.
		return nil
	}
	c.statusMu.Lock()
	same := c.status.Band == adifBand
	c.statusMu.Unlock()
	if same {
		return nil
	}
	return c.send(fmt.Sprintf("^BN%02d;", n))
}

// bandNumber is BandName backwards.
func bandNumber(adifBand string) (int, bool) {
	b := strings.ToLower(strings.TrimSpace(adifBand))
	for n, name := range bandNames {
		if name == b {
			return n, true
		}
	}
	return 0, false
}
