package main

// Relay auto-control: drives the Station Control relay boards automatically from
// the rig's current frequency / band — the equivalent of PstRotator's "Automatic
// Control". Each relay carries at most one rule:
//   - "freq": ON while the frequency is inside [lo,hi] kHz, OFF otherwise;
//   - "band": ON while the current band is one of the listed bands, OFF otherwise;
//   - "off"/empty: not managed (left to manual control).
//
// Evaluated on every CAT frequency/band change. A relay is only switched when its
// desired state actually changed since the last apply, so a slow relay board isn't
// hammered while you tune within the same range.

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	wruntime "github.com/wailsapp/wails/v2/pkg/runtime"

	"hamlog/internal/applog"
)

const keyRelayAuto = "relayauto.config"

// RelayAutoRule is one relay's automatic-control rule.
type RelayAutoRule struct {
	DeviceID  string   `json:"device_id"`
	Relay     int      `json:"relay"` // 1-based
	Mode      string   `json:"mode"`  // "off" | "freq" | "band"
	FreqLoKHz float64  `json:"freq_lo_khz"`
	FreqHiKHz float64  `json:"freq_hi_khz"`
	Bands     []string `json:"bands"`
}

// RelayAutoConfig is the whole auto-control setup: a master switch + the rules.
type RelayAutoConfig struct {
	Enabled bool            `json:"enabled"`
	Rules   []RelayAutoRule `json:"rules"`
}

// GetRelayAuto returns the relay auto-control configuration for the settings UI.
func (a *App) GetRelayAuto() RelayAutoConfig {
	var cfg RelayAutoConfig
	if a.settings == nil {
		return cfg
	}
	s, _ := a.settings.GetGlobal(a.ctx, keyRelayAuto)
	if strings.TrimSpace(s) != "" {
		_ = json.Unmarshal([]byte(s), &cfg)
	}
	return cfg
}

// SaveRelayAuto persists the configuration and applies it immediately from the
// current rig state, so toggling a rule takes effect without waiting for the next
// frequency change.
func (a *App) SaveRelayAuto(cfg RelayAutoConfig) error {
	if a.settings == nil {
		return fmt.Errorf("db not initialized")
	}
	b, err := json.Marshal(cfg)
	if err != nil {
		return err
	}
	if err := a.settings.SetGlobal(a.ctx, keyRelayAuto, string(b)); err != nil {
		return err
	}
	a.relayAutoOn.Store(cfg.Enabled) // keep the CAT hot-path flag in sync
	// Re-apply from the live frequency so a just-changed rule takes hold now. Also
	// forget the last-applied cache so a rule the user just switched to "off" and
	// back gets re-sent even if the value is unchanged.
	a.relayAutoMu.Lock()
	a.relayAutoLast = map[string]bool{}
	a.relayAutoMu.Unlock()
	if a.cat != nil {
		st := a.cat.State()
		go a.applyRelayAuto(st.FreqHz, st.Band)
	}
	return nil
}

func relayAutoKey(dev string, relay int) string { return dev + "|" + strconv.Itoa(relay) }

func bandInList(bands []string, band string) bool {
	band = strings.ToLower(strings.TrimSpace(band))
	if band == "" {
		return false
	}
	for _, b := range bands {
		if strings.ToLower(strings.TrimSpace(b)) == band {
			return true
		}
	}
	return false
}

// relayAction is one relay's computed desired state for this evaluation.
type relayAction struct {
	dev   string
	relay int
	want  bool
}

// applyRelayAuto evaluates every rule against the current frequency/band and
// switches only the relays that are NOT already in the wanted position. Two things
// it deliberately does NOT do, which used to make the relay clunk on every
// launch/close:
//   - Never acts on an UNKNOWN frequency/band. When the CAT disconnects (app close)
//     the frequency drops to 0; reading that as "out of range" and switching the
//     relay off — then back on at the next launch — was the whole bug.
//   - Never commands a relay already in the right position: on the first evaluation
//     after launch/save it reads the boards' LIVE state, so a relay that's already
//     correct is left untouched instead of being re-sent.
func (a *App) applyRelayAuto(freqHz int64, band string) {
	a.relayAutoMu.Lock()
	defer a.relayAutoMu.Unlock()

	cfg := a.GetRelayAuto()
	if !cfg.Enabled || len(cfg.Rules) == 0 {
		return
	}
	if a.relayAutoLast == nil {
		a.relayAutoLast = map[string]bool{}
	}
	khz := float64(freqHz) / 1000.0
	band = strings.TrimSpace(band)

	// Compute desired states, skipping rules whose input is unknown right now.
	var acts []relayAction
	needLive := false
	for _, r := range cfg.Rules {
		if r.Relay < 1 {
			continue
		}
		var want bool
		switch r.Mode {
		case "freq":
			if freqHz <= 0 {
				continue // no known frequency (CAT off/closing) → leave the relay as-is
			}
			if r.FreqLoKHz <= 0 && r.FreqHiKHz <= 0 {
				continue // unconfigured range
			}
			lo, hi := r.FreqLoKHz, r.FreqHiKHz
			if hi < lo {
				lo, hi = hi, lo
			}
			want = khz >= lo && khz <= hi
		case "band":
			if band == "" {
				continue // no known band → leave the relay as-is
			}
			if len(r.Bands) == 0 {
				continue
			}
			want = bandInList(r.Bands, band)
		default:
			continue // "off"/empty → not managed
		}
		acts = append(acts, relayAction{r.DeviceID, r.Relay, want})
		if _, ok := a.relayAutoLast[relayAutoKey(r.DeviceID, r.Relay)]; !ok {
			needLive = true
		}
	}
	if len(acts) == 0 {
		return
	}

	// First evaluation after launch/save: read the boards' LIVE relay states once
	// so we don't re-command a relay that's already in the wanted position.
	var live map[string]bool
	if needLive {
		live = map[string]bool{}
		for _, ds := range a.GetStationStatus() {
			for _, rl := range ds.Relays {
				live[relayAutoKey(ds.ID, rl.Number)] = rl.On
			}
		}
	}

	changed := false
	for _, ac := range acts {
		key := relayAutoKey(ac.dev, ac.relay)
		cur, known := a.relayAutoLast[key]
		if !known && live != nil {
			cur, known = live[key]
		}
		if known && cur == ac.want {
			a.relayAutoLast[key] = ac.want // already in position — record it, don't switch
			continue
		}
		if err := a.StationSetRelay(ac.dev, ac.relay, ac.want); err != nil {
			applog.Printf("relay auto: set %s relay %d = %v failed: %v", ac.dev, ac.relay, ac.want, err)
			continue // don't cache a failed write — retry next change
		}
		a.relayAutoLast[key] = ac.want
		changed = true
	}

	if changed && a.ctx != nil {
		wruntime.EventsEmit(a.ctx, "station:relay_auto", nil) // nudge the Station Control UI to re-poll
	}
}

// EntryBandChanged drives the band-following features from the QSO ENTRY band
// selector, for a station whose rig OpsLog does not control.
//
// Both of them — relay automatic control and the band-change outbound rows —
// hang off the rig state, and that is right when there IS a rig: changing Band
// in the entry strip pushes a QSY to it, the new state comes back through the
// CAT callback, and the relays follow from there. Without a CAT connection
// nothing is pushed and nothing comes back, so an operator with an antenna
// switch and no rig control changed band in OpsLog and watched the switch sit
// exactly where it was. Reported as automatic control not working; it was never
// told the band had changed.
//
// The frontend calls this ONLY when the CAT push did not happen, and never when
// the band or frequency lock is on — a lock means the entry is deliberately
// decoupled from the rig (logging an old contact off-frequency), and moving an
// antenna to match a QSO from last year is worse than doing nothing.
//
// The frequency is passed as unknown, deliberately: a band selector gives a
// band and nothing else, and rules written on a frequency RANGE must be left
// alone rather than evaluated against a made-up dial reading.
func (a *App) EntryBandChanged(band string) {
	band = strings.TrimSpace(band)
	if band == "" {
		return
	}
	if a.relayAutoOn.Load() {
		go a.applyRelayAuto(0, band)
	}
	a.emitBandChangeTrigger(band, "", 0)
}
