using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Api;
using Jellyfin.Plugin.SRFPlay.Api.Models;
using Jellyfin.Plugin.SRFPlay.Configuration;
using Jellyfin.Plugin.SRFPlay.Constants;
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
using Jellyfin.Plugin.SRFPlay.Utilities;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;

namespace Jellyfin.Plugin.SRFPlay.Channels;

/// <summary>
/// Base channel for browsing and playing content from a single SRG SSR business unit.
/// One concrete subclass exists per unit (SRF, RTS, RSI, RTR, SWI), each shown as its own tile.
/// </summary>
public abstract class SrgChannelBase : IChannel, IHasCacheKey
{
    private readonly ILogger<SrgChannelBase> _logger;
    private readonly IContentRefreshService _contentRefreshService;
    private readonly IStreamUrlResolver _streamResolver;
    private readonly IMediaSourceFactory _mediaSourceFactory;
    private readonly ICategoryService? _categoryService;
    private readonly ISRFApiClientFactory _apiClientFactory;
    private readonly IRecordingService _recordingService;

    /// <summary>
    /// Initializes a new instance of the <see cref="SrgChannelBase"/> class.
    /// </summary>
    /// <param name="loggerFactory">The logger factory.</param>
    /// <param name="contentRefreshService">The content refresh service.</param>
    /// <param name="streamResolver">The stream resolver.</param>
    /// <param name="mediaSourceFactory">The media source factory.</param>
    /// <param name="categoryService">The category service (optional).</param>
    /// <param name="apiClientFactory">The API client factory.</param>
    /// <param name="recordingService">The recording service.</param>
    protected SrgChannelBase(
        ILoggerFactory loggerFactory,
        IContentRefreshService contentRefreshService,
        IStreamUrlResolver streamResolver,
        IMediaSourceFactory mediaSourceFactory,
        ICategoryService? categoryService,
        ISRFApiClientFactory apiClientFactory,
        IRecordingService recordingService)
    {
        _logger = loggerFactory.CreateLogger<SrgChannelBase>();
        _contentRefreshService = contentRefreshService;
        _streamResolver = streamResolver;
        _mediaSourceFactory = mediaSourceFactory;
        _categoryService = categoryService;
        _apiClientFactory = apiClientFactory;
        _recordingService = recordingService;

        if (_categoryService == null)
        {
            _logger.LogWarning("CategoryService not available - category folders will be disabled");
        }

        _logger.LogDebug("SrgChannelBase initialized");
    }

    /// <summary>
    /// Gets the business unit this channel serves.
    /// </summary>
    protected abstract BusinessUnit Unit { get; }

    /// <summary>
    /// Gets the embedded resource name of this unit's logo PNG.
    /// </summary>
    protected abstract string LogoResourceName { get; }

    /// <inheritdoc />
    public string Name => $"{Unit} Play";

    /// <inheritdoc />
    public abstract string Description { get; }

    /// <inheritdoc />
    public string DataVersion => "2.0"; // Back to authenticating at channel refresh with auto-refresh for fresh tokens

    /// <inheritdoc />
    public string HomePageUrl => ApiEndpoints.SrfPlayHomepage;

    /// <inheritdoc />
    public ChannelParentalRating ParentalRating => ChannelParentalRating.GeneralAudience;

    /// <inheritdoc />
    public InternalChannelFeatures GetChannelFeatures()
    {
        _logger.LogDebug("GetChannelFeatures called");

        return new InternalChannelFeatures
        {
            ContentTypes = new List<ChannelMediaContentType>
            {
                ChannelMediaContentType.Episode,
                ChannelMediaContentType.Movie
            },
            MediaTypes = new List<ChannelMediaType>
            {
                ChannelMediaType.Video
            },
            SupportsSortOrderToggle = false,
            DefaultSortFields = new List<ChannelItemSortField>
            {
                ChannelItemSortField.DateCreated,
                ChannelItemSortField.Name
            },
            MaxPageSize = 50
        };
    }

    /// <inheritdoc />
    public Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken)
    {
        var assembly = GetType().Assembly;
        var resourceStream = assembly.GetManifestResourceStream(LogoResourceName)
            ?? assembly.GetManifestResourceStream("Jellyfin.Plugin.SRFPlay.Images.logo.png");

        if (resourceStream == null)
        {
            return Task.FromResult(new DynamicImageResponse { HasImage = false });
        }

        return Task.FromResult(new DynamicImageResponse
        {
            HasImage = true,
            Stream = resourceStream,
            Format = MediaBrowser.Model.Drawing.ImageFormat.Png
        });
    }

    /// <inheritdoc />
    public IEnumerable<ImageType> GetSupportedChannelImages()
    {
        return new List<ImageType>
        {
            ImageType.Primary,
            ImageType.Thumb
        };
    }

    /// <inheritdoc />
    public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
    {
        _logger.LogDebug("GetChannelItems called for folder {FolderId}", query.FolderId);

        try
        {
            var items = await GetFolderItemsAsync(query.FolderId, cancellationToken).ConfigureAwait(false);
            _logger.LogDebug("Returning {Count} channel items for folder {FolderId}", items.Count, query.FolderId);

            return new ChannelItemResult
            {
                Items = items,
                TotalRecordCount = items.Count
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting channel items for folder {FolderId}", query.FolderId);
            return new ChannelItemResult { Items = new List<ChannelItemInfo>(), TotalRecordCount = 0 };
        }
    }

    private async Task<List<ChannelItemInfo>> GetFolderItemsAsync(string? folderId, CancellationToken cancellationToken)
    {
        // Root level - show folder list
        if (string.IsNullOrEmpty(folderId))
        {
            return await GetRootFoldersAsync(cancellationToken).ConfigureAwait(false);
        }

        // Handle known folder types
        return folderId switch
        {
            "latest" => await GetLatestVideosAsync(cancellationToken).ConfigureAwait(false),
            "trending" => await GetTrendingVideosAsync(cancellationToken).ConfigureAwait(false),
            "live_sports" => await GetLiveSportsAsync(cancellationToken).ConfigureAwait(false),
            "recordings" => GetRecordingItems(),
            _ when folderId.StartsWith("category_", StringComparison.Ordinal) => await GetCategoryVideosAsync(folderId, cancellationToken).ConfigureAwait(false),
            _ => new List<ChannelItemInfo>()
        };
    }

    private async Task<List<ChannelItemInfo>> GetRootFoldersAsync(CancellationToken cancellationToken)
    {
        var items = new List<ChannelItemInfo>
        {
            CreateFolder("latest", "Latest Videos"),
            CreateFolder("trending", "Trending Videos"),
            CreateFolder("live_sports", "Live Sports & Events"),
            CreateFolder("recordings", "Recordings")
        };

        // Add category folders if enabled
        var config = Plugin.Instance?.Configuration;
        if (config?.EnableCategoryFolders == true && _categoryService != null)
        {
            try
            {
                var businessUnit = Unit.ToLowerString();
                var topics = await _categoryService.GetTopicsAsync(businessUnit, cancellationToken).ConfigureAwait(false);

                foreach (var topic in topics.Where(t => !string.IsNullOrEmpty(t.Id)))
                {
                    if (config.EnabledTopics != null && config.EnabledTopics.Count > 0 && !config.EnabledTopics.Contains(topic.Id!))
                    {
                        continue;
                    }

                    // Generate placeholder image for topic
                    var placeholderUrl = CreatePlaceholderImageUrl(topic.Title ?? topic.Id!, _mediaSourceFactory.GetServerBaseUrl());
                    items.Add(CreateFolder($"category_{topic.Id}", topic.Title ?? topic.Id!, topic.Lead, placeholderUrl));
                }

                _logger.LogInformation("Added {Count} category folders", topics.Count);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to load category folders - continuing without categories");
            }
        }

        return items;
    }

    private static ChannelItemInfo CreateFolder(string id, string name, string? overview = null, string? imageUrl = null)
    {
        return new ChannelItemInfo
        {
            Id = id,
            Name = name,
            Type = ChannelItemType.Folder,
            FolderType = ChannelFolderType.Container,
            ImageUrl = imageUrl,
            Overview = overview
        };
    }

    private async Task<List<ChannelItemInfo>> GetLatestVideosAsync(CancellationToken cancellationToken)
    {
        var urns = await _contentRefreshService.RefreshLatestContentAsync(Unit.ToLowerString(), cancellationToken).ConfigureAwait(false);
        return await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
    }

    private async Task<List<ChannelItemInfo>> GetTrendingVideosAsync(CancellationToken cancellationToken)
    {
        var urns = await _contentRefreshService.RefreshTrendingContentAsync(Unit.ToLowerString(), cancellationToken).ConfigureAwait(false);
        return await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
    }

    private async Task<List<ChannelItemInfo>> GetLiveSportsAsync(CancellationToken cancellationToken)
    {
        var items = new List<ChannelItemInfo>();

        try
        {
            var businessUnit = Unit.ToLowerString();

            using var apiClient = _apiClientFactory.CreateClient();
            var scheduledLivestreams = await apiClient.GetScheduledLivestreamsAsync(businessUnit, "SPORT", cancellationToken).ConfigureAwait(false);

            if (scheduledLivestreams == null)
            {
                return items;
            }

            // Filter for upcoming/current events (within next 7 days)
            var now = DateTime.UtcNow;
            var weekFromNow = now.AddDays(7);

            var upcomingEvents = scheduledLivestreams
                .Where(p => p.Urn != null &&
                           !string.IsNullOrEmpty(p.Title) &&
                           p.ValidFrom != null &&
                           p.ValidFrom.Value.ToUniversalTime() <= weekFromNow &&
                           (p.ValidTo == null || p.ValidTo.Value.ToUniversalTime() > now))
                .OrderBy(p => p.ValidFrom)
                .ToList();

            _logger.LogInformation("Found {Count} scheduled live sports events", upcomingEvents.Count);

            var urns = upcomingEvents.Select(e => e.Urn!).ToList();
            items = await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);

            // Enhance items with scheduled time information
            foreach (var item in items)
            {
                var matchingEvent = upcomingEvents.FirstOrDefault(e => item.ProviderIds.ContainsKey("SRF") && item.ProviderIds["SRF"] == e.Urn);
                if (matchingEvent?.ValidFrom != null)
                {
                    var eventTime = matchingEvent.ValidFrom.Value;
                    item.Name = $"[{eventTime:dd.MM HH:mm}] {matchingEvent.Title}";
                    item.PremiereDate = eventTime;
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load live sports events");
        }

        return items;
    }

    private List<ChannelItemInfo> GetRecordingItems()
    {
        var items = new List<ChannelItemInfo>();
        var unit = Unit.ToLowerString();
        var recordings = _recordingService.GetRecordings(RecordingState.Completed);

        foreach (var recording in recordings)
        {
            if (string.IsNullOrEmpty(recording.OutputPath) || !System.IO.File.Exists(recording.OutputPath))
            {
                continue;
            }

            // Only show recordings belonging to this unit. Recordings created before the
            // BusinessUnit tag existed have an empty value and are shown everywhere so they
            // are not lost.
            if (!string.IsNullOrEmpty(recording.BusinessUnit) &&
                !string.Equals(recording.BusinessUnit, unit, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var fileInfo = new System.IO.FileInfo(recording.OutputPath);
            var itemId = $"recording_{recording.Id}";

            var mediaSource = new MediaSourceInfo
            {
                Id = itemId,
                Name = recording.Title,
                Path = recording.OutputPath,
                Protocol = MediaProtocol.File,
                Container = "mkv",
                SupportsDirectPlay = true,
                SupportsDirectStream = true,
                SupportsTranscoding = true,
                IsRemote = false,
                Size = fileInfo.Length,
                Type = MediaSourceType.Default
            };

            var item = new ChannelItemInfo
            {
                Id = itemId,
                Name = recording.Title,
                Overview = recording.Description,
                Type = ChannelItemType.Media,
                ContentType = ChannelMediaContentType.Movie,
                MediaType = ChannelMediaType.Video,
                DateCreated = recording.RecordingStartedAt,
                ImageUrl = !string.IsNullOrEmpty(recording.ImageUrl)
                    ? CreateProxiedImageUrl(recording.ImageUrl, _mediaSourceFactory.GetServerBaseUrl())
                    : CreatePlaceholderImageUrl(recording.Title, _mediaSourceFactory.GetServerBaseUrl()),
                MediaSources = new List<MediaSourceInfo> { mediaSource }
            };

            items.Add(item);
        }

        _logger.LogInformation("Returning {Count} completed recordings as channel items", items.Count);
        return items;
    }

    private async Task<List<ChannelItemInfo>> GetCategoryVideosAsync(string folderId, CancellationToken cancellationToken)
    {
        var items = new List<ChannelItemInfo>();

        if (_categoryService == null)
        {
            _logger.LogWarning("CategoryService not available - cannot display category folder");
            return items;
        }

        try
        {
            var topicId = folderId.Substring("category_".Length);
            var businessUnit = Unit.ToLowerString();

            var shows = await _categoryService.GetShowsByTopicAsync(topicId, businessUnit, 20, cancellationToken).ConfigureAwait(false);
            var urns = new List<string>();

            using var apiClient = _apiClientFactory.CreateClient();

            foreach (var show in shows)
            {
                if (show.Id == null || cancellationToken.IsCancellationRequested)
                {
                    continue;
                }

                try
                {
                    var latestUrn = await GetLatestVideoUrnForShowAsync(apiClient, businessUnit, show, topicId, cancellationToken).ConfigureAwait(false);
                    if (latestUrn != null)
                    {
                        urns.Add(latestUrn);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Error fetching videos for show {ShowId} in category {TopicId}", show.Id, topicId);
                }
            }

            items = await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
            _logger.LogInformation("Found {Count} videos for category {TopicId}", items.Count, topicId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load category videos");
        }

        return items;
    }

    private async Task<string?> GetLatestVideoUrnForShowAsync(
        Api.SRFApiClient apiClient,
        string businessUnit,
        Api.Models.PlayV3Show show,
        string topicId,
        CancellationToken cancellationToken)
    {
        var videos = await apiClient.GetVideosForShowAsync(businessUnit, show.Id!, cancellationToken).ConfigureAwait(false);
        if (videos == null || videos.Count == 0)
        {
            _logger.LogDebug("Category {TopicId}, Show {Show} ({ShowId}): No videos returned from API", topicId, show.Title, show.Id);
            return null;
        }

        _logger.LogDebug("Category {TopicId}, Show {Show} ({ShowId}): Found {Count} videos", topicId, show.Title, show.Id, videos.Count);

        // Filter to available videos
        var now = DateTime.UtcNow;
        var availableVideos = videos.Where(v =>
            (v.ValidFrom == null || v.ValidFrom.Value.ToUniversalTime() <= now) &&
            (v.ValidTo == null || v.ValidTo.Value.ToUniversalTime() > now)).ToList();

        _logger.LogDebug("Category {TopicId}, Show {Show}: {AvailableCount} available out of {TotalCount} videos", topicId, show.Title, availableVideos.Count, videos.Count);

        if (availableVideos.Count == 0)
        {
            _logger.LogDebug("Category {TopicId}, Show {Show}: No available videos (all expired or not yet published)", topicId, show.Title);
            return null;
        }

        var latestVideo = availableVideos.OrderByDescending(v => v.Date).FirstOrDefault();
        if (latestVideo?.Urn == null)
        {
            _logger.LogWarning("Category {TopicId}, Show {Show}: Latest available video has null URN", topicId, show.Title);
            return null;
        }

        _logger.LogInformation(
            "Category {TopicId}: Added video from show {Show}: {Title} (URN: {Urn}, Date: {Date})",
            topicId,
            show.Title,
            latestVideo.Title,
            latestVideo.Urn,
            latestVideo.Date);

        return latestVideo.Urn;
    }

    /// <inheritdoc />
    public string? GetCacheKey(string? userId)
    {
        var config = Plugin.Instance?.Configuration;
        var enabledTopics = config?.EnabledTopics != null && config.EnabledTopics.Count > 0
            ? string.Join(",", config.EnabledTopics)
            : "all";

        // Use 15-minute time buckets for cache key so live content refreshes frequently
        // This ensures livestream folders update as programs start/end throughout the day
        var now = DateTime.Now;
        var timeBucket = new DateTime(now.Year, now.Month, now.Day, now.Hour, (now.Minute / 15) * 15, 0);
        var timeKey = timeBucket.ToString("yyyy-MM-dd-HH-mm", CultureInfo.InvariantCulture);

        var recordingCount = _recordingService.GetRecordings(RecordingState.Completed).Count;
        return $"{Unit}_{config?.EnableLatestContent}_{config?.EnableTrendingContent}_{config?.EnableCategoryFolders}_{enabledTopics}_{timeKey}_rec{recordingCount}";
    }

    private async Task<List<ChannelItemInfo>> ConvertUrnsToChannelItems(List<string> urns, CancellationToken cancellationToken)
    {
        var items = new List<ChannelItemInfo>();
        var config = Plugin.Instance?.Configuration;

        if (config == null)
        {
            _logger.LogWarning("Plugin configuration is null");
            return items;
        }

        _logger.LogInformation("Converting {Count} URNs to channel items", urns.Count);

        using var apiClient = _apiClientFactory.CreateClient();
        int successCount = 0;
        int failedCount = 0;
        int expiredCount = 0;
        int noStreamCount = 0;

        foreach (var urn in urns.Take(50)) // Limit to 50 items per request
        {
            try
            {
                _logger.LogDebug("Processing URN: {Urn}", urn);
                var mediaComposition = await apiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).ConfigureAwait(false);

                if (mediaComposition?.HasChapters != true)
                {
                    _logger.LogWarning("URN {Urn}: No media composition or chapters found", urn);
                    failedCount++;
                    continue;
                }

                var chapter = mediaComposition.ChapterList[0];

                // Check if content is expired
                if (_streamResolver.IsContentExpired(chapter))
                {
                    _logger.LogDebug("URN {Urn}: Content expired (ValidTo: {ValidTo})", urn, chapter.ValidTo);
                    expiredCount++;
                    continue;
                }

                // Check if content has playable streams
                if (!_streamResolver.HasPlayableContent(chapter))
                {
                    _logger.LogWarning("URN {Urn}: No playable content (likely DRM protected)", urn);
                    noStreamCount++;
                    continue;
                }

                // Generate deterministic GUID from URN
                var itemId = UrnHelper.ToGuid(urn);

                // Use factory to create MediaSourceInfo (handles stream URL, auth, proxy registration)
                var mediaSource = await _mediaSourceFactory.CreateMediaSourceAsync(
                    chapter,
                    itemId,
                    urn,
                    config.QualityPreference,
                    cancellationToken).ConfigureAwait(false);

                // Skip items without a valid media source (no stream URL available)
                if (mediaSource == null)
                {
                    if (chapter.Type == "SCHEDULED_LIVESTREAM")
                    {
                        _logger.LogDebug(
                            "URN {Urn}: Skipping upcoming livestream '{Title}' - stream not yet available (starts at {ValidFrom})",
                            urn,
                            chapter.Title,
                            chapter.ValidFrom);
                    }
                    else
                    {
                        _logger.LogWarning(
                            "URN {Urn}: Skipping '{Title}' - no valid stream URL available",
                            urn,
                            chapter.Title);
                        noStreamCount++;
                    }

                    continue;
                }

                // Build overview
                var overview = chapter.Description ?? chapter.Lead;

                // Determine image URL based on configuration
                string? imageUrl;
                var generateTitleCards = config.GenerateTitleCards;

                if (generateTitleCards)
                {
                    // Generate title card with content name
                    _logger.LogDebug("URN {Urn}: Generating title card for '{Title}'", urn, chapter.Title);
                    imageUrl = CreatePlaceholderImageUrl(chapter.Title ?? "SRF Play", _mediaSourceFactory.GetServerBaseUrl());
                }
                else
                {
                    // Use SRF-provided images - prefer chapter image, fall back to show image
                    var originalImageUrl = chapter.ImageUrl;
                    if (string.IsNullOrEmpty(originalImageUrl) && mediaComposition.Show != null)
                    {
                        originalImageUrl = mediaComposition.Show.ImageUrl;
                        _logger.LogDebug("URN {Urn}: Using show image as fallback", urn);
                    }

                    if (string.IsNullOrEmpty(originalImageUrl))
                    {
                        _logger.LogDebug("URN {Urn}: No image URL available for '{Title}', using placeholder", urn, chapter.Title);
                        imageUrl = CreatePlaceholderImageUrl(chapter.Title ?? "SRF Play", _mediaSourceFactory.GetServerBaseUrl());
                    }
                    else
                    {
                        imageUrl = CreateProxiedImageUrl(originalImageUrl, _mediaSourceFactory.GetServerBaseUrl());
                    }
                }

                // Use ValidFrom for premiere date if this is a scheduled livestream, otherwise use Date
                var premiereDate = chapter.Type == "SCHEDULED_LIVESTREAM" ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime();

                // Store authenticated URL - tokens refresh automatically via scheduled channel scans
                var item = new ChannelItemInfo
                {
                    Id = itemId,
                    Name = chapter.Title,
                    Overview = overview,
                    ImageUrl = imageUrl,
                    Type = ChannelItemType.Media,
                    ContentType = ChannelMediaContentType.Episode,
                    MediaType = ChannelMediaType.Video,
                    DateCreated = chapter.Date?.ToUniversalTime(),
                    PremiereDate = premiereDate,
                    ProductionYear = chapter.Date?.Year,
                    RunTimeTicks = chapter.Duration > 0 ? TimeSpan.FromMilliseconds(chapter.Duration).Ticks : null,
                    ProviderIds = new Dictionary<string, string>
                    {
                        { "SRF", urn }
                    }
                };

                // Add series info if available
                if (mediaComposition.Show != null)
                {
                    item.SeriesName = mediaComposition.Show.Title;
                }

                items.Add(item);
                successCount++;
                _logger.LogInformation("URN {Urn}: Successfully converted to channel item - {Title}", urn, chapter.Title);
                _logger.LogDebug(
                    "URN {Urn}: MediaSource created via factory - DirectPlay={DirectPlay}, Transcoding={Transcoding}",
                    urn,
                    mediaSource.SupportsDirectPlay,
                    mediaSource.SupportsTranscoding);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error converting URN {Urn} to channel item", urn);
                failedCount++;
            }
        }

        _logger.LogInformation(
            "Conversion complete: {Success} successful, {Failed} failed, {Expired} expired, {NoStream} no stream",
            successCount,
            failedCount,
            expiredCount,
            noStreamCount);

        return items;
    }

    /// <summary>
    /// Creates a proxy URL for an image to fix Content-Type headers from SRF CDN.
    /// </summary>
    /// <param name="originalUrl">The original SRF image URL.</param>
    /// <param name="serverUrl">The server base URL.</param>
    /// <returns>The proxied image URL, or null if original is null.</returns>
    private static string? CreateProxiedImageUrl(string? originalUrl, string serverUrl)
    {
        if (string.IsNullOrEmpty(originalUrl))
        {
            return null;
        }

        // Encode the original URL as base64 for safe transport
        var encodedUrl = Convert.ToBase64String(Encoding.UTF8.GetBytes(originalUrl));
        return $"{serverUrl}/Plugins/SRFPlay/Proxy/Image?url={encodedUrl}";
    }

    /// <summary>
    /// Creates a placeholder image URL with the given text.
    /// </summary>
    /// <param name="text">The text to display on the placeholder.</param>
    /// <param name="serverUrl">The server base URL.</param>
    /// <returns>The placeholder image URL.</returns>
    private static string CreatePlaceholderImageUrl(string text, string serverUrl)
    {
        var encodedText = Convert.ToBase64String(Encoding.UTF8.GetBytes(text));
        return $"{serverUrl}/Plugins/SRFPlay/Proxy/Placeholder?text={encodedText}";
    }

    /// <inheritdoc />
    public bool IsEnabledFor(string userId)
    {
        return true;
    }
}
