package clublog

// mostwanted.go — the ClubLog "Most Wanted" DXCC ranking. ClubLog publishes, per
// callsign, how wanted each DXCC entity is (rank 1 = the single most wanted, e.g.
// P5 North Korea). We fetch it, invert it to a DXCC-number → rank map, cache it on
// disk (personalised to the operator's callsign) and refresh daily. The rank is
// surfaced next to the entity in the entry-strip band/slot matrix.
//
// Endpoint (same one DXHunter uses): mostwanted.php?api=1 returns JSON
//   { "1": "344", "2": "123", ... }   rank → ADIF (DXCC) number
// Passing &callsign=<CALL> personalises the ranking to that operator's log.
// api=1 is just the "return JSON" flag — no API key is needed here.

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

const mwURL = "https://clublog.org/mostwanted.php?api=1"
const mwFile = "clublog_mostwanted.json"

// mwUserAgent — ClubLog's nginx 403s the default Go user-agent, so set a real one.
const mwUserAgent = "OpsLog (amateur radio logger)"

// mwCache is the on-disk shape (ranks keyed by DXCC number as a string for JSON).
type mwCache struct {
	Callsign  string         `json:"callsign"`
	FetchedAt time.Time      `json:"fetched_at"`
	Ranks     map[string]int `json:"ranks"`
}

// MostWanted owns the on-disk cache and the parsed DXCC-number → rank map.
type MostWanted struct {
	dir string

	mu      sync.RWMutex
	ranks   map[int]int // dxcc number → rank (1 = most wanted)
	call    string      // callsign the current ranks were fetched for
	fetched time.Time
}

func NewMostWanted(dataDir string) *MostWanted {
	return &MostWanted{dir: dataDir, ranks: map[int]int{}}
}

func (m *MostWanted) path() string { return filepath.Join(m.dir, mwFile) }

// Rank returns the most-wanted rank for a DXCC number, or 0 if unknown/unloaded.
func (m *MostWanted) Rank(dxcc int) int {
	if dxcc <= 0 {
		return 0
	}
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.ranks[dxcc]
}

// Ranks returns a copy of the whole DXCC-number → rank map.
func (m *MostWanted) Ranks() map[int]int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	out := make(map[int]int, len(m.ranks))
	for k, v := range m.ranks {
		out[k] = v
	}
	return out
}

// Info reports the callsign the list was fetched for, its date, and entity count.
func (m *MostWanted) Info() (call, date string, count int) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	d := ""
	if !m.fetched.IsZero() {
		d = m.fetched.Format("2006-01-02")
	}
	return m.call, d, len(m.ranks)
}

func (m *MostWanted) Loaded() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return len(m.ranks) > 0
}

// LoadFromDisk reads the cached JSON into memory. Does NOT download.
func (m *MostWanted) LoadFromDisk() error {
	b, err := os.ReadFile(m.path())
	if err != nil {
		return err
	}
	var c mwCache
	if err := json.Unmarshal(b, &c); err != nil {
		return err
	}
	ranks := make(map[int]int, len(c.Ranks))
	for k, v := range c.Ranks {
		if n, err := strconv.Atoi(k); err == nil && n > 0 && v > 0 {
			ranks[n] = v
		}
	}
	m.mu.Lock()
	m.ranks = ranks
	m.call = c.Callsign
	m.fetched = c.FetchedAt
	m.mu.Unlock()
	return nil
}

// NeedsRefresh reports whether the list is empty, older than maxAge, or was
// fetched for a DIFFERENT callsign (the ranking is personalised, so a profile
// switch invalidates it).
func (m *MostWanted) NeedsRefresh(callsign string, maxAge time.Duration) bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if len(m.ranks) == 0 {
		return true
	}
	if !strings.EqualFold(strings.TrimSpace(callsign), m.call) {
		return true
	}
	return time.Since(m.fetched) > maxAge
}

// Download fetches the most-wanted list for callsign, writes it atomically, and
// swaps it into memory.
func (m *MostWanted) Download(ctx context.Context, callsign string) error {
	if err := os.MkdirAll(m.dir, 0o755); err != nil {
		return err
	}
	call := strings.ToUpper(strings.TrimSpace(callsign))
	url := mwURL
	if call != "" {
		url += "&callsign=" + call
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	req.Header.Set("User-Agent", mwUserAgent)
	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("download: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("clublog mostwanted HTTP %d", resp.StatusCode)
	}
	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return err
	}
	// { "1": "344", "2": "123", ... } — rank → ADIF (DXCC) number.
	var raw map[string]string
	if err := json.Unmarshal(body, &raw); err != nil {
		return fmt.Errorf("clublog mostwanted parse: %w", err)
	}
	ranks := make(map[int]int, len(raw))
	strRanks := make(map[string]int, len(raw))
	for rankStr, adif := range raw {
		rank, e1 := strconv.Atoi(strings.TrimSpace(rankStr))
		dxcc, e2 := strconv.Atoi(strings.TrimSpace(adif))
		if e1 == nil && e2 == nil && rank > 0 && dxcc > 0 {
			ranks[dxcc] = rank
			strRanks[adif] = rank
		}
	}
	if len(ranks) == 0 {
		return fmt.Errorf("clublog mostwanted: empty/invalid response")
	}
	now := time.Now()
	if b, err := json.Marshal(mwCache{Callsign: call, FetchedAt: now, Ranks: strRanks}); err == nil {
		tmp := m.path() + ".tmp"
		if os.WriteFile(tmp, b, 0o644) == nil {
			_ = os.Rename(tmp, m.path())
		}
	}
	m.mu.Lock()
	m.ranks = ranks
	m.call = call
	m.fetched = now
	m.mu.Unlock()
	return nil
}
