package dxcc

import (
	"sort"
	"strings"
)

// The dxccByName table itself lives in dxcc_names_gen.go (generated by joining
// cty.dat to the authoritative ARRL/ADIF entity list). cty.dat doesn't carry
// the ADIF DXCC number, so we map its entity names → numbers here to stamp
// MY_DXCC / DXCC at log time without a network round-trip.

// EntityDXCC returns the ADIF DXCC entity number for the given cty.dat entity
// name, or 0 when unknown (callers should then leave the field empty rather
// than guess). Case-insensitive and whitespace-tolerant.
func EntityDXCC(name string) int {
	if name == "" {
		return 0
	}
	// Fast path: exact (lower-cased) match against the cty.dat names.
	if n := dxccByName[strings.ToLower(strings.TrimSpace(name))]; n != 0 {
		return n
	}
	// Fallback: canonicalise so abbreviation/spelling differences still match
	// (e.g. an ADIF import that wrote "Lord Howe I." instead of cty.dat's
	// "Lord Howe Island").
	if n := dxccByCanon[canonEntity(name)]; n != 0 {
		return n
	}
	// Last resort: cty.dat pseudo-entities (Sicily, African Italy) report a
	// parent DXCC entity for the number.
	if c := CanonicalEntityName(name); !strings.EqualFold(c, name) {
		return dxccByName[strings.ToLower(strings.TrimSpace(c))]
	}
	return 0
}

// nameByDXCC reverses dxccByName (number → a representative entity name),
// built once. When several names share a number, the longest (usually the most
// complete) wins. Names are Title-cased for display.
var nameByDXCC = func() map[int]string {
	m := make(map[int]string, len(dxccByName))
	for name, num := range dxccByName {
		if cur, ok := m[num]; !ok || len(name) > len(cur) {
			m[num] = name
		}
	}
	return m
}()

// NameForDXCC returns a display name for an ADIF DXCC entity number, or "" if
// unknown.
func NameForDXCC(n int) string {
	name, ok := nameByDXCC[n]
	if !ok {
		return ""
	}
	return strings.Title(name) //nolint:staticcheck // ASCII entity names
}

// ZoneByCallDistrict returns the CQ and ITU zone for a callsign in a country
// that is split across zones by call district (USA, Australia…). cty.dat and
// ClubLog's cty.xml only carry one zone per entity, so loggers apply this
// district→zone convention to get e.g. W6 = CQ3/ITU6 instead of the entity
// default CQ5/ITU8. ok=false means no district rule applies (use the entity
// default). The district is the first digit of the callsign.
func ZoneByCallDistrict(adif int, call string) (cqz, ituz int, ok bool) {
	d := districtDigit(call)
	if d < 0 {
		return 0, 0, false
	}
	switch adif {
	case 291: // United States — standard district defaults (state-level
		// exceptions exist, but this matches what Log4OM/DXKeeper default to).
		if z, o := usDistrictZones[d]; o {
			return z[0], z[1], true
		}
	case 150: // Australia — VK6/VK8 (west/north) are CQ29/ITU58, rest CQ30/ITU59.
		if d == 6 || d == 8 {
			return 29, 58, true
		}
		return 30, 59, true
	}
	return 0, 0, false
}

// usDistrictZones maps a US call district digit to its {CQ, ITU} zone.
var usDistrictZones = map[int][2]int{
	0: {4, 7}, 1: {5, 8}, 2: {5, 8}, 3: {5, 8}, 4: {5, 8},
	5: {4, 7}, 6: {3, 6}, 7: {3, 6}, 8: {5, 8}, 9: {4, 8},
}

// firstDigit returns the first 0-9 digit in a callsign, or -1 if none.
func firstDigit(call string) int {
	for i := 0; i < len(call); i++ {
		if call[i] >= '0' && call[i] <= '9' {
			return int(call[i] - '0')
		}
	}
	return -1
}

// districtDigit returns the effective call-area digit: a trailing "/N" (single
// digit) re-homes the call to area N (W6ABC/7 → area 7), otherwise the first
// digit of the call.
func districtDigit(call string) int {
	if i := strings.LastIndex(call, "/"); i >= 0 && i == len(call)-2 {
		if c := call[len(call)-1]; c >= '0' && c <= '9' {
			return int(c - '0')
		}
	}
	return firstDigit(call)
}

// EntityNumberName pairs a DXCC entity number with its display name.
type EntityNumberName struct {
	Num  int
	Name string
}

// AllEntities returns every known DXCC entity (number + display name), sorted by
// number. Used to seed the DXCC award's reference list.
func AllEntities() []EntityNumberName {
	out := make([]EntityNumberName, 0, len(nameByDXCC))
	for num := range nameByDXCC {
		out = append(out, EntityNumberName{Num: num, Name: NameForDXCC(num)})
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Num < out[j].Num })
	return out
}

// dxccByCanon is dxccByName re-keyed by the canonical entity form, built once.
var dxccByCanon = func() map[string]int {
	m := make(map[string]int, len(dxccByName))
	for name, num := range dxccByName {
		m[canonEntity(name)] = num
	}
	return m
}()

// canonEntity reduces an entity name to a canonical token stream, expanding the
// common abbreviations that differ between naming conventions and normalising
// punctuation / "&".
func canonEntity(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	s = strings.ReplaceAll(s, "&", " and ")
	fields := strings.FieldsFunc(s, func(r rune) bool {
		switch r {
		case ' ', '.', ',', '-', '\'', '(', ')', '/':
			return true
		}
		return false
	})
	for i, w := range fields {
		switch w {
		case "i":
			fields[i] = "island"
		case "is":
			fields[i] = "islands"
		case "st":
			fields[i] = "saint"
		case "mt":
			fields[i] = "mount"
		case "rep":
			fields[i] = "republic"
		case "dem":
			fields[i] = "democratic"
		case "fed":
			fields[i] = "federal"
		}
	}
	return strings.Join(fields, " ")
}

// ctyEntityAliases maps cty.dat's non-DXCC pseudo-entities — the CQ-zone /
// WAE splits AD1C lists separately — onto the ARRL DXCC entity they belong
// to. cty.dat reports e.g. "Sicily" so contesters get the right zones, but
// for DXCC (and the COUNTRY field) they are Italy.
var ctyEntityAliases = map[string]string{
	"sicily":        "Italy",
	"african italy": "Italy",
	// NB: Sardinia is intentionally absent — it's a real DXCC entity.
}

// CanonicalEntityName normalises a cty.dat entity name to its ARRL DXCC
// entity. Names that already are DXCC entities pass through unchanged.
func CanonicalEntityName(name string) string {
	if c, ok := ctyEntityAliases[strings.ToLower(strings.TrimSpace(name))]; ok {
		return c
	}
	return name
}
