// Package relaydev drives network relay boards used for station control (power
// sequencing, switching accessories). Two devices are supported, both over HTTP:
//
//   - WebSwitch 1216H — 5 relays. Control: GET /relaycontrol/{on|off}/{n};
//     status: GET /relaystate/get2/1$2$3$4$5$ → lines "n,state".
//     (Protocol taken from the operator's own working ShackMaster driver.)
//   - KMTronic LAN 8-relay WEB board — 8 relays. Control: GET /FF{rr}{ss}
//     (rr = 01..08, ss = 01 on / 00 off); status: GET /status.xml with
//     <relay1>..<relay8> (relay0 is reserved). Optional HTTP basic auth.
//
// A Device presents the same surface to the app regardless of wire protocol.
package relaydev

import (
	"context"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// Device is one relay board.
type Device interface {
	Count() int                                       // number of user-controllable relays
	Status(ctx context.Context) ([]bool, error)       // state of each relay (index 0 = relay 1)
	Set(ctx context.Context, relay int, on bool) error // relay is 1-based
}

func httpClient() *http.Client { return &http.Client{Timeout: 5 * time.Second} }

// get issues a GET with optional basic auth and returns the body on 2xx.
func get(ctx context.Context, url, user, pass string) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	if user != "" || pass != "" {
		req.SetBasicAuth(user, pass)
	}
	resp, err := httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}
	return body, nil
}

// ── WebSwitch 1216H ────────────────────────────────────────────────────

type webswitch struct {
	host  string
	count int
}

// NewWebswitch builds a WebSwitch 1216H client (5 relays).
func NewWebswitch(host string) Device { return &webswitch{host: host, count: 5} }

func (w *webswitch) Count() int { return w.count }

func (w *webswitch) Set(ctx context.Context, relay int, on bool) error {
	if relay < 1 || relay > w.count {
		return fmt.Errorf("relay %d out of range 1..%d", relay, w.count)
	}
	action := "off"
	if on {
		action = "on"
	}
	_, err := get(ctx, fmt.Sprintf("http://%s/relaycontrol/%s/%d", w.host, action, relay), "", "")
	return err
}

func (w *webswitch) Status(ctx context.Context) ([]bool, error) {
	// Build the "1$2$3$..." selector the device expects.
	var sel strings.Builder
	for i := 1; i <= w.count; i++ {
		sel.WriteString(strconv.Itoa(i))
		sel.WriteByte('$')
	}
	body, err := get(ctx, fmt.Sprintf("http://%s/relaystate/get2/%s", w.host, sel.String()), "", "")
	if err != nil {
		return nil, err
	}
	out := make([]bool, w.count)
	// Lines "n,state" — "1,1", "2,0", …
	for _, line := range strings.Split(strings.TrimSpace(string(body)), "\n") {
		parts := strings.Split(strings.TrimSpace(line), ",")
		if len(parts) != 2 {
			continue
		}
		n, e1 := strconv.Atoi(parts[0])
		st, e2 := strconv.Atoi(strings.TrimSpace(parts[1]))
		if e1 != nil || e2 != nil || n < 1 || n > w.count {
			continue
		}
		out[n-1] = st == 1
	}
	return out, nil
}

// ── KMTronic LAN 8-relay WEB ───────────────────────────────────────────

type kmtronic struct {
	host       string
	user, pass string
	count      int
}

// NewKMTronic builds a KMTronic LAN WEB relay client (8 relays). user/pass are
// blank unless the board's HTTP authentication is enabled.
func NewKMTronic(host, user, pass string) Device {
	return &kmtronic{host: host, user: user, pass: pass, count: 8}
}

func (k *kmtronic) Count() int { return k.count }

func (k *kmtronic) Set(ctx context.Context, relay int, on bool) error {
	if relay < 1 || relay > k.count {
		return fmt.Errorf("relay %d out of range 1..%d", relay, k.count)
	}
	state := "00"
	if on {
		state = "01"
	}
	// FF<rr><ss>: e.g. FF0101 = relay 1 on, FF0800 = relay 8 off.
	_, err := get(ctx, fmt.Sprintf("http://%s/FF%02d%s", k.host, relay, state), k.user, k.pass)
	return err
}

// kmStatus mirrors status.xml. relay0 is reserved; relay1..relay8 are the board.
type kmStatus struct {
	XMLName xml.Name `xml:"response"`
	Relays  []struct {
		XMLName xml.Name
		Value   string `xml:",chardata"`
	} `xml:",any"`
}

func (k *kmtronic) Status(ctx context.Context) ([]bool, error) {
	body, err := get(ctx, fmt.Sprintf("http://%s/status.xml", k.host), k.user, k.pass)
	if err != nil {
		return nil, err
	}
	var s kmStatus
	if err := xml.Unmarshal(body, &s); err != nil {
		return nil, fmt.Errorf("kmtronic: bad status.xml: %w", err)
	}
	out := make([]bool, k.count)
	for _, r := range s.Relays {
		// Element names are relay0..relay8; relay0 is reserved.
		name := r.XMLName.Local
		if !strings.HasPrefix(name, "relay") {
			continue
		}
		n, e := strconv.Atoi(strings.TrimPrefix(name, "relay"))
		if e != nil || n < 1 || n > k.count {
			continue
		}
		out[n-1] = strings.TrimSpace(r.Value) == "1"
	}
	return out, nil
}
