package main

import (
	"strings"

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

// New-grid scoping: WHICH prior contacts count as "I already have this square".
//
// The question has no single right answer, which is why it is a setting rather
// than a rule. A VUCC chaser counts a square per band; someone filling in a
// wall map counts it once. GridTracker offers the same six combinations, and an
// operator running both alongside each other needs them to agree — a square
// "wanted" in one and not the other is a bug report every time.
const (
	keyGridScope = "grid.scope" // band+mode combination — see gridScopes
	keyGridHunt  = "grid.hunt"  // "new" | "new_unconfirmed"
)

// gridScope is one band × mode combination.
type gridScope struct {
	Key string `json:"key"`
	// Band is true when the square has to have been worked on THIS band. False
	// is GridTracker's "Mix Bands": any band counts.
	Band bool `json:"band"`
	// Mode is how much of the mode matters:
	//   "digi" — any digital mode at all
	//   "ftx"  — any of the FT family (FT8/FT4/FT2)
	//   "mode" — this exact mode and no other
	Mode string `json:"mode"`
}

var gridScopes = []gridScope{
	{Key: "band_digi", Band: true, Mode: "digi"},
	{Key: "band_mode", Band: true, Mode: "mode"},
	{Key: "band_ftx", Band: true, Mode: "ftx"},
	{Key: "mix_digi", Band: false, Mode: "digi"},
	{Key: "mix_mode", Band: false, Mode: "mode"},
	{Key: "mix_ftx", Band: false, Mode: "ftx"},
}

// defaultGridScope matches what the panel showed before the setting existed:
// any band, any digital mode. Changing an operator's badges on upgrade is worse
// than offering them a choice they have not made yet.
const defaultGridScope = "mix_digi"

func lookupGridScope(key string) gridScope {
	for _, s := range gridScopes {
		if s.Key == key {
			return s
		}
	}
	for _, s := range gridScopes {
		if s.Key == defaultGridScope {
			return s
		}
	}
	return gridScopes[0]
}

// ftxModes is the FT family, which is a narrower thing than "digital": an
// operator chasing squares on FT8 does not count the RTTY contest contact, and
// GridTracker draws the same line.
var ftxModes = map[string]bool{"FT8": true, "FT4": true, "FT2": true}

// gridClassesOf lists every bucket one logged mode belongs to. A single FT8
// contact counts under its own name, under FTx and under digital — the scopes
// overlap by construction, so the index stores all three and the lookup picks.
func gridClassesOf(mode string) []string {
	m := strings.ToUpper(strings.TrimSpace(mode))
	if m == "" {
		return nil
	}
	out := []string{"m:" + m}
	if ftxModes[m] {
		out = append(out, "ftx")
	}
	if award.ModeClass(m) == "DIGI" {
		out = append(out, "digi")
	}
	return out
}

// gridClassFor turns the scope plus the mode being heard into the one class to
// look up. Empty means "this scope cannot judge this mode" — a phone contact
// under an FTx scope — and the caller then flags nothing rather than guessing.
func gridClassFor(s gridScope, mode string) string {
	m := strings.ToUpper(strings.TrimSpace(mode))
	if m == "" {
		return ""
	}
	switch s.Mode {
	case "mode":
		return "m:" + m
	case "ftx":
		if ftxModes[m] {
			return "ftx"
		}
		return ""
	default: // digi
		if award.ModeClass(m) == "DIGI" {
			return "digi"
		}
		return ""
	}
}

// GridScopeSettings is the panel's shape.
type GridScopeSettings struct {
	Scope string `json:"scope"`
	Hunt  string `json:"hunt"`
	// Scopes is the list the UI renders, so it cannot drift from the one the
	// lookup honours.
	Scopes []gridScope `json:"scopes"`
}

func (a *App) GetGridScopeSettings() GridScopeSettings {
	return GridScopeSettings{
		Scope:  lookupGridScope(a.settingOr(keyGridScope, "")).Key,
		Hunt:   gridHunt(a.settingOr(keyGridHunt, "")),
		Scopes: gridScopes,
	}
}

// gridHunt normalises the hunting mode. "new" is never-worked; "new_unconfirmed"
// also chases a square worked but not yet confirmed — the same square is still
// missing from the award either way.
func gridHunt(v string) string {
	if strings.TrimSpace(v) == "new_unconfirmed" {
		return "new_unconfirmed"
	}
	return "new"
}

func (a *App) SaveGridScopeSettings(s GridScopeSettings) error {
	a.setSetting(keyGridScope, lookupGridScope(s.Scope).Key)
	a.setSetting(keyGridHunt, gridHunt(s.Hunt))
	// The worked-grid index is built under these rules, so it is now stale.
	a.clusterStatusMu.Lock()
	a.clusterStatusIdx = nil
	a.clusterStatusMu.Unlock()
	return nil
}

// Grid states a heard square can be in, from the operator's point of view.
//
// "wanted" is not one state but two, and they call for different decisions: a
// square never worked is a QSO to make, one worked and not yet confirmed is a
// QSL to chase. Reporting both as plain NEW GRID — which is what this did when
// the unconfirmed hunt was added — leaves an operator staring at a badge on a
// station the log plainly says they worked an hour ago.
const (
	gridStateNew    = "new"    // never worked under this scope
	gridStateUnconf = "unconf" // worked under this scope, not yet confirmed
)

// gridStateFor classifies one heard square. "" means nothing to chase: already
// confirmed, or a mode this scope cannot judge.
//
// band is where it was heard and mode what it was heard in — the SCOPE decides
// whether either is allowed to matter.
func gridStateFor(idx map[string]bool, s gridScope, hunt, grid, band, mode string) string {
	cls := gridClassFor(s, mode)
	if cls == "" || len(grid) < 4 {
		return ""
	}
	key := qso.GridKey(strings.ToUpper(grid[:4]), "", cls)
	if s.Band {
		key = qso.GridKey(strings.ToUpper(grid[:4]), band, cls)
	}
	confirmed, worked := idx[key]
	switch {
	case !worked:
		return gridStateNew
	case !confirmed && hunt == "new_unconfirmed":
		return gridStateUnconf
	default:
		return ""
	}
}

// gridIsWanted is gridStateFor reduced to the yes/no the filters need.
func gridIsWanted(idx map[string]bool, s gridScope, hunt, grid, band, mode string) bool {
	return gridStateFor(idx, s, hunt, grid, band, mode) != ""
}
