// Package powergenius drives a 4O3A PowerGenius XL amplifier over its TCP text
// API (same "Genius Series" line protocol as the Antenna Genius). OpsLog reads
// the amp's operate state via the FlexRadio amplifier object, but the fan mode
// is a PGXL-only setting only reachable on the amp's own control port — hence
// this small direct client. Commands are "C<id>|<cmd>\n"; replies are
// "R<id>|0|<k=v …>" and asynchronous "S0|<k=v …>".
package powergenius

import (
	"bufio"
	"fmt"
	"net"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

const (
	defaultPort    = 9008
	dialTimeout    = 5 * time.Second
	ioTimeout      = 3 * time.Second
	pollEvery      = 1500 * time.Millisecond
	reconnectDelay = 2 * time.Second
)

// Status is the snapshot the UI renders (only the bits OpsLog needs).
type Status struct {
	Connected   bool    `json:"connected"`
	Host        string  `json:"host,omitempty"`
	LastError   string  `json:"last_error,omitempty"`
	State       string  `json:"state,omitempty"`    // IDLE / TRANSMIT_A …
	FanMode     string  `json:"fan_mode,omitempty"` // STANDARD / CONTEST / BROADCAST
	Temperature float64 `json:"temperature"`
}

type Client struct {
	host string
	port int

	mu     sync.Mutex // serialises command send/recv on the connection
	conn   net.Conn
	reader *bufio.Reader

	statusMu sync.RWMutex
	status   Status
	// Optimistic fan mode kept until the amp's status poll confirms it (or it
	// ages out) — otherwise a stale poll right after a change reverts the UI.
	fanPending   string
	fanPendingAt time.Time

	cmdID   atomic.Int64
	stop    chan struct{}
	running bool
}

func New(host string, port int) *Client {
	if port <= 0 || port > 65535 {
		port = defaultPort
	}
	return &Client{host: host, port: port, stop: make(chan struct{}), status: Status{Host: host}}
}

func (c *Client) Start() error {
	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()
	if c.conn != nil {
		c.conn.Close()
		c.conn = nil
		c.reader = nil
	}
	c.mu.Unlock()
}

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

func (c *Client) setStatus(fn func(*Status)) {
	c.statusMu.Lock()
	fn(&c.status)
	c.statusMu.Unlock()
}

// SetFanMode sets the amplifier fan mode (STANDARD | CONTEST | BROADCAST).
func (c *Client) SetFanMode(mode string) error {
	m := strings.ToUpper(strings.TrimSpace(mode))
	switch m {
	case "STANDARD", "CONTEST", "BROADCAST":
	default:
		return fmt.Errorf("powergenius: invalid fan mode %q", mode)
	}
	if _, err := c.command("setup fanmode=" + m); err != nil {
		return err
	}
	c.statusMu.Lock()
	c.status.FanMode = m // optimistic
	c.fanPending, c.fanPendingAt = m, time.Now()
	c.statusMu.Unlock()
	return nil
}

// SetOperate puts the amp in OPERATE (1) or STANDBY (0).
func (c *Client) SetOperate(on bool) error {
	v := "0"
	if on {
		v = "1"
	}
	_, err := c.command("operate=" + v)
	return err
}

func (c *Client) pollLoop() {
	t := time.NewTicker(pollEvery)
	defer t.Stop()
	for {
		select {
		case <-c.stop:
			return
		case <-t.C:
			if err := c.ensureConnected(); err != nil {
				c.setStatus(func(s *Status) { s.Connected = false; s.LastError = "dial: " + err.Error() })
				continue
			}
			if _, err := c.command("status"); err != nil {
				c.dropConn()
				c.setStatus(func(s *Status) { s.Connected = false; s.LastError = err.Error() })
			}
		}
	}
}

func (c *Client) ensureConnected() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.conn != nil {
		return nil
	}
	conn, err := net.DialTimeout("tcp", net.JoinHostPort(c.host, strconv.Itoa(c.port)), dialTimeout)
	if err != nil {
		return err
	}
	c.conn = conn
	c.reader = bufio.NewReader(conn)
	// Discard the version banner the device sends on connect.
	_ = conn.SetReadDeadline(time.Now().Add(ioTimeout))
	_, _ = c.reader.ReadString('\n')
	return nil
}

func (c *Client) dropConn() {
	c.mu.Lock()
	if c.conn != nil {
		c.conn.Close()
		c.conn = nil
		c.reader = nil
	}
	c.mu.Unlock()
}

// command sends "C<id>|<cmd>\n" and parses the single-line reply into status.
func (c *Client) command(cmd string) (string, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.conn == nil || c.reader == nil {
		return "", fmt.Errorf("powergenius: not connected")
	}
	id := c.cmdID.Add(1)
	_ = c.conn.SetWriteDeadline(time.Now().Add(ioTimeout))
	if _, err := fmt.Fprintf(c.conn, "C%d|%s\n", id, cmd); err != nil {
		return "", err
	}
	_ = c.conn.SetReadDeadline(time.Now().Add(ioTimeout))
	line, err := c.reader.ReadString('\n')
	if err != nil {
		return "", err
	}
	line = strings.TrimSpace(line)
	c.parse(line)
	return line, nil
}

// parse handles "R<id>|0|<k=v …>" and "S0|<k=v …>" status lines.
func (c *Client) parse(resp string) {
	var data string
	switch {
	case strings.HasPrefix(resp, "R"):
		p := strings.SplitN(resp, "|", 3)
		if len(p) < 3 {
			return
		}
		data = p[2]
	case strings.HasPrefix(resp, "S"):
		p := strings.SplitN(resp, "|", 2)
		if len(p) < 2 {
			return
		}
		data = p[1]
	default:
		return
	}
	c.statusMu.Lock()
	c.status.Connected = true
	c.status.LastError = ""
	for _, pair := range strings.Fields(data) {
		kv := strings.SplitN(pair, "=", 2)
		if len(kv) != 2 {
			continue
		}
		switch kv[0] {
		case "state":
			c.status.State = kv[1]
		case "fanmode":
			dev := strings.ToUpper(kv[1])
			// Honour a recent optimistic change until the amp confirms it.
			if c.fanPending != "" && time.Since(c.fanPendingAt) < 3*time.Second && dev != c.fanPending {
				break
			}
			c.fanPending = ""
			c.status.FanMode = dev
		case "temp":
			c.status.Temperature, _ = strconv.ParseFloat(kv[1], 64)
		}
	}
	c.statusMu.Unlock()
}
