// Package solar fetches live space-weather / propagation numbers (solar flux,
// sunspot number, A/K indices, X-ray, geomagnetic field) from N0NBH's public
// hamqsl.com XML feed, caches them, and refreshes periodically. OpsLog shows the
// numbers in a header strip and stamps SFI / A_INDEX / K_INDEX onto each logged
// QSO (SSN has no standard ADIF field — the app stores it as an extra).
package solar

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

// feedURL is N0NBH's solar data as XML. Free, no key.
const feedURL = "https://www.hamqsl.com/solarxml.php"

// Data is the parsed snapshot exposed to the UI (strings — some fields read
// "No Report" or carry units, so we keep them verbatim and parse to numbers only
// when stamping a QSO).
type Data struct {
	SFI         string    `json:"sfi"`          // solar flux index
	SSN         string    `json:"ssn"`          // sunspot number
	AIndex      string    `json:"a_index"`      // geomagnetic A index
	KIndex      string    `json:"k_index"`      // geomagnetic K index
	XRay        string    `json:"xray"`         // X-ray flux class (e.g. C1.5)
	GeomagField string    `json:"geomag_field"` // QUIET / UNSETTLED / STORM…
	Aurora      string    `json:"aurora"`
	MUF         string    `json:"muf"`
	Updated     string    `json:"updated"` // source timestamp text
	Source      string    `json:"source"`  // e.g. N0NBH
	OK          bool      `json:"ok"`      // last fetch succeeded
	Error       string    `json:"error,omitempty"`
	FetchedAt   time.Time `json:"fetched_at"`
}

// xmlSolar mirrors the hamqsl solarxml.php document.
type xmlSolar struct {
	XMLName   xml.Name `xml:"solar"`
	SolarData struct {
		Source      string `xml:"source"`
		Updated     string `xml:"updated"`
		SolarFlux   string `xml:"solarflux"`
		AIndex      string `xml:"aindex"`
		KIndex      string `xml:"kindex"`
		XRay        string `xml:"xray"`
		Sunspots    string `xml:"sunspots"`
		Aurora      string `xml:"aurora"`
		GeomagField string `xml:"geomagfield"`
		MUF         string `xml:"muf"`
	} `xml:"solardata"`
}

// Fetch performs one GET + parse of the feed.
func Fetch(ctx context.Context, client *http.Client) (Data, error) {
	if client == nil {
		client = &http.Client{Timeout: 20 * time.Second}
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, feedURL, nil)
	if err != nil {
		return Data{}, err
	}
	req.Header.Set("User-Agent", "OpsLog")
	resp, err := client.Do(req)
	if err != nil {
		return Data{}, fmt.Errorf("solar: request failed: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return Data{}, fmt.Errorf("solar: read: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		return Data{}, fmt.Errorf("solar: http %d", resp.StatusCode)
	}
	return parseSolar(body)
}

// parseSolar turns the feed's XML bytes into a Data (split out so it can be
// unit-tested without a network call).
func parseSolar(body []byte) (Data, error) {
	var x xmlSolar
	if err := xml.Unmarshal(body, &x); err != nil {
		return Data{}, fmt.Errorf("solar: parse: %w", err)
	}
	s := x.SolarData
	return Data{
		SFI:         strings.TrimSpace(s.SolarFlux),
		SSN:         strings.TrimSpace(s.Sunspots),
		AIndex:      strings.TrimSpace(s.AIndex),
		KIndex:      strings.TrimSpace(s.KIndex),
		XRay:        strings.TrimSpace(s.XRay),
		GeomagField: strings.TrimSpace(s.GeomagField),
		Aurora:      strings.TrimSpace(s.Aurora),
		MUF:         strings.TrimSpace(s.MUF),
		Updated:     strings.TrimSpace(s.Updated),
		Source:      strings.TrimSpace(s.Source),
		OK:          true,
		FetchedAt:   time.Now(),
	}, nil
}

// Manager caches the latest Data and refreshes it on a timer.
type Manager struct {
	mu       sync.RWMutex
	data     Data
	client   *http.Client
	onChange func()
	stop     chan struct{}
	once     sync.Once
}

// NewManager builds a manager. onChange (optional) fires after each refresh so
// the host can push a UI event.
func NewManager(onChange func()) *Manager {
	return &Manager{
		client:   &http.Client{Timeout: 20 * time.Second},
		onChange: onChange,
		stop:     make(chan struct{}),
	}
}

// Start fetches now and every hour until Stop (the feed updates roughly hourly).
func (m *Manager) Start() {
	go func() {
		m.refresh()
		t := time.NewTicker(1 * time.Hour)
		defer t.Stop()
		for {
			select {
			case <-m.stop:
				return
			case <-t.C:
				m.refresh()
			}
		}
	}()
}

// Stop ends the refresh loop.
func (m *Manager) Stop() {
	if m == nil {
		return
	}
	m.once.Do(func() { close(m.stop) })
}

// Get returns the latest snapshot.
func (m *Manager) Get() Data {
	if m == nil {
		return Data{}
	}
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.data
}

// Refresh forces an immediate fetch (used by a manual "refresh" binding).
func (m *Manager) Refresh() { m.refresh() }

func (m *Manager) refresh() {
	d, err := Fetch(context.Background(), m.client)
	m.mu.Lock()
	if err != nil {
		m.data.OK = false
		m.data.Error = err.Error()
	} else {
		m.data = d
	}
	m.mu.Unlock()
	if m.onChange != nil {
		m.onChange()
	}
}
