package tags

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 compiled-in provider registry with the
// tag_provider_settings table at boot and on every admin update, and owns
// the tag_sources_meta current_version stamp. Parallel to (and independent
// of) coverart's SettingsService — the two never share a row, so a new tag
// source is added without touching art settings.
type SettingsService struct {
	pool   *pgxpool.Pool
	logger *slog.Logger

	mu             sync.RWMutex
	enabledIDs     map[string]bool
	apiKeys        map[string]string
	currentVersion int32
}

// ProviderUpdatePatch is the admin-PATCH body. nil pointers mean "leave
// unchanged"; non-nil means "set" (empty APIKey clears it).
type ProviderUpdatePatch struct {
	Enabled *bool
	APIKey  *string
}

// ProviderInfo is the settings view of a provider for the admin GET handler.
type ProviderInfo struct {
	ID             string
	DisplayName    string
	RequiresAPIKey bool
	Supports       []string
	Enabled        bool
	APIKeySet      bool
	DisplayOrder   int32
	Testable       bool
}

// NewSettingsService boots the service: upserts default rows for any
// registered provider, Configures each with its row, and loads the version.
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("tag settings boot: %w", err)
	}
	return s, nil
}

// reconcile reads the DB, inserts defaults for missing providers, Configures
// each registered provider, and caches the current version.
func (s *SettingsService) reconcile(ctx context.Context) error {
	q := dbq.New(s.pool)

	registered := AllProviders()
	for i, p := range registered {
		if err := q.UpsertTagProviderSettings(ctx, dbq.UpsertTagProviderSettingsParams{
			ProviderID:   p.ID(),
			Enabled:      p.DefaultEnabled(),
			DisplayOrder: int32(i),
		}); err != nil {
			return fmt.Errorf("upsert provider %q: %w", p.ID(), err)
		}
	}

	rows, err := q.ListTagProviderSettings(ctx)
	if err != nil {
		return fmt.Errorf("list provider settings: %w", err)
	}
	settingsByID := map[string]dbq.TagProviderSetting{}
	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("tag 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("tag settings: provider Configure failed", "provider", p.ID(), "err", err)
		}
	}

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

// EnabledTrackTagProviders returns the enabled providers implementing
// TrackTagProvider, in registration order. Snapshot — do not mutate.
func (s *SettingsService) EnabledTrackTagProviders() []TrackTagProvider {
	s.mu.RLock()
	defer s.mu.RUnlock()
	var out []TrackTagProvider
	for _, p := range AllProviders() {
		if !s.enabledIDs[p.ID()] {
			continue
		}
		if tp, ok := p.(TrackTagProvider); ok {
			out = append(out, tp)
		}
	}
	return out
}

// EnabledArtistTagProviders returns the enabled providers implementing
// ArtistTagProvider, in registration order. Snapshot — do not mutate.
//
// Separate from EnabledTrackTagProviders rather than one call with a capability
// argument: the two chains are consumed by different drains, and a provider may
// implement one capability without the other.
func (s *SettingsService) EnabledArtistTagProviders() []ArtistTagProvider {
	s.mu.RLock()
	defer s.mu.RUnlock()
	var out []ArtistTagProvider
	for _, p := range AllProviders() {
		if !s.enabledIDs[p.ID()] {
			continue
		}
		if ap, ok := p.(ArtistTagProvider); ok {
			out = append(out, ap)
		}
	}
	return out
}

// CurrentVersion returns the version the enricher stamps onto rows.
func (s *SettingsService) CurrentVersion() int32 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.currentVersion
}

// UpdateProvider applies an admin patch. Returns versionBumped=true if the
// enabled set changed (not on key-only changes). Re-Configures the provider.
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)

	s.mu.RLock()
	beforeSig := enabledSetSignature(s.enabledIDs)
	currentEnabled := s.enabledIDs[providerID]
	s.mu.RUnlock()

	enabledVal := currentEnabled
	if patch.Enabled != nil {
		enabledVal = *patch.Enabled
	}
	if err := q.UpdateTagProviderSettings(ctx, dbq.UpdateTagProviderSettingsParams{
		ProviderID: providerID,
		Enabled:    enabledVal,
		Column3:    patch.APIKey != nil,
		ApiKey:     patch.APIKey,
	}); err != nil {
		return false, fmt.Errorf("update provider settings: %w", err)
	}

	rows, err := q.ListTagProviderSettings(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("tag settings: Configure after update failed", "provider", providerID, "err", err)
			}
		}
	}

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

// enabledSetSignature is a deterministic string of the 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 bumps the version + stores the new provider
// hash when the compiled-in provider set changed since the last boot, so
// 'none' rows become eligible for a retry through the widened chain. This
// is what makes "add a source" a zero-touch change — a new registered
// provider trips this on the next start.
func (s *SettingsService) BumpVersionIfProvidersChanged(ctx context.Context) (version int32, bumped bool, err error) {
	hash := registeredProvidersHash()
	q := dbq.New(s.pool)

	stored, qerr := q.GetTagProvidersHash(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.BumpTagVersionAndSetProvidersHash(ctx, hash)
	if berr != nil {
		return 0, false, fmt.Errorf("bump version: %w", berr)
	}
	s.mu.Lock()
	s.currentVersion = newVer
	s.mu.Unlock()
	return newVer, true, nil
}

// registeredProvidersHash is 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)
	sum := sha256.Sum256([]byte(strings.Join(ids, ":")))
	return hex.EncodeToString(sum[:])
}

// BumpVersion unconditionally increments the version. Used by the admin
// "re-search missing tags" action to re-attempt every 'none' row next pass.
func (s *SettingsService) BumpVersion(ctx context.Context) (int32, error) {
	q := dbq.New(s.pool)
	newVer, err := q.BumpTagVersionAndSetProvidersHash(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 if the provider supports it.
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 admin-GET view 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.(TrackTagProvider); ok {
			info.Supports = append(info.Supports, "track_tags")
		}
		if _, ok := p.(TestableProvider); ok {
			info.Testable = true
		}
		out = append(out, info)
	}
	return out
}
