package coverart

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

// FetcherConfig assembles the operator-tunable knobs for the MBCAA
// HTTP client. Kept exported because main.go's boot wiring constructs
// one to pass into NewMBCAAProviderFromConfig.
type FetcherConfig struct {
	BaseURL    string        // default "https://coverartarchive.org"
	UserAgent  string        // e.g. "Minstrel/<version> (<contact>)"
	MinPeriod  time.Duration // 1s default per MBCAA etiquette
	HTTPClient *http.Client  // optional override for tests
}

// Sample MBID used by TestConnection. Beatles' "Abbey Road" UK release.
const mbcaaTestSampleMBID = "f4b3df80-8a6c-3e87-b51e-5c1ee70a78ee"

// mbcaaProvider implements Provider, AlbumCoverProvider, and
// TestableProvider against the MusicBrainz Cover Art Archive.
// Safe for concurrent use; the internal mutex serialises HTTP
// requests so the rate limit is respected.
type mbcaaProvider struct {
	cfg      FetcherConfig
	enabled  atomic.Bool
	mu       sync.Mutex
	lastCall time.Time
	client   *http.Client
}

func init() {
	Register(newMBCAAProvider(FetcherConfig{}))
}

// NewMBCAAProviderFromConfig replaces the registered MBCAA provider's
// internal config with the supplied one. main.go calls this at boot
// to set the production User-Agent.
func NewMBCAAProviderFromConfig(cfg FetcherConfig) {
	p, err := ProviderByID("mbcaa")
	if err != nil {
		Register(newMBCAAProvider(cfg))
		return
	}
	mp, ok := p.(*mbcaaProvider)
	if !ok {
		panic("coverart: provider with ID 'mbcaa' is not *mbcaaProvider")
	}
	cfg = applyMBCAADefaults(cfg)
	mp.mu.Lock()
	mp.cfg = cfg
	mp.client = cfg.HTTPClient
	mp.mu.Unlock()
}

func newMBCAAProvider(cfg FetcherConfig) *mbcaaProvider {
	cfg = applyMBCAADefaults(cfg)
	return &mbcaaProvider{cfg: cfg, client: cfg.HTTPClient}
}

func applyMBCAADefaults(cfg FetcherConfig) FetcherConfig {
	if cfg.BaseURL == "" {
		cfg.BaseURL = "https://coverartarchive.org"
	}
	if cfg.MinPeriod == 0 {
		cfg.MinPeriod = time.Second
	}
	if cfg.HTTPClient == nil {
		cfg.HTTPClient = &http.Client{Timeout: 30 * time.Second}
	}
	return cfg
}

func (p *mbcaaProvider) ID() string           { return "mbcaa" }
func (p *mbcaaProvider) DisplayName() string  { return "Cover Art Archive" }
func (p *mbcaaProvider) RequiresAPIKey() bool { return false }
func (p *mbcaaProvider) DefaultEnabled() bool { return true }

func (p *mbcaaProvider) Configure(s ProviderSettings) error {
	p.enabled.Store(s.Enabled)
	return nil
}

// FetchAlbumCover retrieves the 500px front cover for the given
// release MBID. MBCAA is MBID-only; returns ErrNotFound when
// ref.MBID is empty. Returns ErrNotFound on 404 or ErrTransient otherwise.
func (p *mbcaaProvider) 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 // MBCAA is MBID-only
	}

	// Rate limit guard.
	p.mu.Lock()
	cfg := p.cfg
	if cfg.MinPeriod > 0 {
		wait := time.Until(p.lastCall.Add(cfg.MinPeriod))
		if wait > 0 {
			timer := time.NewTimer(wait)
			p.mu.Unlock()
			select {
			case <-ctx.Done():
				timer.Stop()
				return nil, ctx.Err()
			case <-timer.C:
			}
			p.mu.Lock()
		}
	}
	p.lastCall = time.Now()
	p.mu.Unlock()

	url := fmt.Sprintf("%s/release/%s/front-500", cfg.BaseURL, ref.MBID)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("coverart: build request: %w", err)
	}
	req.Header.Set("User-Agent", cfg.UserAgent)
	req.Header.Set("Accept", "image/*")

	resp, err := p.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTransient, err)
	}
	defer func() { _ = resp.Body.Close() }()

	switch {
	case resp.StatusCode == http.StatusNotFound:
		return nil, ErrNotFound
	case resp.StatusCode != http.StatusOK:
		return nil, fmt.Errorf("%w: status %d", ErrTransient, resp.StatusCode)
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
	if err != nil {
		return nil, fmt.Errorf("%w: read body: %v", ErrTransient, err)
	}
	return body, nil
}

func (p *mbcaaProvider) TestConnection(ctx context.Context) error {
	_, err := p.FetchAlbumCover(ctx, AlbumRef{MBID: mbcaaTestSampleMBID})
	if err == nil {
		return nil
	}
	if errors.Is(err, ErrNotFound) {
		return nil
	}
	return err
}
