package gobackend

import (
	"errors"
	"fmt"
	"path/filepath"
	"sort"
	"strings"
)

func (m *extensionManager) GetMetadataProviders() []*extensionProviderWrapper {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var providers []*extensionProviderWrapper
	for _, ext := range m.extensions {
		if ext.Enabled && ext.Manifest.IsMetadataProvider() && ext.Error == "" {
			providers = append(providers, newExtensionProviderWrapper(ext))
		}
	}
	return providers
}

func (m *extensionManager) GetDownloadProviders() []*extensionProviderWrapper {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var providers []*extensionProviderWrapper
	for _, ext := range m.extensions {
		if ext.Enabled && ext.Manifest.IsDownloadProvider() && ext.Error == "" {
			providers = append(providers, newExtensionProviderWrapper(ext))
		}
	}
	return providers
}

func metadataTrackDedupKey(track ExtTrackMetadata) string {
	if isrc := strings.TrimSpace(track.ISRC); isrc != "" {
		return "isrc:" + strings.ToUpper(isrc)
	}
	if spotifyID := strings.TrimSpace(track.SpotifyID); spotifyID != "" {
		return "spotify:" + spotifyID
	}
	if providerID := strings.TrimSpace(track.ProviderID); providerID != "" && strings.TrimSpace(track.ID) != "" {
		return providerID + ":" + strings.TrimSpace(track.ID)
	}
	return strings.TrimSpace(track.Name) + "|" + strings.TrimSpace(track.Artists)
}

func (m *extensionManager) SearchTracksWithMetadataProviders(query string, limit int, includeExtensions bool) ([]ExtTrackMetadata, error) {
	return m.SearchTracksWithMetadataProvidersForItemID(query, limit, includeExtensions, "")
}

// SearchTracksWithMetadataProvider searches one explicitly selected metadata
// provider. Unlike the priority-based search, this never falls through to a
// different extension, so callers can reliably attribute the returned fields
// to the provider selected by the user.
func (m *extensionManager) SearchTracksWithMetadataProvider(providerID, query string, limit int) ([]ExtTrackMetadata, error) {
	providerID = strings.TrimSpace(providerID)
	if providerID == "" {
		return nil, fmt.Errorf("metadata provider ID is required")
	}
	if limit <= 0 {
		limit = 20
	}

	ext, err := m.GetExtension(providerID)
	if err != nil {
		return nil, err
	}
	if ext == nil || ext.Manifest == nil || !ext.Manifest.IsMetadataProvider() {
		return nil, fmt.Errorf("extension '%s' is not a metadata provider", providerID)
	}
	if !ext.Enabled {
		return nil, fmt.Errorf("extension '%s' is disabled", providerID)
	}
	if ext.Error != "" {
		return nil, fmt.Errorf("extension '%s' is unavailable: %s", providerID, ext.Error)
	}

	result, err := newExtensionProviderWrapper(ext).SearchTracks(query, limit)
	if err != nil {
		return nil, err
	}
	if result == nil || len(result.Tracks) <= limit {
		if result == nil {
			return []ExtTrackMetadata{}, nil
		}
		return result.Tracks, nil
	}
	return result.Tracks[:limit], nil
}

func (m *extensionManager) SearchTracksWithMetadataProvidersForItemID(query string, limit int, includeExtensions bool, itemID string) ([]ExtTrackMetadata, error) {
	priority := GetMetadataProviderPriority()
	if limit <= 0 {
		limit = 20
	}

	extensionProviders := make(map[string]*extensionProviderWrapper)
	if includeExtensions {
		for _, provider := range m.GetMetadataProviders() {
			extensionProviders[provider.extension.ID] = provider
		}
	}

	orderedProviderIDs := make([]string, 0, len(priority)+len(extensionProviders))
	seenProviderIDs := make(map[string]struct{}, len(priority)+len(extensionProviders))
	for _, providerID := range priority {
		providerID = strings.TrimSpace(providerID)
		if providerID == "" {
			continue
		}
		orderedProviderIDs = append(orderedProviderIDs, providerID)
		seenProviderIDs[providerID] = struct{}{}
	}
	if includeExtensions {
		remainingIDs := make([]string, 0, len(extensionProviders))
		for providerID := range extensionProviders {
			if _, exists := seenProviderIDs[providerID]; exists {
				continue
			}
			remainingIDs = append(remainingIDs, providerID)
		}
		sort.Strings(remainingIDs)
		orderedProviderIDs = append(orderedProviderIDs, remainingIDs...)
	}

	tracks := make([]ExtTrackMetadata, 0, limit)
	seenTracks := make(map[string]struct{})
	var verificationErr error
	for _, providerID := range orderedProviderIDs {
		if isDownloadCancelled(itemID) {
			return nil, ErrDownloadCancelled
		}

		if !includeExtensions {
			continue
		}
		provider := extensionProviders[providerID]
		if provider == nil {
			continue
		}
		result, err := provider.SearchTracksForItemID(query, limit, itemID)
		providerTracks := []ExtTrackMetadata(nil)
		if result != nil {
			providerTracks = result.Tracks
		}

		if err != nil {
			if errors.Is(err, ErrDownloadCancelled) {
				return nil, ErrDownloadCancelled
			}
			if verificationErr == nil && strings.EqualFold(classifyDownloadErrorType(err.Error()), "verification_required") {
				verificationErr = fmt.Errorf(
					"verification_required: extension '%s' needs verification: %w",
					providerID,
					err,
				)
			}
			GoLog("[MetadataSearch] Search error from %s: %v\n", providerID, err)
			continue
		}

		for _, track := range providerTracks {
			key := metadataTrackDedupKey(track)
			if key == "" {
				continue
			}
			if _, exists := seenTracks[key]; exists {
				continue
			}
			seenTracks[key] = struct{}{}
			tracks = append(tracks, track)
			if len(tracks) >= limit {
				return tracks, nil
			}
		}
	}
	if len(tracks) == 0 && verificationErr != nil {
		return nil, verificationErr
	}

	return tracks, nil
}

// FindURLHandler returns the enabled handler matching the URL. When several
// extensions match (e.g. two Spotify handlers), the user's metadata provider
// priority breaks the tie deterministically instead of Go's random map
// iteration order.
func (m *extensionManager) FindURLHandler(url string) *extensionProviderWrapper {
	m.mu.RLock()
	matches := make([]*loadedExtension, 0, 2)
	for _, ext := range m.extensions {
		if ext.Enabled && ext.Manifest.MatchesURL(url) && ext.Error == "" {
			matches = append(matches, ext)
		}
	}
	m.mu.RUnlock()

	if len(matches) == 0 {
		return nil
	}
	if len(matches) > 1 {
		rank := map[string]int{}
		for i, id := range GetMetadataProviderPriority() {
			rank[strings.ToLower(strings.TrimSpace(id))] = i
		}
		sort.SliceStable(matches, func(i, j int) bool {
			ri, oki := rank[strings.ToLower(matches[i].ID)]
			rj, okj := rank[strings.ToLower(matches[j].ID)]
			switch {
			case oki && okj:
				return ri < rj
			case oki:
				return true
			case okj:
				return false
			default:
				return matches[i].ID < matches[j].ID
			}
		})
	}
	return newExtensionProviderWrapper(matches[0])
}

type ExtURLHandleResultWithExtID struct {
	Result      *ExtURLHandleResult
	ExtensionID string
}

func (m *extensionManager) HandleURLWithExtension(url string) (*ExtURLHandleResultWithExtID, error) {
	handler := m.FindURLHandler(url)
	if handler == nil {
		return nil, fmt.Errorf("no extension found to handle URL: %s", url)
	}

	result, err := handler.HandleURL(url)
	if err != nil {
		return &ExtURLHandleResultWithExtID{
			Result:      nil,
			ExtensionID: handler.extension.ID,
		}, err
	}

	return &ExtURLHandleResultWithExtID{
		Result:      result,
		ExtensionID: handler.extension.ID,
	}, nil
}

func (m *extensionManager) GetPostProcessingProviders() []*extensionProviderWrapper {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var providers []*extensionProviderWrapper
	for _, ext := range m.extensions {
		if ext.Enabled && ext.Manifest.HasPostProcessing() && ext.Error == "" {
			providers = append(providers, newExtensionProviderWrapper(ext))
		}
	}
	return providers
}

func (m *extensionManager) RunPostProcessingV2(input PostProcessInput, metadata map[string]any) (*PostProcessResult, error) {
	providers := m.GetPostProcessingProviders()
	if len(providers) == 0 {
		return &PostProcessResult{Success: true, NewFilePath: input.Path, NewFileURI: input.URI}, nil
	}

	logTag := "[PostProcessV2]"

	currentInput := input
	for _, provider := range providers {
		hooks := provider.extension.Manifest.GetPostProcessingHooks()
		for _, hook := range hooks {
			if !hook.DefaultEnabled {
				continue
			}

			ext := strings.ToLower(filepath.Ext(currentInput.Path))
			if ext == "" && currentInput.Name != "" {
				ext = strings.ToLower(filepath.Ext(currentInput.Name))
			}
			if len(hook.SupportedFormats) > 0 && ext != "" {
				supported := false
				for _, format := range hook.SupportedFormats {
					if "."+format == ext || format == ext[1:] {
						supported = true
						break
					}
				}
				if !supported {
					continue
				}
			}

			GoLog("%s Running hook %s from %s on %s\n", logTag, hook.ID, provider.extension.ID, currentInput.Path)

			result, err := provider.PostProcessV2(currentInput, metadata, hook.ID)
			if err != nil {
				GoLog("%s Hook %s failed: %v\n", logTag, hook.ID, err)
				continue
			}
			if result.Success {
				if err := validatePostProcessResult(provider.extension, currentInput, result); err != nil {
					GoLog("%s Hook %s returned an unsafe result: %v\n", logTag, hook.ID, err)
					continue
				}
			}

			if result.Success && result.NewFilePath != "" {
				currentInput.Path = result.NewFilePath
				if currentInput.Name == "" {
					currentInput.Name = filepath.Base(result.NewFilePath)
				}
			}
			if result.Success && result.NewFileURI != "" {
				currentInput.URI = result.NewFileURI
			}
		}
	}

	return &PostProcessResult{Success: true, NewFilePath: currentInput.Path, NewFileURI: currentInput.URI}, nil
}

func validatePostProcessResult(ext *loadedExtension, input PostProcessInput, result *PostProcessResult) error {
	if ext == nil || ext.Manifest == nil || result == nil {
		return fmt.Errorf("invalid post-processing result")
	}
	if result.NewFileURI != "" && result.NewFileURI != input.URI {
		return fmt.Errorf("an extension cannot replace the destination URI")
	}
	if result.NewFilePath == "" || filepath.Clean(result.NewFilePath) == filepath.Clean(input.Path) {
		return nil
	}
	if !ext.Manifest.Permissions.File {
		return fmt.Errorf("file permission is required to replace the processed file")
	}
	if !filepath.IsAbs(result.NewFilePath) {
		return fmt.Errorf("replacement file path must be absolute")
	}
	if input.Path != "" && isPathWithinBase(filepath.Dir(input.Path), result.NewFilePath) {
		return nil
	}
	if ext.DataDir != "" && isPathWithinBase(ext.DataDir, result.NewFilePath) {
		return nil
	}
	allowedDownloadDirsMu.RLock()
	defer allowedDownloadDirsMu.RUnlock()
	for _, dir := range allowedDownloadDirs {
		if isPathWithinBase(dir, result.NewFilePath) {
			return nil
		}
	}
	return fmt.Errorf("replacement file path is outside allowed directories")
}

func (m *extensionManager) GetLyricsProviders() []*extensionProviderWrapper {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var providers []*extensionProviderWrapper
	for _, ext := range m.extensions {
		if ext.Enabled && ext.Manifest.IsLyricsProvider() && ext.Error == "" {
			providers = append(providers, newExtensionProviderWrapper(ext))
		}
	}

	sort.Slice(providers, func(i, j int) bool {
		return providers[i].extension.ID < providers[j].extension.ID
	})

	return providers
}
