package coverart

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
)

// Provider is the base shape every cover-art source implements.
// Providers register themselves at init() time via Register(). The
// enricher reads the current per-provider settings and dispatches to
// the capability methods below.
type Provider interface {
	// ID is a stable lowercase token persisted in DB columns
	// (cover_art_source / artist_art_source) and the
	// cover_art_provider_settings.provider_id key, plus referenced
	// in the operator-facing UI.
	ID() string

	// DisplayName is the human-readable label shown in the admin
	// Settings card.
	DisplayName() string

	// RequiresAPIKey reports whether the provider needs a key to
	// function. True for theaudiodb / fanart.tv; false for mbcaa.
	// The admin UI uses this to show or hide the api_key text field;
	// it does NOT gate the enabled toggle (per the no-coercive-
	// settings principle, providers with usable defaults — such as
	// TheAudioDB's public test key — ship working).
	RequiresAPIKey() bool

	// DefaultEnabled is whether a freshly-installed Minstrel ships
	// with this provider on. Both v1 providers return true.
	DefaultEnabled() bool

	// Configure receives the operator's settings (api_key, enabled
	// flag) and applies them. Called at boot during reconciliation
	// and on every admin PATCH. Implementations must be idempotent
	// and goroutine-safe (the enricher may be reading concurrently).
	Configure(ProviderSettings) error
}

// ProviderSettings is the runtime config a provider receives when
// Configure() is called. Providers may interpret an empty APIKey
// however they wish — TheAudioDB falls back to its public test key,
// MBCAA ignores the field entirely.
type ProviderSettings struct {
	Enabled bool
	APIKey  string
}

// ArtistRef is the lookup key passed to ArtistArtProvider.FetchArtistArt.
// MBID is preferred when present; Name is the always-populated fallback
// for providers that support name-based search (Deezer, Last.fm).
// MBID-only providers (TheAudioDB) return ErrNotFound when MBID is empty.
type ArtistRef struct {
	MBID string
	Name string
}

// AlbumRef is the lookup key passed to AlbumCoverProvider.FetchAlbumCover.
// Same MBID-preferred semantics as ArtistRef; ArtistName + AlbumTitle
// support name-based providers.
type AlbumRef struct {
	MBID       string
	ArtistName string
	AlbumTitle string
}

// AlbumCoverProvider is an opt-in capability: providers implementing
// it can fetch album cover art. MBID-only providers use ref.MBID and
// return ErrNotFound when it is empty; name-based providers (Deezer,
// Last.fm) fall back to ref.ArtistName + ref.AlbumTitle.
type AlbumCoverProvider interface {
	Provider
	// FetchAlbumCover returns the image bytes for the given album ref,
	// or ErrNotFound (terminal — no art available) /
	// ErrTransient (retry-eligible failure).
	FetchAlbumCover(ctx context.Context, ref AlbumRef) ([]byte, error)
}

// ArtistArtProvider is an opt-in capability: providers implementing
// it can fetch artist images. Returns thumb + fanart atomically at
// the JSON metadata step (one round-trip yields both URLs); the two
// image GETs are independent — partial success returns whatever bytes
// landed and the source is still stamped, so the enricher persists
// asymmetric results (thumb-only or fanart-only) gracefully.
type ArtistArtProvider interface {
	Provider
	FetchArtistArt(ctx context.Context, ref ArtistRef) (thumb, fanart []byte, err error)
}

// TestableProvider is an opt-in capability: providers that can answer
// "is my config working?" without performing a full enrichment cycle.
// Used by the admin Test-Connection button. v1: both MBCAA and
// TheAudioDB implement this.
type TestableProvider interface {
	Provider
	TestConnection(ctx context.Context) error
}

// ErrNotTestable is returned by the SettingsService.TestProvider
// method when the named provider doesn't implement TestableProvider.
var ErrNotTestable = errors.New("coverart: provider does not support test connection")

// ErrProviderNotFound is returned when the named provider is not
// registered.
var ErrProviderNotFound = errors.New("coverart: provider not registered")

// ErrNotFound indicates the upstream confirmed there is no art for
// this MBID — a terminal-this-pass result. The enricher treats this
// as "try the next provider in the chain"; if all providers return
// ErrNotFound, the row settles to cover_art_source='none'.
//
// Aliased to apierror.ErrNotFound so handler error mapping (errors.Is
// against the shared sentinel) and existing per-package callsites both
// resolve to the same pointer.
var ErrNotFound = apierror.ErrNotFound

// ErrTransient indicates a temporary failure (5xx, network, timeout,
// auth issue). The enricher leaves the row's source NULL so the next
// scan pass can retry.
var ErrTransient = errors.New("coverart: transient error")

// registry is the package-private list of all compiled-in providers.
// Providers register at init() via Register(). The enricher iterates
// over this list filtered by the current per-provider Settings.
var registry = struct {
	mu        sync.Mutex
	providers []Provider
}{}

// Register adds a provider to the registry. Must be called from
// init() of a provider file (provider_mbcaa.go, provider_theaudiodb.go,
// etc.). Calling twice with the same ID panics — providers are
// compiled-in, duplicate registration is a build-time bug.
func Register(p Provider) {
	registry.mu.Lock()
	defer registry.mu.Unlock()
	for _, existing := range registry.providers {
		if existing.ID() == p.ID() {
			panic(fmt.Sprintf("coverart: duplicate provider ID %q", p.ID()))
		}
	}
	registry.providers = append(registry.providers, p)
}

// AllProviders returns a snapshot of all registered providers in
// registration order. Caller must not mutate the returned slice.
func AllProviders() []Provider {
	registry.mu.Lock()
	defer registry.mu.Unlock()
	out := make([]Provider, len(registry.providers))
	copy(out, registry.providers)
	return out
}

// ProviderByID looks up a registered provider by ID. Returns
// ErrProviderNotFound if no such provider has registered.
func ProviderByID(id string) (Provider, error) {
	registry.mu.Lock()
	defer registry.mu.Unlock()
	for _, p := range registry.providers {
		if p.ID() == id {
			return p, nil
		}
	}
	return nil, ErrProviderNotFound
}

// resetRegistryForTests clears the registry. Test-only helper
// (provider_test.go); never called from production code.
func resetRegistryForTests() {
	registry.mu.Lock()
	defer registry.mu.Unlock()
	registry.providers = nil
}

// ResetRegistryForTests is an exported wrapper around resetRegistryForTests
// for use by tests in other packages (e.g. internal/api). Never call from
// production code.
func ResetRegistryForTests() {
	resetRegistryForTests()
}
