package tle

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

const (
	// Celestrak amateur satellite TLE feed (primary)
	PrimaryURL  = "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=tle"
	FallbackURL = "http://tle.pe0sat.nl/kepler/amateur.txt"
	CacheFile   = "satmaster_tle_cache.txt"
	MaxAgeDays  = 3
)

// Satellite holds parsed TLE data.
type Satellite struct {
	Name string
	TLE1 string
	TLE2 string
}

// Manager handles TLE fetching, caching, and lookup.
type Manager struct {
	mu         sync.RWMutex
	satellites map[string]*Satellite
	fetchedAt  time.Time
	cacheDir   string
}

func NewManager() *Manager {
	cacheDir, _ := os.UserCacheDir()
	cacheDir = filepath.Join(cacheDir, "SatMaster")
	os.MkdirAll(cacheDir, 0755)
	return &Manager{
		satellites: make(map[string]*Satellite),
		cacheDir:   cacheDir,
	}
}

// fetchURLResult holds the result of a single URL fetch.
type fetchURLResult struct {
	url  string
	sats map[string]*Satellite
	raw  string
	err  error
}

// FetchAndCache downloads TLE data from all sources in parallel, merges them
// (primary takes precedence on duplicates), and saves to disk cache.
func (m *Manager) FetchAndCache() error {
	urls := []string{PrimaryURL, FallbackURL}
	results := make([]fetchURLResult, len(urls))

	// Fetch all sources concurrently
	var wg sync.WaitGroup
	for i, url := range urls {
		wg.Add(1)
		go func(i int, url string) {
			defer wg.Done()
			log.Printf("[TLE] Fetching from %s", url)
			data, err := fetchURL(url)
			if err != nil {
				log.Printf("[TLE] Failed %s: %v", url, err)
				results[i] = fetchURLResult{url: url, err: err}
				return
			}
			sats, err := parseTLE(data)
			if err != nil || len(sats) == 0 {
				log.Printf("[TLE] Parse failed or empty from %s: %v", url, err)
				results[i] = fetchURLResult{url: url, err: fmt.Errorf("parse failed")}
				return
			}
			log.Printf("[TLE] Fetched %d satellites from %s", len(sats), url)
			results[i] = fetchURLResult{url: url, sats: sats, raw: data}
		}(i, url)
	}
	wg.Wait()

	// Merge: start with fallback (lower priority), then overlay primary
	// This way primary always wins on duplicates
	merged := make(map[string]*Satellite)
	var combinedRaw strings.Builder
	anySuccess := false

	for i := len(results) - 1; i >= 0; i-- {
		r := results[i]
		if r.err != nil || r.sats == nil {
			continue
		}
		anySuccess = true
		added := 0
		for k, v := range r.sats {
			if _, exists := merged[k]; !exists {
				merged[k] = v
				added++
			}
		}
		combinedRaw.WriteString(r.raw)
		log.Printf("[TLE] Merged %d new satellites from %s (total: %d)", added, r.url, len(merged))
	}

	if !anySuccess {
		return fmt.Errorf("all TLE sources failed")
	}

	// Save combined raw data to disk cache
	cachePath := filepath.Join(m.cacheDir, CacheFile)
	if err := os.WriteFile(cachePath, []byte(combinedRaw.String()), 0644); err != nil {
		log.Printf("[TLE] Cache write failed: %v", err)
	}

	m.mu.Lock()
	m.satellites = merged
	m.fetchedAt = time.Now()
	m.mu.Unlock()

	log.Printf("[TLE] Total: %d satellites loaded from %d sources", len(merged), len(urls))
	return nil
}

// LoadLocal loads TLE data from disk cache.
func (m *Manager) LoadLocal() error {
	cachePath := filepath.Join(m.cacheDir, CacheFile)
	data, err := os.ReadFile(cachePath)
	if err != nil {
		return m.loadBundledFallback()
	}
	sats, err := parseTLE(string(data))
	if err != nil {
		return err
	}
	info, _ := os.Stat(cachePath)
	m.mu.Lock()
	m.satellites = sats
	if info != nil {
		m.fetchedAt = info.ModTime()
	}
	m.mu.Unlock()
	log.Printf("[TLE] Loaded %d satellites from local cache", len(sats))
	return nil
}

// loadBundledFallback loads a minimal built-in TLE set for common amateur sats.
func (m *Manager) loadBundledFallback() error {
	sats, err := parseTLE(defaultTLEs)
	if err != nil {
		return err
	}
	m.mu.Lock()
	m.satellites = sats
	m.fetchedAt = time.Now().Add(-48 * time.Hour)
	m.mu.Unlock()
	log.Printf("[TLE] Loaded %d bundled fallback satellites", len(sats))
	return nil
}

// Get returns a satellite by name (case-insensitive).
func (m *Manager) Get(name string) *Satellite {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if s, ok := m.satellites[strings.ToUpper(name)]; ok {
		return s
	}
	upper := strings.ToUpper(name)
	for k, v := range m.satellites {
		if strings.Contains(k, upper) {
			return v
		}
	}
	return nil
}

// All returns all loaded satellites.
func (m *Manager) All() []*Satellite {
	m.mu.RLock()
	defer m.mu.RUnlock()
	result := make([]*Satellite, 0, len(m.satellites))
	for _, s := range m.satellites {
		result = append(result, s)
	}
	return result
}

// SatelliteNames returns sorted list of satellite names.
func (m *Manager) SatelliteNames() []string {
	m.mu.RLock()
	defer m.mu.RUnlock()
	names := make([]string, 0, len(m.satellites))
	for k := range m.satellites {
		names = append(names, k)
	}
	return names
}

// AgeHours returns how many hours old the TLE data is.
func (m *Manager) AgeHours() float64 {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if m.fetchedAt.IsZero() {
		return 9999
	}
	return time.Since(m.fetchedAt).Hours()
}

func fetchURL(url string) (string, error) {
	client := &http.Client{Timeout: 15 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return "", fmt.Errorf("HTTP %d", resp.StatusCode)
	}
	body, err := io.ReadAll(resp.Body)
	return string(body), err
}

func parseTLE(data string) (map[string]*Satellite, error) {
	sats := make(map[string]*Satellite)
	scanner := bufio.NewScanner(strings.NewReader(data))
	var lines []string

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		lines = append(lines, line)
	}

	for i := 0; i+2 < len(lines); i += 3 {
		name := strings.TrimSpace(lines[i])
		tle1 := strings.TrimSpace(lines[i+1])
		tle2 := strings.TrimSpace(lines[i+2])

		if !strings.HasPrefix(tle1, "1 ") || !strings.HasPrefix(tle2, "2 ") {
			i -= 2
			continue
		}

		key := strings.ToUpper(name)
		sats[key] = &Satellite{
			Name: name,
			TLE1: tle1,
			TLE2: tle2,
		}
	}

	if len(sats) == 0 {
		return nil, fmt.Errorf("no valid TLE entries found")
	}
	return sats, nil
}

const defaultTLEs = `ISS (ZARYA)
1 25544U 98067A   24001.50000000  .00016717  00000-0  10270-3 0  9999
2 25544  51.6416 247.4627 0006703 130.5360 325.0288 15.50174753471696
AO-7
1 07530U 74089B   24001.50000000 -.00000023  00000-0  13694-4 0  9999
2 07530 101.7044 116.7600 0012184  36.7810 323.4116 12.53590024534125
AO-27
1 22825U 93061C   24001.50000000  .00000199  00000-0  54717-4 0  9999
2 22825  98.6398 100.2553 0008530 335.2107  24.8705 14.29831944581475
SO-50
1 27607U 02058C   24001.50000000  .00000306  00000-0  71007-4 0  9999
2 27607  98.0070 105.9740 0084804 339.5060  19.9560 14.74561589148781
FO-29
1 24278U 96046B   24001.50000000  .00000056  00000-0  68723-5 0  9999
2 24278  98.5355 100.4746 0350771 334.0040  24.2890 13.52863590383849
RS-44
1 44909U 19096E   24001.50000000  .00000103  00000-0  12583-4 0  9999
2 44909  97.6561 101.2437 0009786 341.6020  18.4590 14.93614256218765
`
