// Package netctl persists "NET" definitions and their station rosters for the
// NET Control feature (managing a directed net / round-table on a frequency).
//
// A NET is a named net (e.g. "French QSO", "QSO des Brasses") with a roster of
// stations that habitually check in. The roster grows over time as you add new
// callsigns. Storage is a single JSON file in the data dir — global/shared
// across all logbooks (a net like "French QSO" is reused whatever logbook is
// open). The QSOs themselves are logged into the active logbook by the caller;
// this package only owns the net definitions + rosters, not the live session.
package netctl

import (
	"encoding/json"
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

// Station is one roster entry: a station registered in a net.
type Station struct {
	Callsign string  `json:"callsign"`
	Name     string  `json:"name,omitempty"`
	QTH      string  `json:"qth,omitempty"`
	Country  string  `json:"country,omitempty"`
	DXCC     int     `json:"dxcc,omitempty"`
	ITU      int     `json:"itu,omitempty"`
	CQ       int     `json:"cq,omitempty"`
	// Full lookup detail so a roster contact carries EVERYTHING into the QSO when
	// put on air (was previously only name/qth — grid/address/etc. were lost).
	Grid    string  `json:"grid,omitempty"`
	Address string  `json:"address,omitempty"`
	State   string  `json:"state,omitempty"`
	Cnty    string  `json:"cnty,omitempty"`
	Cont    string  `json:"cont,omitempty"`
	Lat     float64 `json:"lat,omitempty"`
	Lon     float64 `json:"lon,omitempty"`
	Email   string  `json:"email,omitempty"`
	Groups  string  `json:"groups,omitempty"`
	SIG     string  `json:"sig,omitempty"`
	SIGInfo string  `json:"sig_info,omitempty"`
}

// Net is a named net with default report values and a station roster.
type Net struct {
	ID             string    `json:"id"`
	Name           string    `json:"name"`
	DefaultRSTSent string    `json:"default_rst_sent,omitempty"`
	DefaultRSTRcvd string    `json:"default_rst_rcvd,omitempty"`
	DefaultComment string    `json:"default_comment,omitempty"`
	Stations       []Station `json:"stations,omitempty"`
}

// Store is the persistent collection of nets, backed by a JSON file.
type Store struct {
	mu   sync.Mutex
	path string
	nets []Net
}

// Open loads the store from path (creating an empty one if the file is absent).
func Open(path string) (*Store, error) {
	s := &Store{path: path}
	b, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return s, nil
		}
		return nil, err
	}
	if len(b) > 0 {
		if err := json.Unmarshal(b, &s.nets); err != nil {
			// Corrupt file: start empty rather than failing the whole app.
			s.nets = nil
		}
	}
	return s, nil
}

// save writes the current state to disk. Caller must hold s.mu.
func (s *Store) save() error {
	b, err := json.MarshalIndent(s.nets, "", "  ")
	if err != nil {
		return err
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, b, 0o644); err != nil {
		return err
	}
	return os.Rename(tmp, s.path)
}

func newID() string { return strconv.FormatInt(time.Now().UnixNano(), 36) }

// List returns a copy of all nets (with rosters), ordered by name.
func (s *Store) List() []Net {
	s.mu.Lock()
	defer s.mu.Unlock()
	out := make([]Net, len(s.nets))
	copy(out, s.nets)
	sort.Slice(out, func(i, j int) bool {
		return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
	})
	return out
}

// find returns the index of the net with id, or -1. Caller must hold s.mu.
func (s *Store) find(id string) int {
	for i := range s.nets {
		if s.nets[i].ID == id {
			return i
		}
	}
	return -1
}

// Create adds a new net with default reports of 59/59 and returns it.
func (s *Store) Create(name string) (Net, error) {
	name = strings.TrimSpace(name)
	if name == "" {
		return Net{}, fmt.Errorf("net name required")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	for i := range s.nets {
		if strings.EqualFold(s.nets[i].Name, name) {
			return Net{}, fmt.Errorf("a net named %q already exists", name)
		}
	}
	n := Net{ID: newID(), Name: name, DefaultRSTSent: "59", DefaultRSTRcvd: "59"}
	s.nets = append(s.nets, n)
	return n, s.save()
}

// Rename changes a net's name.
func (s *Store) Rename(id, name string) error {
	name = strings.TrimSpace(name)
	if name == "" {
		return fmt.Errorf("net name required")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	i := s.find(id)
	if i < 0 {
		return fmt.Errorf("net not found")
	}
	s.nets[i].Name = name
	return s.save()
}

// SetDefaults updates the per-net default report/comment values.
func (s *Store) SetDefaults(id, rstSent, rstRcvd, comment string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	i := s.find(id)
	if i < 0 {
		return fmt.Errorf("net not found")
	}
	s.nets[i].DefaultRSTSent = rstSent
	s.nets[i].DefaultRSTRcvd = rstRcvd
	s.nets[i].DefaultComment = comment
	return s.save()
}

// Delete removes a net and its roster.
func (s *Store) Delete(id string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	i := s.find(id)
	if i < 0 {
		return fmt.Errorf("net not found")
	}
	s.nets = append(s.nets[:i], s.nets[i+1:]...)
	return s.save()
}

// Get returns a copy of one net by id.
func (s *Store) Get(id string) (Net, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	i := s.find(id)
	if i < 0 {
		return Net{}, false
	}
	return s.nets[i], true
}

// Roster returns a net's stations, sorted by callsign.
func (s *Store) Roster(id string) ([]Station, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	i := s.find(id)
	if i < 0 {
		return nil, fmt.Errorf("net not found")
	}
	out := make([]Station, len(s.nets[i].Stations))
	copy(out, s.nets[i].Stations)
	sort.Slice(out, func(a, b int) bool { return out[a].Callsign < out[b].Callsign })
	return out, nil
}

// RosterUpsert adds st to the net's roster, or updates it if the callsign is
// already present (matched case-insensitively; the callsign is stored upper).
func (s *Store) RosterUpsert(id string, st Station) error {
	st.Callsign = strings.ToUpper(strings.TrimSpace(st.Callsign))
	if st.Callsign == "" {
		return fmt.Errorf("callsign required")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	i := s.find(id)
	if i < 0 {
		return fmt.Errorf("net not found")
	}
	for j := range s.nets[i].Stations {
		if strings.EqualFold(s.nets[i].Stations[j].Callsign, st.Callsign) {
			s.nets[i].Stations[j] = st
			return s.save()
		}
	}
	s.nets[i].Stations = append(s.nets[i].Stations, st)
	return s.save()
}

// RosterRemove deletes a callsign from a net's roster.
func (s *Store) RosterRemove(id, callsign string) error {
	callsign = strings.ToUpper(strings.TrimSpace(callsign))
	s.mu.Lock()
	defer s.mu.Unlock()
	i := s.find(id)
	if i < 0 {
		return fmt.Errorf("net not found")
	}
	for j := range s.nets[i].Stations {
		if strings.EqualFold(s.nets[i].Stations[j].Callsign, callsign) {
			s.nets[i].Stations = append(s.nets[i].Stations[:j], s.nets[i].Stations[j+1:]...)
			return s.save()
		}
	}
	return nil // not present → nothing to do
}
