package clublog

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

// ctyURL is the ClubLog Country File endpoint. It returns a gzipped cty.xml.
const ctyURL = "https://cdn.clublog.org/cty.php?api="

// Manager owns the on-disk cty.xml.gz cache and the parsed exception DB.
type Manager struct {
	apiKey   string
	cacheDir string

	mu sync.RWMutex
	db *DB
}

func NewManager(apiKey, cacheDir string) *Manager {
	return &Manager{apiKey: apiKey, cacheDir: cacheDir}
}

// Path is where the cached gzipped country file lives.
func (m *Manager) Path() string {
	return filepath.Join(m.cacheDir, "clublog_cty.xml.gz")
}

// Loaded reports whether an exception DB is in memory.
func (m *Manager) Loaded() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.db != nil
}

// Info returns the loaded file's generation date + exception count (zeros when
// not loaded).
func (m *Manager) Info() (date string, count int) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if m.db == nil {
		return "", 0
	}
	return m.db.Date(), m.db.Count()
}

// NeedsRefresh reports whether the cached country file is missing or older than
// maxAge — so the caller re-downloads it to pick up newly-added date-ranged
// exceptions (e.g. an event call's fresh SARDINIA period).
func (m *Manager) NeedsRefresh(maxAge time.Duration) bool {
	fi, err := os.Stat(m.Path())
	if err != nil {
		return true // missing
	}
	return time.Since(fi.ModTime()) > maxAge
}

// EnsureLoaded loads the cached file into memory if present. Does NOT download.
func (m *Manager) EnsureLoaded() error {
	if m.Loaded() {
		return nil
	}
	return m.LoadFromDisk()
}

// LoadFromDisk parses the cached cty.xml.gz and swaps it in.
func (m *Manager) LoadFromDisk() error {
	f, err := os.Open(m.Path())
	if err != nil {
		return err
	}
	defer f.Close()
	db, err := LoadGzip(f)
	if err != nil {
		return err
	}
	m.mu.Lock()
	m.db = db
	m.mu.Unlock()
	return nil
}

// Download fetches a fresh cty.xml.gz from ClubLog and writes it atomically,
// then loads it.
func (m *Manager) Download(ctx context.Context) error {
	if m.apiKey == "" {
		return fmt.Errorf("clublog api key not set")
	}
	if err := os.MkdirAll(m.cacheDir, 0o755); err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, "GET", ctyURL+m.apiKey, nil)
	if err != nil {
		return err
	}
	client := &http.Client{Timeout: 60 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("download: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("clublog HTTP %d", resp.StatusCode)
	}
	tmp, err := os.CreateTemp(m.cacheDir, "clublog-*.tmp")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	if _, err := io.Copy(tmp, resp.Body); err != nil {
		tmp.Close()
		os.Remove(tmpName)
		return err
	}
	tmp.Close()
	if err := os.Rename(tmpName, m.Path()); err != nil {
		os.Remove(tmpName)
		return err
	}
	return m.LoadFromDisk()
}

// Resolve returns the matching exception for a callsign at a date, if loaded.
func (m *Manager) Resolve(call string, date time.Time) (Exception, bool) {
	m.mu.RLock()
	db := m.db
	m.mu.RUnlock()
	if db == nil {
		return Exception{}, false
	}
	return db.Resolve(call, date)
}

// ResolvePrefix resolves a call by ClubLog's date-ranged PREFIX table.
func (m *Manager) ResolvePrefix(call string, date time.Time) (Exception, bool) {
	m.mu.RLock()
	db := m.db
	m.mu.RUnlock()
	return db.ResolvePrefix(call, date)
}

// HasException reports whether the call has any full-call exception (any date).
func (m *Manager) HasException(call string) bool {
	m.mu.RLock()
	db := m.db
	m.mu.RUnlock()
	return db.HasException(call)
}
