// Package pota polls the Parks On The Air activator-spots API and exposes a
// fast in-memory lookup so DX-cluster spots can be tagged "this station is
// currently activating a park". No API key required.
package pota

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

const apiURL = "https://api.pota.app/spot/activator"

// Info is the park data we surface for a currently-active activator.
type Info struct {
	Reference    string `json:"reference"`     // park id, e.g. "US-2072"
	ParkName     string `json:"park_name"`     // human name
	LocationDesc string `json:"location_desc"` // e.g. "US-NY"
}

// apiSpot is the subset of the POTA API record we read.
type apiSpot struct {
	Activator    string `json:"activator"`
	Reference    string `json:"reference"`
	ParkName     string `json:"parkName"`
	Name         string `json:"name"`
	LocationDesc string `json:"locationDesc"`
}

// Cache holds the latest activator set, refreshed in the background.
type Cache struct {
	mu     sync.RWMutex
	byCall map[string]Info // base callsign (upper) → info
	client *http.Client
	logf   func(string, ...any)
}

// New creates a cache. logf may be nil.
func New(logf func(string, ...any)) *Cache {
	return &Cache{
		byCall: map[string]Info{},
		client: &http.Client{Timeout: 20 * time.Second},
		logf:   logf,
	}
}

// Run refreshes immediately, then every 60 s until ctx is cancelled.
func (c *Cache) Run(ctx context.Context) {
	c.refresh(ctx)
	t := time.NewTicker(60 * time.Second)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			c.refresh(ctx)
		}
	}
}

func (c *Cache) log(format string, a ...any) {
	if c.logf != nil {
		c.logf(format, a...)
	}
}

func (c *Cache) refresh(ctx context.Context) {
	req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
	if err != nil {
		c.log("pota: request: %v", err)
		return
	}
	resp, err := c.client.Do(req)
	if err != nil {
		c.log("pota: fetch: %v", err)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		c.log("pota: http %d", resp.StatusCode)
		return
	}
	var spots []apiSpot
	if err := json.NewDecoder(resp.Body).Decode(&spots); err != nil {
		c.log("pota: decode: %v", err)
		return
	}
	m := make(map[string]Info, len(spots))
	for _, s := range spots {
		call := baseCall(s.Activator)
		if call == "" {
			continue
		}
		name := strings.TrimSpace(s.Name)
		if name == "" {
			name = strings.TrimSpace(s.ParkName)
		}
		// Keep the first reference seen for a call (most-recent-first ordering
		// from the API), but don't clobber with a blank.
		if _, exists := m[call]; exists {
			continue
		}
		m[call] = Info{Reference: s.Reference, ParkName: name, LocationDesc: s.LocationDesc}
	}
	c.mu.Lock()
	c.byCall = m
	c.mu.Unlock()
	c.log("pota: %d active activators", len(m))
}

// Lookup returns park info for a callsign if it's currently activating.
func (c *Cache) Lookup(call string) (Info, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	if len(c.byCall) == 0 {
		return Info{}, false
	}
	i, ok := c.byCall[baseCall(call)]
	return i, ok
}

// BaseCall is the exported callsign normaliser used for hunter-log matching.
func BaseCall(s string) string { return baseCall(s) }

// baseCall normalises a callsign for matching: upper-cased, and when it carries
// "/" segments (F4BPO/P, HB9/F4BPO) we take the longest segment, which is
// almost always the home call.
func baseCall(s string) string {
	s = strings.ToUpper(strings.TrimSpace(s))
	if s == "" {
		return ""
	}
	if !strings.Contains(s, "/") {
		return s
	}
	best := ""
	for _, part := range strings.Split(s, "/") {
		if len(part) > len(best) {
			best = part
		}
	}
	return best
}
