// Package radio implements a playlist.Provider for internet radio stations.
// It includes a built-in cliamp radio stream, user-defined stations from
// ~/.config/cliamp/radios.toml, favorites from radio_favorites.toml, and
// lazy-loaded catalog stations from the Radio Browser API.
package radio

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"

	"github.com/bjarneo/cliamp/internal/appdir"
	"github.com/bjarneo/cliamp/internal/tomlutil"
	"github.com/bjarneo/cliamp/playlist"
	"github.com/bjarneo/cliamp/provider"
)

// Compile-time interface checks.
var (
	_ provider.FavoriteToggler = (*Provider)(nil)
	_ provider.CatalogLoader   = (*Provider)(nil)
	_ provider.CatalogSearcher = (*Provider)(nil)
	_ provider.SectionedList   = (*Provider)(nil)
)

const builtinName = "cliamp radio"
const builtinURL = "https://radio.cliamp.stream/streams.m3u"

// Provider serves radio stations as single-track playlists.
// It combines local stations, user favorites, and catalog stations
// from the Radio Browser API into a single unified list.
type Provider struct {
	mu            sync.Mutex
	stations      []station        // built-in + user-defined (radios.toml)
	favorites     *Favorites       // user favorites (radio_favorites.toml)
	catalog       []CatalogStation // lazily loaded from Radio Browser API
	searchResults []CatalogStation // non-nil when API search is active
}

type station struct {
	name string
	url  string
}

// New creates a Provider with the built-in station plus any user-defined
// stations from ~/.config/cliamp/radios.toml and favorites.
func New() *Provider {
	p := &Provider{
		stations: []station{
			{name: builtinName, url: builtinURL},
		},
	}

	dir, err := appdir.Dir()
	if err != nil {
		p.favorites = &Favorites{byURL: make(map[string]struct{})}
		return p
	}
	if extra, err := loadStations(filepath.Join(dir, "radios.toml")); err == nil {
		p.stations = append(p.stations, extra...)
	}
	p.favorites = LoadFavorites()
	return p
}

func (p *Provider) Name() string { return "Radio" }

// Playlists returns a unified list: local stations, then favorites (★ prefixed),
// then catalog stations (with metadata). IDs are prefixed: "l:", "f:", "c:".
func (p *Provider) Playlists() ([]playlist.PlaylistInfo, error) {
	p.mu.Lock()
	defer p.mu.Unlock()

	var out []playlist.PlaylistInfo

	// When search is active, show only search results.
	if p.searchResults != nil {
		for i, s := range p.searchResults {
			out = append(out, p.catalogEntry("s", i, s))
		}
		return out, nil
	}

	// Local stations.
	for i, s := range p.stations {
		out = append(out, playlist.PlaylistInfo{
			ID:   fmt.Sprintf("l:%d", i),
			Name: s.name,
		})
	}

	// Favorites.
	for i, s := range p.favorites.Stations() {
		out = append(out, playlist.PlaylistInfo{
			ID:   fmt.Sprintf("f:%d", i),
			Name: "★ " + formatCatalogName(s),
		})
	}

	// Catalog stations.
	for i, s := range p.catalog {
		out = append(out, p.catalogEntry("c", i, s))
	}

	return out, nil
}

// catalogEntry builds a PlaylistInfo for a CatalogStation, marking favorites with ★.
func (p *Provider) catalogEntry(prefix string, idx int, s CatalogStation) playlist.PlaylistInfo {
	name := formatCatalogName(s)
	if p.favorites.Contains(s.URL) {
		name = "★ " + name
	}
	return playlist.PlaylistInfo{
		ID:   fmt.Sprintf("%s:%d", prefix, idx),
		Name: name,
	}
}

// Tracks returns a single-track playlist for the given station ID.
func (p *Provider) Tracks(id string) ([]playlist.Track, error) {
	p.mu.Lock()
	defer p.mu.Unlock()

	prefix, idx, err := parseStationID(id)
	if err != nil {
		return nil, err
	}

	var url, title string
	switch prefix {
	case "l":
		if idx < 0 || idx >= len(p.stations) {
			return nil, errors.New("invalid local station index")
		}
		url, title = p.stations[idx].url, p.stations[idx].name
	case "f":
		favs := p.favorites.Stations()
		if idx < 0 || idx >= len(favs) {
			return nil, errors.New("invalid favorite index")
		}
		url, title = favs[idx].URL, favs[idx].Name
	case "c":
		if idx < 0 || idx >= len(p.catalog) {
			return nil, errors.New("invalid catalog station index")
		}
		url, title = p.catalog[idx].URL, p.catalog[idx].Name
	case "s":
		if p.searchResults == nil || idx < 0 || idx >= len(p.searchResults) {
			return nil, errors.New("invalid search result index")
		}
		url, title = p.searchResults[idx].URL, p.searchResults[idx].Name
	default:
		return nil, errors.New("unknown station type")
	}

	return []playlist.Track{{
		Path: url, Title: title, Stream: true, Realtime: true,
	}}, nil
}

// AppendCatalog adds catalog stations fetched from the Radio Browser API.
func (p *Provider) AppendCatalog(stations []CatalogStation) {
	p.mu.Lock()
	defer p.mu.Unlock()
	seen := make(map[string]struct{}, len(p.catalog)+len(stations))
	for _, station := range p.catalog {
		seen[station.URL] = struct{}{}
	}
	for _, station := range stations {
		if _, exists := seen[station.URL]; exists {
			continue
		}
		seen[station.URL] = struct{}{}
		p.catalog = append(p.catalog, station)
	}
}

// ToggleFavorite toggles the favorite status of a catalog or favorite entry.
// Returns (true, name) if added, (false, name) if removed.
func (p *Provider) ToggleFavorite(id string) (added bool, name string, err error) {
	p.mu.Lock()
	defer p.mu.Unlock()

	prefix, idx, err := parseStationID(id)
	if err != nil {
		return false, "", err
	}

	var s CatalogStation
	switch prefix {
	case "c":
		if idx < 0 || idx >= len(p.catalog) {
			return false, "", errors.New("invalid catalog index")
		}
		s = p.catalog[idx]
	case "s":
		if p.searchResults == nil || idx < 0 || idx >= len(p.searchResults) {
			return false, "", errors.New("invalid search result index")
		}
		s = p.searchResults[idx]
	case "f":
		favs := p.favorites.Stations()
		if idx < 0 || idx >= len(favs) {
			return false, "", errors.New("invalid favorite index")
		}
		s = favs[idx]
	default:
		return false, "", errors.New("cannot favorite local stations")
	}

	if p.favorites.Contains(s.URL) {
		return false, s.Name, p.favorites.Remove(s.URL)
	}
	return true, s.Name, p.favorites.Add(s)
}

// SetSearchResults activates search mode with the given results.
// Playlists() will return search results instead of catalog stations.
func (p *Provider) SetSearchResults(stations []CatalogStation) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.searchResults = stations
}

// ClearSearch deactivates search mode, restoring the catalog view.
func (p *Provider) ClearSearch() {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.searchResults = nil
}

// IsSearching returns true if API search results are active.
func (p *Provider) IsSearching() bool {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.searchResults != nil
}

// LoadCatalogPage fetches the next page of catalog entries from the Radio
// Browser API and appends them to the provider's catalog.
// Implements provider.CatalogLoader.
func (p *Provider) LoadCatalogPage(offset, limit int) (int, error) {
	stations, err := TopStationsOffset(offset, limit)
	if err != nil {
		return 0, err
	}
	p.AppendCatalog(stations)
	return len(stations), nil
}

// SearchCatalog performs a server-side station search via the Radio Browser API.
// Results are reflected in subsequent Playlists() calls.
// Implements provider.CatalogSearcher.
func (p *Provider) SearchCatalog(query string) (int, error) {
	stations, err := SearchStations(query, 200)
	if err != nil {
		return 0, err
	}
	p.SetSearchResults(stations)
	return len(stations), nil
}

// IsFavoritableID reports whether the given ID can be favorited.
// Implements provider.SectionedList.
func (p *Provider) IsFavoritableID(id string) bool {
	return IsCatalogOrFavID(id)
}

// IsCatalogOrFavID returns true if the ID belongs to a catalog, search, or favorite entry.
func IsCatalogOrFavID(id string) bool {
	return strings.HasPrefix(id, "c:") || strings.HasPrefix(id, "f:") || strings.HasPrefix(id, "s:")
}

// IDPrefix returns the type prefix of a provider list ID ("l", "f", "c", or "").
// Also implements provider.SectionedList when called as a method.
func (p *Provider) IDPrefix(id string) string {
	return idPrefix(id)
}

func idPrefix(id string) string {
	prefix, _, ok := strings.Cut(id, ":")
	if !ok {
		return ""
	}
	return prefix
}

// parseStationID splits a prefixed ID like "c:42" into its prefix and index.
// Legacy numeric IDs (no colon) are treated as "l:" local station indices.
func parseStationID(id string) (prefix string, idx int, err error) {
	raw := id
	prefix, idxStr, ok := strings.Cut(id, ":")
	if !ok {
		prefix = "l"
		idxStr = raw
	}
	idx, err = strconv.Atoi(idxStr)
	if err != nil {
		return "", 0, errors.New("invalid station ID")
	}
	return prefix, idx, nil
}

// formatCatalogName builds a display name from a CatalogStation.
func formatCatalogName(s CatalogStation) string {
	name := s.Name
	if s.Bitrate > 0 {
		name += fmt.Sprintf(" [%dk]", s.Bitrate)
	}
	if s.Country != "" {
		name += " · " + s.Country
	}
	return name
}

// loadStations parses a TOML file with [[station]] sections.
func loadStations(path string) ([]station, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil, nil
		}
		return nil, err
	}

	var stations []station
	tomlutil.ParseSections(data, "station", func(f map[string]string) {
		s := station{name: f["name"], url: f["url"]}
		if s.name != "" && s.url != "" {
			stations = append(stations, s)
		}
	})
	return stations, nil
}
