package gobackend

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

func manifestCapabilityStringList(manifest *ExtensionManifest, key string) []string {
	if manifest == nil || manifest.Capabilities == nil {
		return nil
	}

	raw, ok := manifest.Capabilities[key]
	if !ok {
		return nil
	}

	values, ok := raw.([]any)
	if !ok {
		return nil
	}

	result := make([]string, 0, len(values))
	for _, value := range values {
		str, ok := value.(string)
		if !ok {
			continue
		}
		trimmed := strings.ToLower(strings.TrimSpace(str))
		if trimmed == "" {
			continue
		}
		result = append(result, trimmed)
	}
	return result
}

func extensionReplacesBuiltInProvider(ext *loadedExtension, providerID string) bool {
	if ext == nil {
		return false
	}

	normalized := strings.ToLower(strings.TrimSpace(providerID))
	if normalized == "" {
		return false
	}

	for _, replaced := range manifestCapabilityStringList(ext.Manifest, "replacesBuiltInProviders") {
		if replaced == normalized {
			return true
		}
	}

	return false
}

func trimKnownProviderPrefix(trackID, providerID string) string {
	trimmedID := strings.TrimSpace(trackID)
	normalizedProvider := strings.ToLower(strings.TrimSpace(providerID))
	if trimmedID == "" || normalizedProvider == "" {
		return trimmedID
	}

	prefix := normalizedProvider + ":"
	if strings.HasPrefix(strings.ToLower(trimmedID), prefix) {
		return trimmedID[len(prefix):]
	}

	return trimmedID
}

func resolvePreferredTrackIDForExtension(ext *loadedExtension, req DownloadRequest, explicitTrackID string) string {
	candidates := make([]string, 0, 8)
	appendCandidate := func(value string) {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			return
		}
		for _, existing := range candidates {
			if existing == trimmed {
				return
			}
		}
		candidates = append(candidates, trimmed)
	}

	appendCandidate(explicitTrackID)

	if extensionReplacesBuiltInProvider(ext, "tidal") {
		appendCandidate(req.TidalID)
		appendCandidate(trimKnownProviderPrefix(req.SpotifyID, "tidal"))
	}
	if extensionReplacesBuiltInProvider(ext, "qobuz") {
		appendCandidate(req.QobuzID)
		appendCandidate(trimKnownProviderPrefix(req.SpotifyID, "qobuz"))
	}
	if extensionReplacesBuiltInProvider(ext, "deezer") {
		appendCandidate(req.DeezerID)
		appendCandidate(trimKnownProviderPrefix(req.SpotifyID, "deezer"))
	}
	if extensionReplacesBuiltInProvider(ext, "spotify") {
		appendCandidate(trimKnownProviderPrefix(req.SpotifyID, "spotify"))
		appendCandidate(req.SpotifyID)
	}

	appendCandidate(req.SpotifyID)
	appendCandidate(req.TidalID)
	appendCandidate(req.QobuzID)
	appendCandidate(req.DeezerID)

	if len(candidates) == 0 {
		return ""
	}
	return candidates[0]
}

func normalizeDownloadResultExtension(candidates ...string) string {
	for _, candidate := range candidates {
		ext := strings.TrimSpace(strings.ToLower(candidate))
		if ext == "" {
			continue
		}
		if !strings.HasPrefix(ext, ".") {
			ext = "." + ext
		}
		if ext == ".mp4" {
			return ".m4a"
		}
		return ext
	}
	return ""
}

// discardRejectedExtensionOutput removes only a newly downloaded file inside
// the host-selected output directory. Existing-library hits are never removed,
// nor are paths outside that narrow directory.
func discardRejectedExtensionOutput(result *ExtDownloadResult, requestedOutputPath string) {
	if result == nil || result.AlreadyExists {
		return
	}

	resultPath := strings.TrimSpace(result.FilePath)
	requestedPath := strings.TrimSpace(requestedOutputPath)
	if resultPath == "" || requestedPath == "" ||
		strings.HasPrefix(resultPath, "content://") ||
		strings.HasPrefix(resultPath, "/proc/self/fd/") {
		return
	}

	resultAbs, resultErr := filepath.Abs(resultPath)
	outputDirAbs, outputErr := filepath.Abs(filepath.Dir(requestedPath))
	if resultErr != nil || outputErr != nil {
		return
	}
	relative, err := filepath.Rel(outputDirAbs, resultAbs)
	if err != nil || relative == "." || relative == ".." ||
		strings.HasPrefix(relative, ".."+string(filepath.Separator)) ||
		filepath.IsAbs(relative) {
		return
	}

	if err := os.Remove(resultAbs); err != nil && !os.IsNotExist(err) {
		GoLog("[DownloadWithExtensionFallback] Warning: failed to remove rejected provider output %q: %v\n", resultAbs, err)
	}
}

func normalizeExtensionDownloadResult(result *ExtDownloadResult) (DownloadResult, bool) {
	if result == nil {
		return DownloadResult{}, false
	}

	downloadResult := DownloadResult{
		FilePath:                    strings.TrimSpace(result.FilePath),
		BitDepth:                    result.BitDepth,
		SampleRate:                  result.SampleRate,
		AudioCodec:                  strings.TrimSpace(result.AudioCodec),
		Title:                       result.Title,
		Artist:                      result.Artist,
		Album:                       result.Album,
		ReleaseDate:                 result.ReleaseDate,
		TrackNumber:                 result.TrackNumber,
		TotalTracks:                 result.TotalTracks,
		DiscNumber:                  result.DiscNumber,
		TotalDiscs:                  result.TotalDiscs,
		ISRC:                        result.ISRC,
		CoverURL:                    result.CoverURL,
		Genre:                       result.Genre,
		Label:                       result.Label,
		Copyright:                   result.Copyright,
		Composer:                    result.Composer,
		Comment:                     result.Comment,
		Explicit:                    result.Explicit,
		AlbumType:                   result.AlbumType,
		UPC:                         result.UPC,
		LyricsLRC:                   result.LyricsLRC,
		DecryptionKey:               result.DecryptionKey,
		Decryption:                  normalizeDownloadDecryptionInfo(result.Decryption, result.DecryptionKey),
		ActualExtension:             normalizeDownloadResultExtension(result.ActualExtension, result.OutputExtension),
		ActualContainer:             strings.TrimSpace(result.ActualContainer),
		RequiresContainerConversion: result.RequiresContainerConversion,
	}

	alreadyExists := result.AlreadyExists
	if strings.HasPrefix(downloadResult.FilePath, "EXISTS:") {
		alreadyExists = true
		downloadResult.FilePath = strings.TrimPrefix(downloadResult.FilePath, "EXISTS:")
	}

	enrichResultQualityFromFile(&downloadResult)
	return downloadResult, alreadyExists
}

// overlayStr sets *dst = src when dst is empty and src is not. If field is
// non-empty it logs "<field> from enrichment: <src>" on overlay.
func overlayStr(dst *string, src, field string) {
	if src == "" || *dst != "" {
		return
	}
	*dst = src
	if field != "" {
		GoLog("[DownloadWithExtensionFallback] %s from enrichment: %s\n", field, src)
	}
}

// overlayExtensionReleaseMetadata carries release-level metadata discovered by
// the source catalog into a provider-agnostic download request. Existing
// source values always win; enrichment only fills fields that were missing.
func overlayExtensionReleaseMetadata(req *DownloadRequest, track ExtTrackMetadata) {
	if req == nil {
		return
	}
	overlayStr(&req.AlbumType, track.AlbumType, "AlbumType")
	overlayStr(&req.UPC, track.UPC, "UPC")
	overlayStr(&req.Comment, track.Comment, "Comment")
	if !req.Explicit && track.Explicit {
		req.Explicit = true
		GoLog("[DownloadWithExtensionFallback] Explicit flag from enrichment\n")
	}
}

// overlayStrTrim is overlayStr but treats a whitespace-only dst as empty too.
func overlayStrTrim(dst *string, src string) {
	if src == "" || strings.TrimSpace(*dst) != "" {
		return
	}
	*dst = src
}

// overlayInt sets *dst = src when dst is zero and src is positive. If field is
// non-empty it logs "<field> from enrichment: <src>" on overlay.
func overlayInt(dst *int, src int, field string) {
	if src <= 0 || *dst != 0 {
		return
	}
	*dst = src
	if field != "" {
		GoLog("[DownloadWithExtensionFallback] %s from enrichment: %d\n", field, src)
	}
}

func overlayExtensionDownloadMetadata(resp *DownloadResponse, result *ExtDownloadResult) {
	if resp == nil || result == nil {
		return
	}

	overlayStrTrim(&resp.Title, result.Title)
	overlayStrTrim(&resp.Artist, result.Artist)
	overlayStrTrim(&resp.Album, result.Album)
	overlayStrTrim(&resp.AlbumArtist, result.AlbumArtist)
	overlayInt(&resp.TrackNumber, result.TrackNumber, "")
	overlayInt(&resp.DiscNumber, result.DiscNumber, "")
	overlayInt(&resp.TotalTracks, result.TotalTracks, "")
	overlayInt(&resp.TotalDiscs, result.TotalDiscs, "")
	overlayStrTrim(&resp.ReleaseDate, result.ReleaseDate)
	overlayStrTrim(&resp.CoverURL, result.CoverURL)
	overlayStrTrim(&resp.ISRC, result.ISRC)
	overlayStrTrim(&resp.Genre, result.Genre)
	overlayStrTrim(&resp.Label, result.Label)
	overlayStrTrim(&resp.Copyright, result.Copyright)
	overlayStrTrim(&resp.Composer, result.Composer)
	overlayStrTrim(&resp.Comment, result.Comment)
	overlayStrTrim(&resp.AlbumType, result.AlbumType)
	overlayStrTrim(&resp.UPC, result.UPC)
	if result.Explicit {
		resp.Explicit = true
	}
	if result.LyricsLRC != "" {
		resp.LyricsLRC = result.LyricsLRC
	}
	if result.DecryptionKey != "" {
		resp.DecryptionKey = result.DecryptionKey
	}
	if normalized := normalizeDownloadDecryptionInfo(result.Decryption, result.DecryptionKey); normalized != nil {
		resp.Decryption = normalized
	}
	if ext := normalizeDownloadResultExtension(result.ActualExtension, result.OutputExtension); ext != "" {
		resp.ActualExtension = ext
	}
	if container := strings.TrimSpace(result.ActualContainer); container != "" {
		resp.ActualContainer = container
	}
	if result.RequiresContainerConversion {
		resp.RequiresContainerConversion = true
	}
}

func applyExtensionRequestFallbacks(resp *DownloadResponse, req DownloadRequest) {
	if resp == nil {
		return
	}

	overlayStr(&resp.Album, req.AlbumName, "")
	overlayStr(&resp.AlbumArtist, req.AlbumArtist, "")
	overlayStr(&resp.ReleaseDate, req.ReleaseDate, "")
	overlayStr(&resp.ISRC, req.ISRC, "")
	overlayInt(&resp.TrackNumber, req.TrackNumber, "")
	overlayInt(&resp.TotalTracks, req.TotalTracks, "")
	overlayInt(&resp.DiscNumber, req.DiscNumber, "")
	overlayInt(&resp.TotalDiscs, req.TotalDiscs, "")
	overlayStr(&resp.CoverURL, req.CoverURL, "")
	overlayStr(&resp.Comment, req.Comment, "")
	overlayStr(&resp.AlbumType, req.AlbumType, "")
	overlayStr(&resp.UPC, req.UPC, "")
	if req.Explicit {
		resp.Explicit = true
	}
}

func shouldStopProviderFallback(availability *ExtAvailabilityResult) bool {
	return availability != nil && availability.SkipFallback
}

func fallbackRuntimeHealthStatus(ext *loadedExtension) string {
	if ext == nil || ext.Manifest == nil || len(ext.Manifest.ServiceHealth) == 0 {
		return "unknown"
	}

	status := strings.ToLower(strings.TrimSpace(CheckExtensionHealthCached(ext).Status))
	switch status {
	case "online", "degraded", "offline":
		return status
	default:
		return "unknown"
	}
}

func prioritizeFallbackProvidersByHealth(priority []string, extManager *extensionManager, protectedProvider string) []string {
	if len(priority) == 0 || extManager == nil {
		return priority
	}

	online := make([]string, 0, len(priority))
	degraded := make([]string, 0, len(priority))
	unknown := make([]string, 0, len(priority))

	for _, rawProviderID := range priority {
		providerID := strings.TrimSpace(rawProviderID)
		if providerID == "" {
			continue
		}
		if strings.EqualFold(providerID, protectedProvider) || !isExtensionFallbackAllowed(providerID) {
			unknown = append(unknown, providerID)
			continue
		}

		ext, err := extManager.GetExtension(providerID)
		if err != nil || ext == nil || !ext.Enabled || ext.Error != "" || ext.Manifest == nil || !ext.Manifest.IsDownloadProvider() {
			unknown = append(unknown, providerID)
			continue
		}

		switch fallbackRuntimeHealthStatus(ext) {
		case "online":
			online = append(online, providerID)
		case "degraded":
			degraded = append(degraded, providerID)
		case "offline":
			GoLog("[DownloadWithExtensionFallback] Skipping extension provider %s (service health offline)\n", providerID)
		default:
			unknown = append(unknown, providerID)
		}
	}

	result := make([]string, 0, len(online)+len(degraded)+len(unknown))
	result = append(result, online...)
	result = append(result, degraded...)
	result = append(result, unknown...)
	return result
}

// moveProviderToFront preserves the user's explicit provider selection after
// health-based fallback sorting. Health may order the remaining fallback
// candidates, but it must never silently replace the provider the user picked.
func moveProviderToFront(priority []string, providerID string) []string {
	providerID = strings.TrimSpace(providerID)
	if providerID == "" || len(priority) < 2 {
		return priority
	}

	selectedIndex := -1
	for i, candidate := range priority {
		if strings.EqualFold(strings.TrimSpace(candidate), providerID) {
			selectedIndex = i
			break
		}
	}
	if selectedIndex <= 0 {
		return priority
	}

	reordered := make([]string, 0, len(priority))
	reordered = append(reordered, priority[selectedIndex])
	reordered = append(reordered, priority[:selectedIndex]...)
	reordered = append(reordered, priority[selectedIndex+1:]...)
	return reordered
}

func resolveExtensionAvailabilityReason(availability *ExtAvailabilityResult, err error) string {
	if availability != nil {
		if reason := strings.TrimSpace(availability.Reason); reason != "" {
			return reason
		}
	}
	if err != nil {
		return err.Error()
	}
	return "extension requested no further fallback"
}

func buildExtensionFallbackStoppedResponse(providerID string, availability *ExtAvailabilityResult, err error) *DownloadResponse {
	reason := resolveExtensionAvailabilityReason(availability, err)
	errorType := classifyDownloadErrorType(reason)
	if errorType == "unknown" {
		errorType = "extension_error"
	}
	return &DownloadResponse{
		Success:   false,
		Error:     fmt.Sprintf("Fallback stopped by %s: %s", providerID, reason),
		ErrorType: errorType,
		Service:   providerID,
	}
}

func shouldAbortCancelledFallback(itemID string, err error) bool {
	if errors.Is(err, ErrDownloadCancelled) {
		return true
	}
	return itemID != "" && isDownloadCancelled(itemID)
}

func normalizeExtensionDownloadErrorType(errorType, message string) string {
	normalized := strings.TrimSpace(errorType)
	classified := classifyDownloadErrorType(message)
	if classified != "" && classified != "unknown" {
		switch strings.ToLower(normalized) {
		case "", "unknown", "runtime_error", "api_error", "download_error", "extension_error":
			return classified
		}
	}
	return normalized
}
