package coverart

import (
	"context"
	"net/http"
	"net/url"
	"path"
	"strings"
	"sync/atomic"
	"time"
)

// lastfmBaseURL is a var (not const) so tests can override it.
var lastfmBaseURL = "https://ws.audioscrobbler.com/2.0/"

const (
	// lastfmMinPeriod throttles to 4 req/s under their 5/s per-key ceiling.
	lastfmMinPeriod = 250 * time.Millisecond

	lastfmMaxRetries = 3
)

// lastfmPlaceholderHashes is the set of known "default" image basename
// hashes Last.fm returns instead of 404'ing for artists/albums that
// don't have art. Provider detects these in the response URL and
// treats them as ErrNotFound to avoid persisting placeholders as art.
//
// The well-known generic-artist hash has been stable for years; if
// Last.fm ships new placeholder hashes, we'll start persisting them
// and need to add them here. Fail-open: when in doubt, return
// ErrNotFound rather than risk persisting a placeholder.
var lastfmPlaceholderHashes = map[string]bool{
	"2a96cbd8b46e442fc41c2b86b821562f": true, // generic artist star
	"c6f59c1e5e7240a4c0d427abd71f3dbb": true, // generic album cover
}

// lastfmProvider implements Provider, AlbumCoverProvider, and
// ArtistArtProvider against Last.fm's audioscrobbler API. Requires an
// operator-supplied API key — provider stays effectively disabled
// (returns ErrNotFound on every call) when key is empty.
type lastfmProvider struct {
	enabled atomic.Bool
	apiKey  atomic.Pointer[string]
	client  *httpClient
}

func init() {
	Register(&lastfmProvider{
		client: newHTTPClient(httpClientOptions{
			Name:                 "lastfm",
			HTTPClient:           &http.Client{Timeout: 30 * time.Second},
			MinInterval:          lastfmMinPeriod,
			MaxRetries:           lastfmMaxRetries,
			TreatAuthAsTransient: true,
		}),
	})
}

func (p *lastfmProvider) ID() string           { return "lastfm" }
func (p *lastfmProvider) DisplayName() string  { return "Last.fm" }
func (p *lastfmProvider) RequiresAPIKey() bool { return true }
func (p *lastfmProvider) DefaultEnabled() bool { return false }

func (p *lastfmProvider) Configure(s ProviderSettings) error {
	p.enabled.Store(s.Enabled)
	key := s.APIKey
	p.apiKey.Store(&key)
	return nil
}

// currentKey returns the configured API key (empty string when unset).
func (p *lastfmProvider) currentKey() string {
	if k := p.apiKey.Load(); k != nil {
		return *k
	}
	return ""
}

// FetchArtistArt prefers MBID-based lookup (artist.getInfo accepts
// &mbid=) and falls back to artist-name search. Returns ErrNotFound
// on:
//   - provider disabled OR API key empty
//   - both MBID and Name empty
//   - Last.fm returns no image OR returns a known placeholder URL
//
// Last.fm has a single artist image (no separate fanart) — return
// it as thumb, leave fanart nil.
func (p *lastfmProvider) FetchArtistArt(ctx context.Context, ref ArtistRef) (thumb, fanart []byte, err error) {
	if !p.enabled.Load() || p.currentKey() == "" {
		return nil, nil, ErrNotFound
	}
	if ref.MBID == "" && ref.Name == "" {
		return nil, nil, ErrNotFound
	}

	q := url.Values{
		"method":  {"artist.getInfo"},
		"api_key": {p.currentKey()},
		"format":  {"json"},
	}
	if ref.MBID != "" {
		q.Set("mbid", ref.MBID)
	} else {
		q.Set("artist", ref.Name)
	}

	var resp struct {
		Artist struct {
			Name  string        `json:"name"`
			Image []lastfmImage `json:"image"`
		} `json:"artist"`
		Error int `json:"error"` // present on error responses
	}
	if err := p.client.getJSON(ctx, lastfmBaseURL+"?"+q.Encode(), &resp); err != nil {
		return nil, nil, err
	}
	if resp.Error != 0 || resp.Artist.Name == "" {
		return nil, nil, ErrNotFound
	}
	imgURL := pickLargestImage(resp.Artist.Image)
	if imgURL == "" || isLastfmPlaceholder(imgURL) {
		return nil, nil, ErrNotFound
	}
	img, ferr := p.client.getImage(ctx, imgURL)
	if ferr != nil {
		return nil, nil, ferr
	}
	return img, nil, nil
}

// FetchAlbumCover requires both ArtistName and AlbumTitle (album.getInfo
// has no MBID-only mode that's reliable). MBID is sent as an additional
// hint when present.
func (p *lastfmProvider) FetchAlbumCover(ctx context.Context, ref AlbumRef) ([]byte, error) {
	if !p.enabled.Load() || p.currentKey() == "" {
		return nil, ErrNotFound
	}
	if ref.ArtistName == "" || ref.AlbumTitle == "" {
		return nil, ErrNotFound
	}

	q := url.Values{
		"method":  {"album.getInfo"},
		"api_key": {p.currentKey()},
		"format":  {"json"},
		"artist":  {ref.ArtistName},
		"album":   {ref.AlbumTitle},
	}
	if ref.MBID != "" {
		q.Set("mbid", ref.MBID)
	}

	var resp struct {
		Album struct {
			Name  string        `json:"name"`
			Image []lastfmImage `json:"image"`
		} `json:"album"`
		Error int `json:"error"`
	}
	if err := p.client.getJSON(ctx, lastfmBaseURL+"?"+q.Encode(), &resp); err != nil {
		return nil, err
	}
	if resp.Error != 0 || resp.Album.Name == "" {
		return nil, ErrNotFound
	}
	imgURL := pickLargestImage(resp.Album.Image)
	if imgURL == "" || isLastfmPlaceholder(imgURL) {
		return nil, ErrNotFound
	}
	return p.client.getImage(ctx, imgURL)
}

// lastfmImage models one entry of Last.fm's image[] array.
type lastfmImage struct {
	Text string `json:"#text"`
	Size string `json:"size"`
}

// pickLargestImage returns the URL of the largest available image.
// Preference order: mega → extralarge → large → medium → small.
// Returns empty when no entry has a non-empty URL.
func pickLargestImage(imgs []lastfmImage) string {
	pref := []string{"mega", "extralarge", "large", "medium", "small"}
	bySize := make(map[string]string, len(imgs))
	for _, im := range imgs {
		if im.Text != "" {
			bySize[im.Size] = im.Text
		}
	}
	for _, sz := range pref {
		if u, ok := bySize[sz]; ok {
			return u
		}
	}
	return ""
}

// isLastfmPlaceholder returns true if the URL points at one of the
// known generic placeholder image hashes.
func isLastfmPlaceholder(rawURL string) bool {
	u, err := url.Parse(rawURL)
	if err != nil {
		return false
	}
	base := path.Base(u.Path)
	if i := strings.LastIndex(base, "."); i > 0 {
		base = base[:i]
	}
	return lastfmPlaceholderHashes[strings.ToLower(base)]
}

// Compile-time checks: lastfmProvider satisfies both capability interfaces.
var (
	_ AlbumCoverProvider = (*lastfmProvider)(nil)
	_ ArtistArtProvider  = (*lastfmProvider)(nil)
)
