package dxcc

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

// CtyDatURL is the canonical source of cty.dat. AD1C ships updates roughly
// monthly; we cache the file on disk so we don't hammer it.
const CtyDatURL = "https://www.country-files.com/cty/cty.dat"

// Manager owns the on-disk cty.dat cache and the parsed DB. Safe for
// concurrent reads after Load; concurrent reloads serialize on its lock.
type Manager struct {
	cacheDir string

	mu  sync.RWMutex
	db  *DB
	src ctySource // metadata about whichever copy we loaded

	loading atomic.Bool
}

type ctySource struct {
	Path        string    `json:"path"`
	LoadedAt    time.Time `json:"loaded_at"`
	FileModTime time.Time `json:"file_mod_time"`
	Entities    int       `json:"entities"`
	Downloaded  bool      `json:"downloaded"`
}

// NewManager prepares a manager rooted at cacheDir (created if missing).
// Does not load anything — call EnsureLoaded after.
func NewManager(cacheDir string) *Manager {
	return &Manager{cacheDir: cacheDir}
}

// Path returns the on-disk path where cty.dat is/should be cached.
func (m *Manager) Path() string {
	return filepath.Join(m.cacheDir, "cty.dat")
}

// EnsureLoaded loads cty.dat from disk; if missing, downloads it first.
// Safe to call repeatedly — only the first run actually downloads.
func (m *Manager) EnsureLoaded(ctx context.Context) error {
	if _, err := os.Stat(m.Path()); os.IsNotExist(err) {
		if err := m.Download(ctx); err != nil {
			return fmt.Errorf("download cty.dat: %w", err)
		}
	}
	return m.LoadFromDisk()
}

// LoadFromDisk parses the cached cty.dat into a fresh DB and swaps it in.
func (m *Manager) LoadFromDisk() error {
	f, err := os.Open(m.Path())
	if err != nil {
		return err
	}
	defer f.Close()
	info, _ := f.Stat()
	db, err := Load(f)
	if err != nil {
		return err
	}
	m.mu.Lock()
	m.db = db
	m.src = ctySource{
		Path:        m.Path(),
		LoadedAt:    time.Now(),
		FileModTime: info.ModTime(),
		Entities:    len(db.entities),
	}
	m.mu.Unlock()
	return nil
}

// Download fetches a fresh cty.dat from CtyDatURL and atomically replaces
// the on-disk cache. Does NOT reload it into memory — caller can chain
// LoadFromDisk for that.
func (m *Manager) Download(ctx context.Context) error {
	if !m.loading.CompareAndSwap(false, true) {
		return fmt.Errorf("cty.dat download already in progress")
	}
	defer m.loading.Store(false)

	if err := os.MkdirAll(m.cacheDir, 0o755); err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, "GET", CtyDatURL, nil)
	if err != nil {
		return err
	}
	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("HTTP %d", resp.StatusCode)
	}
	// Write to a temp file in the same dir, then atomic rename — avoids a
	// half-written file if we crash mid-download.
	tmp, err := os.CreateTemp(m.cacheDir, "cty-*.tmp")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	_, err = io.Copy(tmp, resp.Body)
	tmp.Close()
	if err != nil {
		os.Remove(tmpPath)
		return err
	}
	if err := os.Rename(tmpPath, m.Path()); err != nil {
		os.Remove(tmpPath)
		return err
	}
	return nil
}

// Refresh = Download + LoadFromDisk in one call.
func (m *Manager) Refresh(ctx context.Context) error {
	if err := m.Download(ctx); err != nil {
		return err
	}
	return m.LoadFromDisk()
}

// Lookup is a passthrough to the loaded DB. Returns false if no DB is
// loaded yet (callers should treat that as graceful degradation).
func (m *Manager) Lookup(callsign string) (Match, bool) {
	m.mu.RLock()
	db := m.db
	m.mu.RUnlock()
	if db == nil {
		return Match{}, false
	}
	return db.Lookup(callsign)
}

// EntityNames returns the sorted, de-duplicated DXCC entity names from the
// loaded cty.dat — the canonical list for a "Country" picker. Empty until
// cty.dat has loaded.
func (m *Manager) EntityNames() []string {
	m.mu.RLock()
	db := m.db
	m.mu.RUnlock()
	if db == nil {
		return nil
	}
	seen := map[string]bool{}
	var out []string
	for _, e := range db.Entities() {
		n := strings.TrimSpace(e.Name)
		if n == "" || seen[n] {
			continue
		}
		seen[n] = true
		out = append(out, n)
	}
	sort.Strings(out)
	return out
}

// PrefixByDXCC maps ADIF DXCC entity number → canonical primary prefix (F, DL,
// XE…) from the loaded cty.dat. cty.dat is keyed by entity NAME, so the join
// goes through EntityDXCC; entries whose name doesn't resolve are skipped
// rather than guessed. Empty until cty.dat has loaded.
func (m *Manager) PrefixByDXCC() map[int]string {
	m.mu.RLock()
	db := m.db
	m.mu.RUnlock()
	if db == nil {
		return nil
	}
	out := make(map[int]string, 400)
	for _, e := range db.Entities() {
		p := strings.TrimSpace(e.Primary)
		if p == "" {
			continue
		}
		// cty.dat marks non-DXCC sub-entities with a leading '*' — they report
		// under a parent entity and must not overwrite the parent's prefix.
		if strings.HasPrefix(p, "*") {
			continue
		}
		if n := EntityDXCC(e.Name); n > 0 {
			if _, dup := out[n]; !dup {
				// DXCC prefixes are conventionally upper-case; cty.dat spells some
				// sub-entity suffixes in lower case (3D2/c → 3D2/C).
				out[n] = strings.ToUpper(p)
			}
		}
	}
	return out
}

// Info returns metadata about the currently-loaded cty.dat (or zero value
// if nothing loaded).
func (m *Manager) Info() ctySource {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.src
}
