package radio

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"sync"
	"time"

	"github.com/gmgauthier/gostations/internal/config"
)

// Station is the normalized record returned from radio-browser searches.
type Station struct {
	Name        string      `json:"name"`
	Codec       string      `json:"codec"`
	Bitrate     json.Number `json:"bitrate"`
	Countrycode string      `json:"countrycode"`
	Tags        string      `json:"tags"`
	Url         string      `json:"url"`
	Lastcheck   int         `json:"lastcheckok"`
	// Future: UUID string `json:"stationuuid"` etc for stable favorites.
}

var (
	apiHostCache string
	apiHostOnce  sync.Once
	httpClient   = &http.Client{Timeout: 12 * time.Second}
)

// Search queries the radio-browser API (with fixes for the old panics and error handling).
// It respects the cached config for host + limit.
func Search(ctx context.Context, name, country, state, tags string, includeDown bool) ([]Station, error) {
	if err := config.Init(); err != nil {
		return nil, fmt.Errorf("config: %w", err)
	}

	params := url.Values{}
	if name != "" {
		params.Add("name", name)
	}
	if country != "" {
		params.Add("country", country)
	}
	if state != "" {
		params.Add("state", state)
	}
	if tags != "" {
		params.Add("tag", tags)
	}

	host, err := getAPIHost(ctx)
	if err != nil {
		return nil, fmt.Errorf("resolve api host: %w", err)
	}

	qstr := params.Encode()
	limit := config.MaxItems()
	if limit <= 0 {
		limit = 9999
	}
	urlstr := fmt.Sprintf("https://%s/json/stations/search?%s&limit=%d", host, qstr, limit)
	if qstr == "" {
		urlstr = fmt.Sprintf("https://%s/json/stations/search?limit=%d", host, limit)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlstr, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "gostations/1 (github.com/gmgauthier/gostations)")

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("http get %s: %w", urlstr, err)
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	if resp.StatusCode != 200 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return nil, fmt.Errorf("api returned %d: %s", resp.StatusCode, string(body))
	}

	var data []Station
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		return nil, fmt.Errorf("decode stations: %w", err)
	}

	if includeDown {
		return data, nil
	}
	return pruneStations(data), nil
}

func pruneStations(stations []Station) []Station {
	filtered := stations[:0]
	for _, s := range stations {
		if s.Lastcheck == 1 {
			filtered = append(filtered, s)
		}
	}
	return filtered
}

// getAPIHost resolves (with caching + error handling) the dynamic host from the configured "all.api..." entry.
// This fixes the old panics in RandomIP / reverseLookup / nslookup.
func getAPIHost(ctx context.Context) (string, error) {
	var resolveErr error
	apiHostOnce.Do(func() {
		apiHostCache, resolveErr = resolveAPIHost(ctx)
	})
	if apiHostCache == "" && resolveErr != nil {
		return "", resolveErr
	}
	if apiHostCache == "" {
		// last resort fallback (common public endpoint)
		return "de1.api.radio-browser.info", nil
	}
	return apiHostCache, nil
}

func resolveAPIHost(ctx context.Context) (string, error) {
	host := config.API()
	if host == "" {
		host = "all.api.radio-browser.info"
	}

	ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
	if err != nil {
		return "", fmt.Errorf("lookup %s: %w", host, err)
	}
	if len(ips) == 0 {
		return "", errors.New("no IPs resolved for api host")
	}

	// simple random among A/AAAA (better than old discarded NewSource)
	randIdx := rand.Intn(len(ips))
	ip := ips[randIdx].IP

	names, err := net.DefaultResolver.LookupAddr(ctx, ip.String())
	if err != nil || len(names) == 0 {
		// fallback to the original host if reverse fails (some IPs may be private or no PTR)
		return host, nil
	}
	return names[0], nil // usually ends with .
}

// ResetAPIHostCache is exposed for tests.
func ResetAPIHostCache() {
	apiHostOnce = sync.Once{}
	apiHostCache = ""
}
