package psu

import (
	"fmt"
	"log"
	"strings"
	"sync"
	"time"

	"go.bug.st/serial"
)

// Config is one supply's serial link.
type Config struct {
	ComPort string
	Baud    int  // 9600 unless the supply has been reconfigured
	Address byte // Modbus slave address, 1…15 on this family
}

// Status is what the UI shows. Everything here is READ from the supply — the
// set points included, which the operator sets on the front panel and OpsLog
// only reports.
type Status struct {
	Connected bool    `json:"connected"`
	On        bool    `json:"on"`        // output enabled
	Volts     float64 `json:"volts"`     // measured output
	Amps      float64 `json:"amps"`      // measured output
	Watts     float64 `json:"watts"`     // measured output
	SetVolts  float64 `json:"set_volts"` // the voltage the supply is set to
	SetAmps   float64 `json:"set_amps"`  // the current limit it is set to
	Protected uint16  `json:"protected"` // protection status word, non-zero = tripped
	Error     string  `json:"error,omitempty"`
}

const pollEvery = 1500 * time.Millisecond

// Client owns the serial link to one supply.
type Client struct {
	cfg Config

	connMu sync.Mutex
	conn   serial.Port

	ioMu sync.Mutex // serialises a request/reply exchange on the shared port

	statusMu sync.Mutex
	last     Status

	stopChan chan struct{}
	stopOnce sync.Once
	lastErr  string
}

// New builds a client. Nothing is opened until Start.
func New(cfg Config) *Client {
	if cfg.Baud <= 0 {
		cfg.Baud = 9600
	}
	if cfg.Address == 0 {
		cfg.Address = 1
	}
	return &Client{cfg: cfg, stopChan: make(chan struct{})}
}

// Start begins the poll loop. It returns immediately: the supply may be off, and
// a shack comes up in whatever order it comes up in.
func (c *Client) Start() error {
	go c.loop()
	return nil
}

// Stop closes the link.
func (c *Client) Stop() {
	c.stopOnce.Do(func() { close(c.stopChan) })
	c.closeConn()
}

// GetStatus returns the last poll's answer.
func (c *Client) GetStatus() Status {
	c.statusMu.Lock()
	defer c.statusMu.Unlock()
	return c.last
}

// SetOutput switches the supply's output on or off — the one thing this driver
// writes. The supply's echo is checked, so a false return of "done" is not
// possible: either it confirmed the value or this is an error.
func (c *Client) SetOutput(on bool) error {
	val := uint16(0)
	if on {
		val = 1
	}
	if err := c.writeRegister(regOnOff, val); err != nil {
		return err
	}
	// Report it at once rather than waiting for the next poll: the operator
	// pressed a button and is looking at it.
	c.statusMu.Lock()
	c.last.On = on
	c.statusMu.Unlock()
	log.Printf("psu: output %s", map[bool]string{true: "ON", false: "OFF"}[on])
	return nil
}

func (c *Client) loop() {
	t := time.NewTicker(pollEvery)
	defer t.Stop()
	for {
		select {
		case <-c.stopChan:
			return
		case <-t.C:
			if err := c.poll(); err != nil {
				c.noteFailure(err)
				continue
			}
		}
	}
}

// poll reads everything the UI shows, in as few exchanges as the register map
// allows: the measurements are contiguous (0x0010…0x0013), the set points are
// contiguous (0x0030, 0x0031), and the on/off and protection words sit together
// at 0x0001/0x0002.
func (c *Client) poll() error {
	if err := c.ensureConn(); err != nil {
		return err
	}
	st := Status{Connected: true}

	state, err := c.readRegisters(regOnOff, 2)
	if err != nil {
		return err
	}
	st.On = state[0] != 0
	st.Protected = state[1]

	meas, err := c.readRegisters(regVolts, 4) // U, I, P high, P low
	if err != nil {
		return err
	}
	st.Volts = float64(meas[0]) / voltScale
	st.Amps = float64(meas[1]) / ampScale
	st.Watts = float64(uint32(meas[2])<<16|uint32(meas[3])) / wattScale

	set, err := c.readRegisters(regSetVolts, 2)
	if err != nil {
		return err
	}
	st.SetVolts = float64(set[0]) / voltScale
	st.SetAmps = float64(set[1]) / ampScale

	c.statusMu.Lock()
	c.last = st
	c.statusMu.Unlock()
	if c.lastErr != "" {
		log.Printf("psu: %s answering again", c.cfg.ComPort)
		c.lastErr = ""
	}
	return nil
}

func (c *Client) readRegisters(reg, count uint16) ([]uint16, error) {
	frame, err := c.exchange(buildRead(c.cfg.Address, reg, count))
	if err != nil {
		return nil, err
	}
	return parseRead(c.cfg.Address, count, frame)
}

func (c *Client) writeRegister(reg, val uint16) error {
	if err := c.ensureConn(); err != nil {
		return err
	}
	frame, err := c.exchange(buildWrite(c.cfg.Address, reg, val))
	if err != nil {
		return err
	}
	return parseWriteEcho(c.cfg.Address, reg, val, frame)
}

// exchange sends one frame and reads one reply, holding the port for the whole
// round trip. Modbus RTU has no way to match a reply to a request, so two
// exchanges in flight at once would read each other's answers.
func (c *Client) exchange(req []byte) ([]byte, error) {
	c.connMu.Lock()
	conn := c.conn
	c.connMu.Unlock()
	if conn == nil {
		return nil, fmt.Errorf("psu: not connected")
	}

	c.ioMu.Lock()
	defer c.ioMu.Unlock()

	// The silence before a frame is part of the protocol, not politeness: it is
	// how the supply knows this is a new message and not the tail of the last.
	time.Sleep(frameGap)
	if _, err := conn.Write(req); err != nil {
		c.closeConn()
		return nil, err
	}
	frame, err := readFrame(conn, replyWait)
	if err != nil {
		c.closeConn()
		return nil, err
	}
	return frame, nil
}

func (c *Client) ensureConn() error {
	c.connMu.Lock()
	defer c.connMu.Unlock()
	if c.conn != nil {
		return nil
	}
	port := strings.TrimSpace(c.cfg.ComPort)
	if port == "" {
		return fmt.Errorf("psu: no serial port configured")
	}
	p, err := serial.Open(port, &serial.Mode{
		BaudRate: c.cfg.Baud,
		DataBits: 8,
		Parity:   serial.NoParity,
		StopBits: serial.OneStopBit,
	})
	if err != nil {
		return fmt.Errorf("psu: cannot open %s: %w", port, err)
	}
	// Short per-read timeout: readFrame decides when a frame has ended by the
	// quiet between bytes, so each Read must come back promptly with whatever
	// has arrived.
	_ = p.SetReadTimeout(2 * time.Millisecond)
	c.conn = p
	log.Printf("psu: %s open at %d baud, Modbus address %d", port, c.cfg.Baud, c.cfg.Address)
	return nil
}

func (c *Client) closeConn() {
	c.connMu.Lock()
	defer c.connMu.Unlock()
	if c.conn != nil {
		_ = c.conn.Close()
		c.conn = nil
	}
}

// noteFailure records a poll failure and says so ONCE per distinct message.
//
// A supply that is switched off at the mains fails every poll, and a line per
// second and a half would be the whole log file — but saying nothing at all is
// how "it stopped working" arrives with no evidence.
func (c *Client) noteFailure(err error) {
	msg := err.Error()
	c.statusMu.Lock()
	c.last = Status{Connected: false, Error: msg}
	c.statusMu.Unlock()
	if msg != c.lastErr {
		c.lastErr = msg
		log.Printf("psu: %v — retrying every %s, and this will not be logged again until it changes", err, pollEvery)
	}
}
