package watchlist

// ClubLog enrichment — phase 2 of the DXHunter port. clublog.org/watch.php
// answers, per callsign: is this an expedition, does it run OQRS, is there a
// LiveStream, the log's QSO total and the last 24 hours' rate. That block has
// been in the Entry schema since phase 1 (the file round-trips DXHunter's);
// this file finally fills it.

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

// WatchData is clublog.org/watch.php's answer, reduced to what the tab shows.
type WatchData struct {
	IsExpedition bool `json:"is_expedition"`
	HasOQRS      bool `json:"has_oqrs"`
	LiveStream   bool `json:"livestream"`
	ClubLogInfo  *struct {
		TotalQSOs int `json:"total_qsos"`
	} `json:"clublog_info"`
	// {} when populated, [] when empty — RawMessage swallows both shapes.
	QSOsPerBand json.RawMessage `json:"24h_qsos_per_band_mode"`
}

// QSOs24h sums the per-band/per-mode counts.
func (d *WatchData) QSOs24h() int {
	if len(d.QSOsPerBand) == 0 {
		return 0
	}
	var parsed map[string]map[string]int
	if err := json.Unmarshal(d.QSOsPerBand, &parsed); err != nil {
		return 0 // "[]" for an empty answer lands here, which is the right zero
	}
	n := 0
	for _, modes := range parsed {
		for _, c := range modes {
			n += c
		}
	}
	return n
}

// FetchWatch asks ClubLog about one callsign. A 404 is "ClubLog has no log for
// this call" — an ordinary answer, not an error.
func FetchWatch(ctx context.Context, client *http.Client, apiKey, callsign string) (*WatchData, error) {
	url := fmt.Sprintf("https://clublog.org/watch.php?call=%s&api=%s",
		strings.ToUpper(strings.TrimSpace(callsign)), apiKey)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "OpsLog")
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotFound {
		return &WatchData{}, nil
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("clublog watch %s: HTTP %d", callsign, resp.StatusCode)
	}
	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	if len(strings.TrimSpace(string(body))) == 0 {
		return &WatchData{}, nil
	}
	var data WatchData
	if err := json.Unmarshal(body, &data); err != nil {
		return nil, fmt.Errorf("clublog watch %s: %w", callsign, err)
	}
	return &data, nil
}

// ApplyClubLog writes one answer into the entry and reports whether anything
// the tab shows actually changed — the caller only broadcasts on true.
func (s *Store) ApplyClubLog(callsign string, d *WatchData) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	e, ok := s.entries[strings.ToUpper(strings.TrimSpace(callsign))]
	if !ok {
		return false
	}
	total := e.ClubLogTotalQSOs
	if d.ClubLogInfo != nil {
		total = d.ClubLogInfo.TotalQSOs
	}
	q24 := d.QSOs24h()
	changed := e.IsExpedition != d.IsExpedition || e.ClubLogHasOQRS != d.HasOQRS ||
		e.ClubLogLiveStream != d.LiveStream || e.ClubLogTotalQSOs != total ||
		e.ClubLogQSOs24h != q24
	e.IsExpedition = d.IsExpedition
	e.ClubLogHasOQRS = d.HasOQRS
	e.ClubLogLiveStream = d.LiveStream
	e.ClubLogTotalQSOs = total
	e.ClubLogQSOs24h = q24
	e.ClubLogUpdatedAt = time.Now()
	s.scheduleSave()
	return changed
}

// StaleEntries lists the callsigns whose ClubLog block is due a refresh.
// Expeditions age faster — their totals are the interesting number and move by
// the hour; a quiet call is asked about four times a day at most.
func (s *Store) StaleEntries(expTTL, otherTTL time.Duration) []string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	now := time.Now()
	var out []string
	for call, e := range s.entries {
		// A starred pattern is a family, not a station — ClubLog has no log
		// for "VK9*". The base (star stripped) is what gets asked about.
		ttl := otherTTL
		if e.IsExpedition {
			ttl = expTTL
		}
		if now.Sub(e.ClubLogUpdatedAt) >= ttl {
			out = append(out, call)
		}
	}
	return out
}
