package coverart

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"sync/atomic"
	"time"
)

// theAudioDBBaseURL is a var (not const) so tests can override it
// to point at a test server. Production callers should never modify
// this.
var theAudioDBBaseURL = "https://www.theaudiodb.com/api/v1/json"

const (
	// theAudioDBTestKey is the upstream's documented public test key.
	// Functional but rate-limited identically to personal keys (2
	// req/sec). Used as the default when an operator hasn't supplied
	// their own key, per the no-coercive-settings principle.
	theAudioDBTestKey = "2"

	// theAudioDBMinPeriod enforces TheAudioDB's documented free/test
	// rate limit (2 req/sec). Patreon supporter keys allow higher
	// rates; we don't differentiate in v1 — operators with supporter
	// keys experience the conservative 2 req/sec ceiling.
	theAudioDBMinPeriod = 500 * time.Millisecond

	// theAudioDBMaxRetries is the number of retry attempts on 429 /
	// 5xx before giving up and surfacing ErrTransient.
	theAudioDBMaxRetries = 3

	// theAudioDBTestSampleMBID is used by TestConnection. Pink Floyd's
	// "The Dark Side of the Moon" — a stable popular release verified
	// present in TheAudioDB at design time.
	theAudioDBTestSampleMBID = "f5093c06-23e3-404f-aeaa-40f72885ee3a"
)

// theAudioDBProvider implements Provider, AlbumCoverProvider,
// ArtistArtProvider, and TestableProvider against the TheAudioDB
// REST API. Safe for concurrent use; the underlying httpClient
// serialises all calls so the 2 req/sec rate limit applies uniformly.
type theAudioDBProvider struct {
	enabled atomic.Bool
	apiKey  atomic.Pointer[string] // nil until first Configure; falls back to test key when empty
	client  *httpClient
}

func init() {
	Register(&theAudioDBProvider{
		client: newHTTPClient(httpClientOptions{
			Name:                 "theaudiodb",
			HTTPClient:           &http.Client{Timeout: 30 * time.Second},
			MinInterval:          theAudioDBMinPeriod,
			MaxRetries:           theAudioDBMaxRetries,
			TreatAuthAsTransient: true,
		}),
	})
}

// jsonURL builds a request URL with the API key embedded in the path
// segment, the auth strategy TheAudioDB uses.
func (p *theAudioDBProvider) jsonURL(path string) string {
	return fmt.Sprintf("%s/%s/%s", theAudioDBBaseURL, p.currentKey(), path)
}

func (p *theAudioDBProvider) ID() string           { return "theaudiodb" }
func (p *theAudioDBProvider) DisplayName() string  { return "TheAudioDB" }
func (p *theAudioDBProvider) RequiresAPIKey() bool { return true }
func (p *theAudioDBProvider) DefaultEnabled() bool { return true }

func (p *theAudioDBProvider) Configure(s ProviderSettings) error {
	p.enabled.Store(s.Enabled)
	key := s.APIKey
	if key == "" {
		key = theAudioDBTestKey // ships working out of the box
	}
	p.apiKey.Store(&key)
	return nil
}

// currentKey returns the active API key (operator-supplied or the
// public test-key fallback).
func (p *theAudioDBProvider) currentKey() string {
	if k := p.apiKey.Load(); k != nil {
		return *k
	}
	return theAudioDBTestKey
}

// FetchAlbumCover hits /album-mb.php?i=<mbid>, parses strAlbumThumb,
// then GETs the image URL. Two HTTP round-trips per call; both
// counted against the rate-limit guard. TheAudioDB is MBID-only;
// returns ErrNotFound when ref.MBID is empty.
func (p *theAudioDBProvider) FetchAlbumCover(ctx context.Context, ref AlbumRef) ([]byte, error) {
	if !p.enabled.Load() {
		return nil, ErrNotFound // defensive; enricher already filters on enabled
	}
	if ref.MBID == "" {
		return nil, ErrNotFound // TheAudioDB is MBID-only
	}

	var resp struct {
		Album []struct {
			StrAlbumThumb *string `json:"strAlbumThumb"`
		} `json:"album"`
	}
	if err := p.client.getJSON(ctx, p.jsonURL("album-mb.php?i="+ref.MBID), &resp); err != nil {
		return nil, err
	}
	if len(resp.Album) == 0 || resp.Album[0].StrAlbumThumb == nil || *resp.Album[0].StrAlbumThumb == "" {
		return nil, ErrNotFound
	}
	return p.client.getImage(ctx, *resp.Album[0].StrAlbumThumb)
}

// FetchArtistArt hits /artist-mb.php?i=<mbid>, parses strArtistThumb +
// strArtistFanart, GETs both. Up to three HTTP round-trips per call.
// TheAudioDB is MBID-only; returns ErrNotFound when ref.MBID is empty.
//
// Atomic at the JSON step: if the JSON parse yields no images at all
// (artist not found OR both image fields null/empty), returns
// ErrNotFound. If only one URL is present, returns just that image.
// If both URLs are present but one image GET fails transiently,
// returns the successful one with nil for the failed one — the
// enricher persists whichever bytes landed. If BOTH image GETs fail
// transiently, returns ErrTransient so the row stays NULL for retry.
func (p *theAudioDBProvider) FetchArtistArt(ctx context.Context, ref ArtistRef) (thumb, fanart []byte, err error) {
	if !p.enabled.Load() {
		return nil, nil, ErrNotFound
	}
	if ref.MBID == "" {
		return nil, nil, ErrNotFound // TheAudioDB is MBID-only
	}

	var resp struct {
		Artists []struct {
			StrArtistThumb  *string `json:"strArtistThumb"`
			StrArtistFanart *string `json:"strArtistFanart"`
		} `json:"artists"`
	}
	if err := p.client.getJSON(ctx, p.jsonURL("artist-mb.php?i="+ref.MBID), &resp); err != nil {
		return nil, nil, err
	}
	if len(resp.Artists) == 0 {
		return nil, nil, ErrNotFound
	}
	a := resp.Artists[0]

	var thumbURL, fanartURL string
	if a.StrArtistThumb != nil {
		thumbURL = *a.StrArtistThumb
	}
	if a.StrArtistFanart != nil {
		fanartURL = *a.StrArtistFanart
	}
	if thumbURL == "" && fanartURL == "" {
		return nil, nil, ErrNotFound
	}

	if thumbURL != "" {
		thumb, err = p.client.getImage(ctx, thumbURL)
		if err != nil && !errors.Is(err, ErrTransient) && !errors.Is(err, ErrNotFound) {
			return nil, nil, err
		}
		// On ErrTransient or ErrNotFound, leave thumb=nil; continue.
		if err != nil {
			thumb = nil
		}
	}
	if fanartURL != "" {
		fanart, err = p.client.getImage(ctx, fanartURL)
		if err != nil && !errors.Is(err, ErrTransient) && !errors.Is(err, ErrNotFound) {
			return thumb, nil, err
		}
		if err != nil {
			fanart = nil
		}
	}

	// At least one of thumbURL/fanartURL was non-empty by the check
	// above. If both image GETs returned nil bytes (transient or
	// 404 on the CDN URL), surface ErrTransient so the enricher
	// leaves the row NULL for next pass. Otherwise return what we got.
	if thumb == nil && fanart == nil {
		return nil, nil, ErrTransient
	}
	return thumb, fanart, nil
}

// TestConnection hits the sample MBID's /album-mb.php endpoint. A
// successful response (200 with parseable JSON, even if the album
// data is empty) indicates the connection works. 401/403/5xx /
// network failures surface as TestConnection failures.
func (p *theAudioDBProvider) TestConnection(ctx context.Context) error {
	if !p.enabled.Load() {
		return errors.New("provider is disabled")
	}
	var resp struct {
		Album []struct {
			IDAlbum *string `json:"idAlbum"`
		} `json:"album"`
	}
	return p.client.getJSON(ctx, p.jsonURL("album-mb.php?i="+theAudioDBTestSampleMBID), &resp)
}
