package coverart

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"log/slog"
	"sort"
	"strings"
	"sync"

	"github.com/jackc/pgx/v5/pgxpool"

	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

// SettingsService reconciles the registry of compiled-in providers
// with the cover_art_provider_settings table at boot, and on every
// admin-initiated update. It owns the cover_art_sources_meta
// singleton's current_version stamp, bumping it whenever the
// *enabled set* of providers changes.
type SettingsService struct {
	pool   *pgxpool.Pool
	logger *slog.Logger

	mu             sync.RWMutex
	enabledIDs     map[string]bool   // provider_id → enabled
	apiKeys        map[string]string // provider_id → api_key (empty if NULL)
	currentVersion int32
}

// ProviderUpdatePatch is the admin-PATCH body shape. nil pointers
// mean "leave unchanged"; non-nil means "set to this value (which
// may be empty string for api_key clearing)".
type ProviderUpdatePatch struct {
	Enabled *bool
	APIKey  *string
}

// ProviderInfo is the SettingsService's view of a provider for the
// admin GET handler. Combines registry metadata with DB settings.
type ProviderInfo struct {
	ID             string
	DisplayName    string
	RequiresAPIKey bool
	Supports       []string // "album_cover", "artist_thumb", "artist_fanart"
	Enabled        bool
	APIKeySet      bool
	DisplayOrder   int32
	Testable       bool
}

// NewSettingsService boots the service: reads the DB rows, INSERTs
// defaults for any registered provider missing a row, calls
// Configure() on every registered provider with the row data, and
// loads the current_version. Returns the populated service.
func NewSettingsService(ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger) (*SettingsService, error) {
	s := &SettingsService{
		pool:       pool,
		logger:     logger,
		enabledIDs: map[string]bool{},
		apiKeys:    map[string]string{},
	}
	if err := s.reconcile(ctx); err != nil {
		return nil, fmt.Errorf("settings service boot: %w", err)
	}
	return s, nil
}

// reconcile reads the DB, INSERTs defaults for missing providers,
// calls Configure() on each registered provider, and loads the
// current_version into the cache.
func (s *SettingsService) reconcile(ctx context.Context) error {
	q := dbq.New(s.pool)

	// Insert defaults for any registered provider missing a row.
	registered := AllProviders()
	for i, p := range registered {
		if err := q.UpsertProviderSettings(ctx, dbq.UpsertProviderSettingsParams{
			ProviderID:   p.ID(),
			Enabled:      p.DefaultEnabled(),
			DisplayOrder: int32(i),
		}); err != nil {
			return fmt.Errorf("upsert provider %q: %w", p.ID(), err)
		}
	}

	// Read all rows back.
	rows, err := q.ListProviderSettings(ctx)
	if err != nil {
		return fmt.Errorf("list provider settings: %w", err)
	}

	// Build the lookup maps and call Configure on each registered
	// provider with the corresponding row data.
	settingsByID := map[string]dbq.CoverArtProviderSetting{}
	for _, r := range rows {
		settingsByID[r.ProviderID] = r
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	s.enabledIDs = map[string]bool{}
	s.apiKeys = map[string]string{}
	for _, p := range registered {
		row, ok := settingsByID[p.ID()]
		if !ok {
			s.logger.Warn("settings: missing row after upsert", "provider", p.ID())
			continue
		}
		key := ""
		if row.ApiKey != nil {
			key = *row.ApiKey
		}
		s.enabledIDs[p.ID()] = row.Enabled
		s.apiKeys[p.ID()] = key
		if err := p.Configure(ProviderSettings{Enabled: row.Enabled, APIKey: key}); err != nil {
			s.logger.Warn("settings: provider Configure failed",
				"provider", p.ID(), "err", err)
		}
	}

	v, err := q.GetCurrentSourcesVersion(ctx)
	if err != nil {
		return fmt.Errorf("get current sources version: %w", err)
	}
	s.currentVersion = v
	return nil
}

// EnabledAlbumProviders returns the enabled providers that implement
// AlbumCoverProvider, in registration order. Snapshot — caller must
// not mutate.
func (s *SettingsService) EnabledAlbumProviders() []AlbumCoverProvider {
	s.mu.RLock()
	defer s.mu.RUnlock()
	var out []AlbumCoverProvider
	for _, p := range AllProviders() {
		if !s.enabledIDs[p.ID()] {
			continue
		}
		if ap, ok := p.(AlbumCoverProvider); ok {
			out = append(out, ap)
		}
	}
	return out
}

// EnabledArtistProviders is the parallel for ArtistArtProvider.
func (s *SettingsService) EnabledArtistProviders() []ArtistArtProvider {
	s.mu.RLock()
	defer s.mu.RUnlock()
	var out []ArtistArtProvider
	for _, p := range AllProviders() {
		if !s.enabledIDs[p.ID()] {
			continue
		}
		if ap, ok := p.(ArtistArtProvider); ok {
			out = append(out, ap)
		}
	}
	return out
}

// CurrentVersion returns the current sources-set version (the stamp
// the enricher writes onto rows it processes).
func (s *SettingsService) CurrentVersion() int32 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.currentVersion
}

// UpdateProvider applies an admin-UI patch. Returns versionBumped=true
// if the change altered the *enabled set* (not on key changes).
// Re-runs the provider's Configure() with the new settings.
func (s *SettingsService) UpdateProvider(ctx context.Context, providerID string, patch ProviderUpdatePatch) (bool, error) {
	p, err := ProviderByID(providerID)
	if err != nil {
		return false, err
	}

	q := dbq.New(s.pool)

	// Compute current enabled-set signature before applying the patch.
	s.mu.RLock()
	beforeSig := enabledSetSignature(s.enabledIDs)
	// Use the cached enabled value when the patch doesn't specify it,
	// since the sqlc-generated Enabled field is bool (not *bool) and
	// COALESCE($2, enabled) only works with a nullable param. Passing
	// the current value when nil achieves the same "leave unchanged"
	// semantics.
	currentEnabled := s.enabledIDs[providerID]
	s.mu.RUnlock()

	enabledVal := currentEnabled
	if patch.Enabled != nil {
		enabledVal = *patch.Enabled
	}

	// Apply DB update. The sqlc-generated UpdateProviderSettingsParams
	// uses Column3/ApiKey because the underlying SQL has $3::boolean
	// (apiKeyChanged flag) and $4 (api_key value). The actual generated
	// field for $4 is ApiKey *string.
	apiKeyChanged := patch.APIKey != nil
	if err := q.UpdateProviderSettings(ctx, dbq.UpdateProviderSettingsParams{
		ProviderID: providerID,
		Enabled:    enabledVal,
		Column3:    apiKeyChanged,
		ApiKey:     patch.APIKey,
	}); err != nil {
		return false, fmt.Errorf("update provider settings: %w", err)
	}

	// Re-read rows + re-cache + Configure the changed provider.
	rows, err := q.ListProviderSettings(ctx)
	if err != nil {
		return false, fmt.Errorf("re-list provider settings: %w", err)
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	for _, r := range rows {
		key := ""
		if r.ApiKey != nil {
			key = *r.ApiKey
		}
		s.enabledIDs[r.ProviderID] = r.Enabled
		s.apiKeys[r.ProviderID] = key
		if r.ProviderID == providerID {
			if err := p.Configure(ProviderSettings{Enabled: r.Enabled, APIKey: key}); err != nil {
				s.logger.Warn("provider Configure failed after update",
					"provider", providerID, "err", err)
			}
		}
	}

	afterSig := enabledSetSignature(s.enabledIDs)
	if beforeSig == afterSig {
		return false, nil
	}

	newVersion, err := q.BumpSourcesVersion(ctx)
	if err != nil {
		return false, fmt.Errorf("bump sources version: %w", err)
	}
	s.currentVersion = newVersion
	return true, nil
}

// enabledSetSignature returns a deterministic string of the
// currently-enabled provider IDs.
func enabledSetSignature(enabledIDs map[string]bool) string {
	ids := make([]string, 0, len(enabledIDs))
	for id, on := range enabledIDs {
		if on {
			ids = append(ids, id)
		}
	}
	sort.Strings(ids)
	return strings.Join(ids, ",")
}

// BumpVersionIfProvidersChanged compares the current registered-provider
// set (by ID) to the last set seen on a prior successful boot. If
// different, it atomically increments cover_art_sources_meta.current_version
// and stores the new hash, returning (newVersion, true, nil). If
// unchanged, returns (currentVersion, false, nil) without touching the DB.
//
// Used at boot to ensure rows previously settled to 'none' get re-tried
// once after a provider chain change (e.g. operator updates the binary
// and a new provider is registered, or one is removed). The version
// bump triggers eligibility for 'none' rows on the next enrichment pass
// without requiring a manual operator action.
func (s *SettingsService) BumpVersionIfProvidersChanged(ctx context.Context) (version int32, bumped bool, err error) {
	hash := registeredProvidersHash()
	q := dbq.New(s.pool)

	stored, qerr := q.GetCoverArtProvidersHash(ctx)
	if qerr != nil {
		return 0, false, fmt.Errorf("get providers hash: %w", qerr)
	}
	if stored == hash {
		return s.CurrentVersion(), false, nil
	}

	newVer, berr := q.BumpVersionAndSetProvidersHash(ctx, hash)
	if berr != nil {
		return 0, false, fmt.Errorf("bump version: %w", berr)
	}
	// Update the in-memory version so the next enrichment pass sees the new value.
	s.mu.Lock()
	s.currentVersion = newVer
	s.mu.Unlock()
	return newVer, true, nil
}

// registeredProvidersHash returns the SHA-256 hex of the sorted,
// colon-joined registered provider IDs.
func registeredProvidersHash() string {
	provs := AllProviders()
	ids := make([]string, 0, len(provs))
	for _, p := range provs {
		ids = append(ids, p.ID())
	}
	sort.Strings(ids)
	joined := strings.Join(ids, ":")
	sum := sha256.Sum256([]byte(joined))
	return hex.EncodeToString(sum[:])
}

// BumpVersion unconditionally increments cover_art_sources_meta.current_version
// and returns the new value. Used by the manual "Re-search missing art"
// admin endpoint when the operator wants every 'none' row re-attempted
// on the next enrichment pass.
//
// Distinct from BumpVersionIfProvidersChanged: that method is the boot-
// time auto-bump and skips the DB write when the registered-provider
// hash matches. This method always writes.
func (s *SettingsService) BumpVersion(ctx context.Context) (int32, error) {
	q := dbq.New(s.pool)
	// Reuse BumpVersionAndSetProvidersHash with the current registered-
	// providers hash; this keeps the stored hash consistent (a manual
	// bump is conceptually equivalent to "force the auto-bump path").
	newVer, err := q.BumpVersionAndSetProvidersHash(ctx, registeredProvidersHash())
	if err != nil {
		return 0, fmt.Errorf("bump version: %w", err)
	}
	s.mu.Lock()
	s.currentVersion = newVer
	s.mu.Unlock()
	return newVer, nil
}

// TestProvider invokes TestConnection on the named provider if it
// implements TestableProvider. Returns ErrNotTestable if the
// provider doesn't, ErrProviderNotFound if not registered.
func (s *SettingsService) TestProvider(ctx context.Context, providerID string) error {
	p, err := ProviderByID(providerID)
	if err != nil {
		return err
	}
	tp, ok := p.(TestableProvider)
	if !ok {
		return ErrNotTestable
	}
	return tp.TestConnection(ctx)
}

// ListProviderInfo builds the wire-shaped slice for GET
// /api/admin/cover-sources. Returns providers in registration order.
func (s *SettingsService) ListProviderInfo() []ProviderInfo {
	s.mu.RLock()
	defer s.mu.RUnlock()

	var out []ProviderInfo
	for i, p := range AllProviders() {
		info := ProviderInfo{
			ID:             p.ID(),
			DisplayName:    p.DisplayName(),
			RequiresAPIKey: p.RequiresAPIKey(),
			Enabled:        s.enabledIDs[p.ID()],
			APIKeySet:      s.apiKeys[p.ID()] != "",
			DisplayOrder:   int32(i),
		}
		if _, ok := p.(AlbumCoverProvider); ok {
			info.Supports = append(info.Supports, "album_cover")
		}
		if _, ok := p.(ArtistArtProvider); ok {
			info.Supports = append(info.Supports, "artist_thumb", "artist_fanart")
		}
		if _, ok := p.(TestableProvider); ok {
			info.Testable = true
		}
		out = append(out, info)
	}
	return out
}
