package main

// Several radios, and the one that is on the air.
//
// Until now the answer to "I have two rigs" was "make two profiles", which is a
// heavy instrument for a light question: a profile carries the whole station —
// logbook, callsign, awards, cluster, macros — and switching one to change
// which radio is connected takes all of that with it, including a logbook
// change if the profiles point at different databases.
//
// So radios are a LIST, the way amplifiers already are, and switching is one
// click on the CAT chip in the status bar. Nothing else about the station
// moves.
//
// The storage deliberately keeps the ACTIVE radio in the settings keys the rest
// of OpsLog already reads (cat.backend, cat.icom_port, …). Switching writes the
// chosen entry into those keys and reloads the link, so every consumer — the
// consoles, the CAT sharing, the band-follow, the panels — sees exactly what it
// saw before and needed no change at all. The list is a second store beside it,
// not a replacement for it.

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

	"hamlog/internal/applog"
)

const (
	// keyRadiosList holds the saved radios as JSON.
	keyRadiosList = "cat.radios.json"
	// keyRadioActive is the id of the one currently connected.
	keyRadioActive = "cat.radios.active"
)

// RadioConfig is one saved radio: a name and the CAT settings that reach it.
type RadioConfig struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	// Settings is the whole CAT configuration for this radio — the same shape
	// the CAT panel has always edited, so a saved radio is exactly "what the
	// settings said the day it was saved".
	Settings CATSettings `json:"settings"`
	// MyRig is what goes into MY_RIG on a QSO made with this radio.
	//
	// It belongs here rather than in Operating conditions once there is more
	// than one rig: the operating conditions describe a PLAN — "on 20 m I use
	// the beam and the 7300" — while this is the fact of which radio is keying.
	// Left empty, nothing changes: the old chain (band default, then the
	// profile's rig) answers exactly as it did.
	MyRig string `json:"my_rig"`
}

// radioLabel is what the status bar shows when the operator never named one.
func radioLabel(c RadioConfig, i int) string {
	if n := strings.TrimSpace(c.Name); n != "" {
		return n
	}
	if b := strings.TrimSpace(c.Settings.Backend); b != "" {
		return strings.ToUpper(b)
	}
	return fmt.Sprintf("Radio %d", i+1)
}

// GetRadios returns the saved radios.
//
// When nothing was ever saved, the CURRENT settings are presented as the single
// entry — so an operator who has been running one rig for a year opens the list
// and finds it there, rather than an empty box suggesting their configuration
// has been lost. It is persisted on the next save, not here: reading a list
// should not write one.
func (a *App) GetRadios() ([]RadioConfig, error) {
	if a.settings == nil {
		return nil, fmt.Errorf("db not initialized")
	}
	raw := a.settingOr(keyRadiosList, "")
	if strings.TrimSpace(raw) != "" {
		var list []RadioConfig
		if err := json.Unmarshal([]byte(raw), &list); err == nil && len(list) > 0 {
			return list, nil
		}
	}
	cur, err := a.GetCATSettings()
	if err != nil {
		return nil, err
	}
	return []RadioConfig{{ID: "radio-1", Name: "", Settings: cur}}, nil
}

// SaveRadios stores the list, giving an id to anything new.
func (a *App) SaveRadios(list []RadioConfig) error {
	if a.settings == nil {
		return fmt.Errorf("db not initialized")
	}
	for i := range list {
		if strings.TrimSpace(list[i].ID) == "" {
			list[i].ID = fmt.Sprintf("radio-%d-%d", time.Now().Unix(), i)
		}
	}
	b, err := json.Marshal(list)
	if err != nil {
		return err
	}
	return a.settings.Set(a.ctx, keyRadiosList, string(b))
}

// ActiveRadioID is the id of the radio currently on the air, or the first one
// when nothing was ever chosen.
func (a *App) ActiveRadioID() string {
	id := strings.TrimSpace(a.settingOr(keyRadioActive, ""))
	list, err := a.GetRadios()
	if err != nil || len(list) == 0 {
		return id
	}
	for _, r := range list {
		if r.ID == id {
			return id
		}
	}
	// The stored id names a radio that has since been deleted. The first one is
	// a better answer than an empty selection: the CAT link is up, and it is up
	// on SOMETHING.
	return list[0].ID
}

// RadioListEntry is what the status-bar menu needs: enough to draw a row.
//
// Name is what the OPERATOR typed, empty when they typed nothing; Label is what
// to draw when there is no better idea. The two are separate because the caller
// has a better idea than we do: the status bar knows what the radio calls
// itself over CAT, and "FTDX10" beats "Radio 2" — but only where the operator
// has not given it a name of their own, which beats both.
type RadioListEntry struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Label   string `json:"label"`
	Backend string `json:"backend"`
	Active  bool   `json:"active"`
}

// ListRadios is the status bar's view of the list.
func (a *App) ListRadios() []RadioListEntry {
	list, err := a.GetRadios()
	if err != nil {
		return nil
	}
	active := a.ActiveRadioID()
	out := make([]RadioListEntry, 0, len(list))
	for i, r := range list {
		out = append(out, RadioListEntry{
			ID: r.ID, Name: strings.TrimSpace(r.Name), Label: radioLabel(r, i),
			Backend: r.Settings.Backend, Active: r.ID == active,
		})
	}
	return out
}

// SetActiveRadio connects the given radio and leaves the rest of the station
// alone.
//
// The chosen entry's settings become THE CAT settings — SaveCATSettings writes
// them and restarts the link — so switching rigs is the same operation as
// editing the CAT panel and pressing Save, which is a path that already works
// everywhere it needs to.
func (a *App) SetActiveRadio(id string) error {
	list, err := a.GetRadios()
	if err != nil {
		return err
	}
	id = strings.TrimSpace(id)
	for i, r := range list {
		if r.ID != id {
			continue
		}
		// Persisted BEFORE the link is rebuilt: reloadCAT can take a moment on a
		// radio that is switched off, and an operator who closes OpsLog during
		// that moment should still come back to the rig they chose.
		a.setSetting(keyRadioActive, id)
		// The list is written back as well when it was only ever implicit, so
		// the first switch is also what makes the list real.
		if strings.TrimSpace(a.settingOr(keyRadiosList, "")) == "" {
			_ = a.SaveRadios(list)
		}
		applog.Printf("cat: switching to %q (%s)", radioLabel(r, i), r.Settings.Backend)
		return a.SaveCATSettings(r.Settings)
	}
	return fmt.Errorf("no radio with id %q", id)
}

// syncActiveRadio writes the settings just saved into the active entry of the
// list, so the list and the live configuration never disagree.
//
// Only when a list actually exists: an operator with one radio who has never
// opened the list has nothing to keep in step, and writing one here would
// create a list as a side effect of saving the CAT panel.
func (a *App) syncActiveRadio(s CATSettings) {
	if a.settings == nil || strings.TrimSpace(a.settingOr(keyRadiosList, "")) == "" {
		return
	}
	list, err := a.GetRadios()
	if err != nil {
		return
	}
	id := a.ActiveRadioID()
	for i := range list {
		if list[i].ID == id {
			list[i].Settings = s
			_ = a.SaveRadios(list)
			return
		}
	}
}

// activeRadioMyRig is the MY_RIG of the radio currently connected, or "".
//
// Consulted BEFORE the per-band default, and deliberately: the band default is
// what the operator planned to use on that band, this is which radio is
// actually on the air. When they disagree — a second rig borrowed for one
// evening on 20 m — the one that is transmitting is the true answer.
func (a *App) activeRadioMyRig() string {
	if a.settings == nil || strings.TrimSpace(a.settingOr(keyRadiosList, "")) == "" {
		return "" // no list was ever made: nothing to say, nothing changes
	}
	list, err := a.GetRadios()
	if err != nil {
		return ""
	}
	id := a.ActiveRadioID()
	for _, r := range list {
		if r.ID == id {
			return strings.TrimSpace(r.MyRig)
		}
	}
	return ""
}

// ActiveRadioMyRig exposes the connected radio's MY_RIG to the frontend, which
// pre-fills the entry form's My-station fields on every band change. Without
// this the per-band default landed in the field FIRST, and the log-time
// priority (the radio that is keying beats the radio that was planned) never
// ran — the field was no longer empty by the time the backend looked.
func (a *App) ActiveRadioMyRig() string {
	return a.activeRadioMyRig()
}
