package main

import (
	"strings"

	"hamlog/internal/applog"
	"hamlog/internal/dxped"
)

// ── DXpeditions (ADXO announcements + DX-World news) ────────────────────
//
// What a logger can say that a news reader cannot: whether the announced
// operation is worth chasing. Every activation is judged against THIS log —
// the same verdict the cluster paints on a spot — so the list reads as "what I
// still need", not "what is on the air".

// DXpedition is one announced operation plus what it is worth here.
type DXpedition struct {
	dxped.Activation
	// Status is the strongest verdict across the announced callsigns, bands and
	// modes: "new" (entity never worked) beats "new-band-mode", which beats
	// "new-band", "new-mode", "new-slot", and finally "worked". Empty when the
	// callsign resolves to no entity — a prefix ADXO knows and cty.dat does not.
	Status string `json:"status_chase"`
	// Unconfirmed marks a need that is only a missing QSL, so the badge can be
	// drawn dimmed exactly as it is in the cluster and the decode list.
	Unconfirmed bool `json:"unconfirmed"`
}

// chaseRank orders the verdicts from "most worth chasing" down. The DXpedition
// list shows ONE badge, so the ranking is the whole decision.
var chaseRank = map[string]int{
	"new":           6,
	"new-band-mode": 5,
	"new-band":      4,
	"new-mode":      3,
	"new-slot":      2,
	"worked":        1,
}

// GetDXpeditions returns the announced operations, freshest feed permitting,
// each carrying its chase verdict.
func (a *App) GetDXpeditions() ([]DXpedition, error) {
	if a.dxped == nil {
		a.dxped = dxped.New()
	}
	acts, err := a.dxped.Activations(a.ctx)
	if err != nil {
		applog.Printf("dxped: adxo fetch: %v", err)
		if len(acts) == 0 {
			return nil, err
		}
		// Stale data with a logged error beats an empty tab.
	}
	out := make([]DXpedition, 0, len(acts))
	for _, act := range acts {
		out = append(out, DXpedition{Activation: act, Status: "", Unconfirmed: false})
	}
	a.judgeDXpeditions(out)
	return out, nil
}

// judgeDXpeditions fills in the chase verdict, in place.
//
// One ClusterSpotStatuses call for the whole list rather than one per row: the
// worked-index it builds is the expensive part (a full pass over the log), and
// asking it forty times to answer forty rows was the difference between a tab
// that opens and a tab that stalls a remote MySQL for a second.
func (a *App) judgeDXpeditions(list []DXpedition) {
	if a.qso == nil || len(list) == 0 {
		return
	}
	type slot struct{ row int }
	var queries []SpotQuery
	var owners []slot
	for i, d := range list {
		calls := d.Calls
		if len(calls) == 0 {
			if c := strings.ToUpper(strings.TrimSpace(d.Callsign)); c != "" {
				calls = []string{c}
			}
		}
		for _, call := range calls {
			// No announced band/mode: ask the entity-level question alone.
			if len(d.Bands) == 0 && len(d.Modes) == 0 {
				queries = append(queries, SpotQuery{Call: call})
				owners = append(owners, slot{i})
				continue
			}
			bands := d.Bands
			if len(bands) == 0 {
				bands = []string{""}
			}
			modes := d.Modes
			if len(modes) == 0 {
				modes = []string{""}
			}
			for _, b := range bands {
				for _, m := range modes {
					queries = append(queries, SpotQuery{Call: call, Band: b, Mode: m})
					owners = append(owners, slot{i})
				}
			}
		}
	}
	if len(queries) == 0 {
		return
	}
	res := a.ClusterSpotStatuses(queries)
	for i, r := range res {
		if i >= len(owners) {
			break
		}
		row := owners[i].row
		if chaseRank[r.Status] > chaseRank[list[row].Status] {
			list[row].Status = r.Status
			list[row].Unconfirmed = r.UnconfStatus
		}
	}
}

// GetDXWorldNews returns the DX-World headlines, with the callsigns mined out
// of each one so the reader can watch them.
//
// Deliberately NOT judged against the log. A headline names no band, so the
// only verdict available is entity-level, and a pane of "NEW DXCC" and
// "worked" badges turned out to say nothing an operator could act on — the
// same two words on every row is noise wearing the clothes of information.
// The announcements pane, which knows the bands and modes, is where a chase
// verdict is worth drawing.
func (a *App) GetDXWorldNews() ([]dxped.News, error) {
	if a.dxped == nil {
		a.dxped = dxped.New()
	}
	news, err := a.dxped.News(a.ctx)
	if err != nil {
		applog.Printf("dxped: dx-world fetch: %v", err)
		if len(news) == 0 {
			return nil, err
		}
	}
	return news, nil
}

// RefreshDXpeditions drops both caches so the next read goes to the network.
// Wired to the tab's refresh button: an operator who has just read of a landing
// on a cluster should not wait out the cache to see it here.
func (a *App) RefreshDXpeditions() {
	if a.dxped == nil {
		a.dxped = dxped.New()
	}
	a.dxped.Invalidate()
}
