package main

// "Have I worked this reference before?" — the NEW badge on a QSO's award
// references.
//
// # Where the answer comes from, and where it must NOT come from
//
// The obvious source is the materialised award_refs column: it is already on
// every row and one query reads it. It is also the wrong answer twice over.
// That column holds DISPLAY LABELS, not references — depending on the award's
// RefDisplay it can read "Krasnogvardeysky" or "ST-25 — Krasnogvardeysky", and
// DXCC stores a country name — and it is a derived cache that lags: a QSO whose
// references were back-filled (see BackfillRDA) counts on the Awards grid long
// before anything recomputes its column. The first build of this badge used it
// and marked a district worked on five bands as NEW.
//
// So the answer comes from the same place the Awards grid's answer comes from:
// award.MatchQSO over the logbook. What is engineered here is the COST of that,
// because the question is asked while the operator is logging:
//
//   - the set is per award CODE, and built only for codes actually asked about;
//   - it reuses the award snapshot when one is already in memory, which it
//     usually is (the Awards tab, the cluster status index);
//   - otherwise it STREAMS the logbook rather than materialising a second copy
//     of it — the references of one award are a few thousand short strings;
//   - and it is built in the BACKGROUND. A caller that asks before the set is
//     ready is told so and gets no badge, never a wait and never a wrong badge.
//
// A freshly logged QSO's references are folded into the sets, so a normal
// evening of logging never rebuilds anything.

import (
	"strings"

	"hamlog/internal/award"
	"hamlog/internal/qso"
)

// workedRefIndex is the per-code set of references already in the log, plus the
// logbook revision it describes.
type workedRefIndex struct {
	rev      string
	byCode   map[string]map[string]struct{}
	building map[string]bool
}

// AwardRefNewResult answers one badge request.
type AwardRefNewResult struct {
	// New maps "CODE@REF" to "never worked". An entry whose award set is still
	// building is ABSENT rather than false: "not worked" and "not known yet"
	// must not look the same to the badge.
	New map[string]bool `json:"new"`
	// Pending is true when at least one award set is still being built, and the
	// caller should ask again shortly.
	Pending bool `json:"pending"`
}

// AwardRefsNew reports which of the given "CODE@REF" entries have never been
// worked. Returns immediately, always.
func (a *App) AwardRefsNew(entries []string) AwardRefNewResult {
	res := AwardRefNewResult{New: map[string]bool{}}
	if a.qso == nil || len(entries) == 0 {
		return res
	}
	rev, err := a.qso.Revision(a.ctx)
	if err != nil {
		return res // logbook unreachable: no badge is the honest answer
	}

	// Group the request by award code, so one code is looked up once however
	// many of its references are on the QSO.
	want := map[string][]string{}
	for _, e := range entries {
		code, _, ok := splitRefEntry(e)
		if !ok {
			continue
		}
		want[code] = append(want[code], e)
	}

	a.workedRefMu.Lock()
	if a.workedRef.rev != rev || a.workedRef.byCode == nil {
		// The logbook moved on: every set describes a log that no longer exists.
		a.workedRef = workedRefIndex{rev: rev, byCode: map[string]map[string]struct{}{}, building: map[string]bool{}}
	}
	var build []string
	for code := range want {
		set, ready := a.workedRef.byCode[code]
		if !ready {
			if !a.workedRef.building[code] {
				a.workedRef.building[code] = true
				build = append(build, code)
			}
			res.Pending = true
			continue
		}
		for _, e := range want[code] {
			_, ref, _ := splitRefEntry(e)
			_, worked := set[ref]
			res.New[e] = !worked
		}
	}
	a.workedRefMu.Unlock()

	for _, code := range build {
		go a.buildWorkedRefs(code, rev)
	}
	return res
}

// splitRefEntry splits "RDA@ST-25" into its uppercased code and reference.
func splitRefEntry(e string) (code, ref string, ok bool) {
	at := strings.Index(e, "@")
	if at <= 0 {
		return "", "", false
	}
	code = strings.ToUpper(strings.TrimSpace(e[:at]))
	ref = strings.ToUpper(strings.TrimSpace(e[at+1:]))
	return code, ref, code != "" && ref != ""
}

// buildWorkedRefs collects every reference of one award already in the log.
func (a *App) buildWorkedRefs(code string, rev string) {
	set := map[string]struct{}{}
	defer func() {
		a.workedRefMu.Lock()
		// Publish only against the revision we were asked for: a QSO logged while
		// this ran has already reset the index, and a set built from the older log
		// would claim to describe the newer one.
		if a.workedRef.rev == rev && a.workedRef.byCode != nil {
			a.workedRef.byCode[code] = set
		}
		delete(a.workedRef.building, code)
		a.workedRefMu.Unlock()
	}()

	var def *award.Def
	for _, d := range a.awardDefs() {
		if strings.EqualFold(d.Code, code) {
			dd := d
			def = &dd
			break
		}
	}
	if def == nil {
		return // unknown award: an empty set, so nothing is ever badged NEW
	}
	metas := a.awardRefMetas([]award.Def{*def})[strings.ToUpper(def.Code)]
	collect := func(q *qso.QSO) {
		for _, r := range award.MatchQSO(*def, metas, q) {
			set[strings.ToUpper(strings.TrimSpace(r))] = struct{}{}
		}
	}

	// The snapshot is already enriched and already in memory whenever the Awards
	// tab has been open — by far the common case, and free.
	if snap := a.awardSnapshotIfLoaded(); snap != nil {
		for i := range snap {
			collect(&snap[i])
		}
		return
	}
	// Otherwise stream. One pass, nothing retained but the reference strings.
	_ = a.qso.IterateForAwards(a.ctx, func(q qso.QSO) error {
		a.enrichQSOForAwards(&q)
		collect(&q)
		return nil
	})
}

// awardSnapshotIfLoaded returns the award snapshot ONLY if one is already built
// and current. Never builds one: the caller is a background convenience, and
// reading the whole logbook for it is the cost this file exists to avoid.
func (a *App) awardSnapshotIfLoaded() []qso.QSO {
	rev, err := a.qso.Revision(a.ctx)
	if err != nil {
		return nil
	}
	a.awardSnapMu.Lock()
	defer a.awardSnapMu.Unlock()
	if a.awardSnap != nil && a.awardSnapRev == rev {
		return a.awardSnap
	}
	return nil
}

// noteWorkedQSO folds a just-logged contact's references into the built sets, so
// logging does not throw away work already done.
func (a *App) noteWorkedQSO(q qso.QSO) {
	a.workedRefMu.Lock()
	codes := make(map[string]bool, len(a.workedRef.byCode))
	for c := range a.workedRef.byCode {
		codes[c] = true
	}
	a.workedRefMu.Unlock()
	if len(codes) == 0 {
		return
	}
	rev, err := a.qso.Revision(a.ctx)
	if err != nil {
		return
	}
	a.enrichQSOForAwards(&q)
	found := map[string][]string{}
	for _, d := range a.awardDefs() {
		code := strings.ToUpper(d.Code)
		if !codes[code] {
			continue
		}
		metas := a.awardRefMetas([]award.Def{d})[code]
		for _, r := range award.MatchQSO(d, metas, &q) {
			found[code] = append(found[code], strings.ToUpper(strings.TrimSpace(r)))
		}
	}
	a.workedRefMu.Lock()
	defer a.workedRefMu.Unlock()
	// Carry the sets forward to the new revision rather than dropping them: they
	// were correct one contact ago, and that contact is what is being added.
	a.workedRef.rev = rev
	for code, refs := range found {
		if set := a.workedRef.byCode[code]; set != nil {
			for _, r := range refs {
				set[r] = struct{}{}
			}
		}
	}
}

// invalidateWorkedRefs drops every set. For the changes a revision cannot see —
// award definitions edited, reference lists updated, QSLs confirmed.
func (a *App) invalidateWorkedRefs() {
	a.workedRefMu.Lock()
	a.workedRef = workedRefIndex{}
	a.workedRefMu.Unlock()
}
