package relaydev

import (
	"context"
	"fmt"
	"strings"
	"sync"

	"go.bug.st/serial"
)

// serialRelay drives the common cheap USB-serial relay boards (CH340 / LCUS-1
// style) that use the "A0" command protocol over a virtual COM port:
//
//	[0xA0] [relay 1-based] [state 0|1] [checksum]     checksum = (0xA0+relay+state) & 0xFF
//
// e.g. relay 1 ON = A0 01 01 A2, relay 1 OFF = A0 01 00 A1. These boards are
// write-only (no state readback), so Status reflects a shadow of what we sent.
// Channel count varies by board (1/2/4/8/16), so it is configurable.
type serialRelay struct {
	portName string
	count    int
	baud     int
	mu       sync.Mutex
	port     serial.Port
	shadow   []bool
}

// NewSerialRelay builds a driver for a CH340/LCUS-style USB-serial relay board on
// the given COM port with `count` channels (defaults to 8).
func NewSerialRelay(port string, count int) Device {
	if count < 1 {
		count = 8
	}
	return &serialRelay{portName: strings.TrimSpace(port), count: count, baud: 9600, shadow: make([]bool, count)}
}

func (s *serialRelay) Count() int { return s.count }

// Close releases the COM port so it can be reopened later (only one handle may
// hold a serial port at a time).
func (s *serialRelay) Close() error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.port != nil {
		err := s.port.Close()
		s.port = nil
		return err
	}
	return nil
}

func (s *serialRelay) ensureOpen() error {
	if s.port != nil {
		return nil
	}
	if s.portName == "" {
		return fmt.Errorf("no COM port set for the USB relay board")
	}
	p, err := serial.Open(s.portName, &serial.Mode{BaudRate: s.baud})
	if err != nil {
		return fmt.Errorf("open %s: %w", s.portName, err)
	}
	s.port = p
	return nil
}

func (s *serialRelay) Set(ctx context.Context, relay int, on bool) error {
	if relay < 1 || relay > s.count {
		return fmt.Errorf("relay %d out of range 1..%d", relay, s.count)
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if err := s.ensureOpen(); err != nil {
		return err
	}
	st := byte(0)
	if on {
		st = 1
	}
	r := byte(relay)
	frame := []byte{0xA0, r, st, byte(0xA0) + r + st} // last byte = checksum
	if _, err := s.port.Write(frame); err != nil {
		// Drop the handle so the next call reopens (USB unplugged / port reset).
		s.port.Close()
		s.port = nil
		return fmt.Errorf("write to %s: %w", s.portName, err)
	}
	s.shadow[relay-1] = on
	return nil
}

func (s *serialRelay) Status(ctx context.Context) ([]bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if err := s.ensureOpen(); err != nil {
		return nil, err
	}
	out := make([]bool, s.count)
	copy(out, s.shadow)
	return out, nil
}
