using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.Logging;

namespace Jellyfin.Plugin.SRFPlay.Services;

/// <summary>
/// Service for proxying SRF Play streams and managing authentication.
/// </summary>
public class StreamProxyService : IDisposable
{
    private readonly ILogger<StreamProxyService> _logger;
    private readonly StreamUrlResolver _streamResolver;
    private readonly HttpClient _httpClient;
    private readonly ConcurrentDictionary<string, StreamInfo> _streamMappings;
    private bool _disposed;

    /// <summary>
    /// Initializes a new instance of the <see cref="StreamProxyService"/> class.
    /// </summary>
    /// <param name="logger">The logger.</param>
    /// <param name="streamResolver">The stream URL resolver.</param>
    public StreamProxyService(ILogger<StreamProxyService> logger, StreamUrlResolver streamResolver)
    {
        _logger = logger;
        _streamResolver = streamResolver;
        _httpClient = new HttpClient
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
        _streamMappings = new ConcurrentDictionary<string, StreamInfo>();
    }

    /// <summary>
    /// Registers a stream for proxying.
    /// </summary>
    /// <param name="itemId">The item ID.</param>
    /// <param name="authenticatedUrl">The authenticated stream URL.</param>
    public void RegisterStream(string itemId, string authenticatedUrl)
    {
        var tokenExpiry = ExtractTokenExpiry(authenticatedUrl);
        var unauthenticatedUrl = StripAuthenticationFromUrl(authenticatedUrl);

        var streamInfo = new StreamInfo
        {
            AuthenticatedUrl = authenticatedUrl,
            UnauthenticatedUrl = unauthenticatedUrl,
            RegisteredAt = DateTime.UtcNow,
            TokenExpiresAt = tokenExpiry
        };

        // Register with the provided item ID
        _streamMappings.AddOrUpdate(itemId, streamInfo, (key, old) => streamInfo);

        // Also register with alternative GUID formats to handle Jellyfin's ID transformations
        if (Guid.TryParse(itemId, out var guid))
        {
            var formats = new[]
            {
                guid.ToString("N"), // Without dashes: 00000000000000000000000000000000
                guid.ToString("D"), // With dashes: 00000000-0000-0000-0000-000000000000
                guid.ToString("B"), // With braces: {00000000-0000-0000-0000-000000000000}
            };

            foreach (var format in formats)
            {
                if (format != itemId) // Don't duplicate the original
                {
                    _streamMappings.AddOrUpdate(format, streamInfo, (key, old) => streamInfo);
                }
            }

            _logger.LogDebug("Registered stream with {Count} GUID format variations", formats.Length);
        }

        if (tokenExpiry.HasValue)
        {
            _logger.LogInformation(
                "Registered stream for item {ItemId} (token expires at {ExpiresAt} UTC): {Url}",
                itemId,
                tokenExpiry.Value,
                authenticatedUrl);
        }
        else
        {
            _logger.LogInformation("Registered stream for item {ItemId}: {Url}", itemId, authenticatedUrl);
        }
    }

    /// <summary>
    /// Gets the authenticated URL for an item.
    /// </summary>
    /// <param name="itemId">The item ID.</param>
    /// <returns>The authenticated URL, or null if not found or expired.</returns>
    public string? GetAuthenticatedUrl(string itemId)
    {
        // Try direct lookup first
        if (_streamMappings.TryGetValue(itemId, out var streamInfo))
        {
            return ValidateAndReturnStream(itemId, streamInfo);
        }

        // Fallback: Try to find by GUID variations (with/without dashes)
        // This handles cases where Jellyfin uses different GUID formats
        var normalizedId = NormalizeGuid(itemId);
        if (normalizedId != null)
        {
            foreach (var kvp in _streamMappings)
            {
                var normalizedKey = NormalizeGuid(kvp.Key);
                if (normalizedKey != null && normalizedKey == normalizedId)
                {
                    _logger.LogInformation(
                        "Found stream by GUID normalization - Requested: {RequestedId}, Registered: {RegisteredId}",
                        itemId,
                        kvp.Key);
                    var url = ValidateAndReturnStream(kvp.Key, kvp.Value);
                    if (url != null)
                    {
                        return url; // Found valid stream
                    }

                    // Stream found but expired, continue to next fallback
                    _logger.LogDebug("GUID-normalized stream was expired, trying other fallbacks");
                    break; // Exit foreach, continue to next fallback strategy
                }
            }
        }

        // Last resort: Use active stream fallbacks (helps when Jellyfin creates random transcoding session IDs)
        var activeStreams = _streamMappings.Where(kvp =>
        {
            if (!kvp.Value.TokenExpiresAt.HasValue)
            {
                return true; // No expiry
            }

            return DateTime.UtcNow < kvp.Value.TokenExpiresAt.Value;
        }).ToList();

        if (activeStreams.Count == 1)
        {
            _logger.LogWarning(
                "No exact match for {RequestedId}, but found single active stream {RegisteredId} - using as fallback",
                itemId,
                activeStreams[0].Key);
            return ValidateAndReturnStream(activeStreams[0].Key, activeStreams[0].Value);
        }

        // If multiple active streams, use the most recently registered one (likely the one being transcoded)
        // This handles cases where Jellyfin creates a random transcoding session ID seconds after registration
        if (activeStreams.Count > 1)
        {
            var mostRecent = activeStreams.OrderByDescending(kvp => kvp.Value.RegisteredAt).First();
            var age = DateTime.UtcNow - mostRecent.Value.RegisteredAt;

            // Only use this fallback if the stream was registered very recently (within 30 seconds)
            // This indicates it's likely the stream currently being set up for transcoding
            if (age.TotalSeconds < 30)
            {
                _logger.LogWarning(
                    "No exact match for {RequestedId}, but using most recently registered stream {RegisteredId} (registered {Seconds}s ago) as fallback",
                    itemId,
                    mostRecent.Key,
                    age.TotalSeconds);
                return ValidateAndReturnStream(mostRecent.Key, mostRecent.Value);
            }
        }

        _logger.LogWarning(
            "No stream mapping found for item {ItemId}. Active streams: {Count}. Registered IDs: {RegisteredIds}",
            itemId,
            activeStreams.Count,
            string.Join(", ", _streamMappings.Keys));
        return null;
    }

    /// <summary>
    /// Validates a stream and returns its URL if valid.
    /// </summary>
    private string? ValidateAndReturnStream(string itemId, StreamInfo streamInfo)
    {
        // Check if token has expired
        if (streamInfo.TokenExpiresAt.HasValue)
        {
            var now = DateTime.UtcNow;
            if (now >= streamInfo.TokenExpiresAt.Value)
            {
                _logger.LogWarning(
                    "Token expired for item {ItemId} (expired at {ExpiresAt}, now is {Now}) - attempting to refresh",
                    itemId,
                    streamInfo.TokenExpiresAt.Value,
                    now);

                // Try to refresh the token
                var refreshedUrl = RefreshToken(itemId, streamInfo);
                if (refreshedUrl != null)
                {
                    _logger.LogInformation("Successfully refreshed token for item {ItemId}", itemId);
                    return refreshedUrl;
                }

                _logger.LogWarning("Failed to refresh token for item {ItemId}, removing mapping", itemId);
                _streamMappings.TryRemove(itemId, out _);
                return null;
            }

            _logger.LogDebug(
                "Token valid for item {ItemId} (expires at {ExpiresAt}, {TimeLeft} remaining)",
                itemId,
                streamInfo.TokenExpiresAt.Value,
                streamInfo.TokenExpiresAt.Value - now);
        }

        return streamInfo.AuthenticatedUrl;
    }

    /// <summary>
    /// Attempts to refresh an expired token.
    /// </summary>
    private string? RefreshToken(string itemId, StreamInfo streamInfo)
    {
        if (string.IsNullOrEmpty(streamInfo.UnauthenticatedUrl))
        {
            _logger.LogWarning("Cannot refresh token for {ItemId} - no unauthenticated URL stored", itemId);
            return null;
        }

        try
        {
            // Re-authenticate the stream URL synchronously (blocking call)
            var newAuthenticatedUrl = _streamResolver.GetAuthenticatedStreamUrlAsync(
                streamInfo.UnauthenticatedUrl,
                CancellationToken.None).GetAwaiter().GetResult();

            if (string.IsNullOrEmpty(newAuthenticatedUrl))
            {
                return null;
            }

            // Update the stream info with the new token
            var newTokenExpiry = ExtractTokenExpiry(newAuthenticatedUrl);
            streamInfo.AuthenticatedUrl = newAuthenticatedUrl;
            streamInfo.TokenExpiresAt = newTokenExpiry;

            _logger.LogInformation(
                "Refreshed token for item {ItemId} (new expiry: {ExpiresAt} UTC)",
                itemId,
                newTokenExpiry);

            return newAuthenticatedUrl;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error refreshing token for item {ItemId}", itemId);
            return null;
        }
    }

    /// <summary>
    /// Strips authentication parameters from a URL to get the base unauthenticated URL.
    /// </summary>
    private string StripAuthenticationFromUrl(string url)
    {
        try
        {
            var uri = new Uri(url);
            var query = uri.Query;

            // Remove hdnts authentication parameter (Akamai token authentication)
            if (query.Contains("hdnts=", StringComparison.OrdinalIgnoreCase))
            {
                // Keep other parameters like caption, webvttbaseurl
                var queryParams = System.Web.HttpUtility.ParseQueryString(query);
                queryParams.Remove("hdnts");

                var newQuery = queryParams.Count > 0 ? "?" + queryParams.ToString() : string.Empty;
                return $"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}{newQuery}";
            }

            return url;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to strip authentication from URL, using as-is");
            return url;
        }
    }

    /// <summary>
    /// Normalizes a GUID string to a consistent format for comparison.
    /// </summary>
    private string? NormalizeGuid(string input)
    {
        if (string.IsNullOrEmpty(input))
        {
            return null;
        }

        // Try to parse as GUID (handles both with and without dashes)
        if (Guid.TryParse(input, out var guid))
        {
            return guid.ToString("N"); // Always return format without dashes
        }

        return null;
    }

    /// <summary>
    /// Fetches and rewrites an HLS manifest to use proxy URLs.
    /// </summary>
    /// <param name="itemId">The item ID.</param>
    /// <param name="baseProxyUrl">The base proxy URL (e.g., https://jellyfin-server/Plugins/SRFPlay/Proxy/{itemId}).</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The rewritten manifest content.</returns>
    public async Task<string?> GetRewrittenManifestAsync(
        string itemId,
        string baseProxyUrl,
        CancellationToken cancellationToken = default)
    {
        var authenticatedUrl = GetAuthenticatedUrl(itemId);
        if (authenticatedUrl == null)
        {
            return null;
        }

        try
        {
            _logger.LogDebug("Fetching manifest from: {Url}", authenticatedUrl);
            var manifestContent = await _httpClient.GetStringAsync(authenticatedUrl, cancellationToken).ConfigureAwait(false);

            // Rewrite the manifest to replace Akamai URLs with proxy URLs
            var rewrittenContent = RewriteManifestUrls(manifestContent, authenticatedUrl, baseProxyUrl);

            _logger.LogDebug("Successfully rewrote manifest for item {ItemId}", itemId);
            return rewrittenContent;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to fetch manifest for item {ItemId} from {Url}", itemId, authenticatedUrl);
            return null;
        }
    }

    /// <summary>
    /// Fetches a segment from the original source.
    /// </summary>
    /// <param name="itemId">The item ID.</param>
    /// <param name="segmentPath">The segment path.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The segment content as bytes.</returns>
    public async Task<byte[]?> GetSegmentAsync(
        string itemId,
        string segmentPath,
        CancellationToken cancellationToken = default)
    {
        var authenticatedUrl = GetAuthenticatedUrl(itemId);
        if (authenticatedUrl == null)
        {
            return null;
        }

        try
        {
            // Build the full segment URL by combining the base URL with the segment path
            var baseUri = new Uri(authenticatedUrl);
            var baseUrl = $"{baseUri.Scheme}://{baseUri.Host}{string.Join('/', baseUri.AbsolutePath.Split('/')[..^1])}";

            // Extract query parameters (auth tokens) from authenticated URL
            var queryParams = baseUri.Query;

            // Build full segment URL
            var segmentUrl = $"{baseUrl}/{segmentPath}{queryParams}";

            _logger.LogDebug("Fetching segment: {SegmentUrl}", segmentUrl);
            var segmentData = await _httpClient.GetByteArrayAsync(segmentUrl, cancellationToken).ConfigureAwait(false);

            _logger.LogDebug("Successfully fetched segment {SegmentPath} ({Size} bytes)", segmentPath, segmentData.Length);
            return segmentData;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to fetch segment {SegmentPath} for item {ItemId}", segmentPath, itemId);
            return null;
        }
    }

    /// <summary>
    /// Rewrites URLs in HLS manifest to point to proxy.
    /// </summary>
    /// <param name="manifestContent">The original manifest content.</param>
    /// <param name="originalBaseUrl">The original base URL.</param>
    /// <param name="proxyBaseUrl">The proxy base URL.</param>
    /// <returns>The rewritten manifest.</returns>
    private string RewriteManifestUrls(string manifestContent, string originalBaseUrl, string proxyBaseUrl)
    {
        var baseUri = new Uri(originalBaseUrl);
        var baseUrl = $"{baseUri.Scheme}://{baseUri.Host}{string.Join('/', baseUri.AbsolutePath.Split('/')[..^1])}";

        // Extract itemId query parameter from proxyBaseUrl to propagate it
        var itemIdParam = string.Empty;
        var queryMarker = "?itemId=";
        if (proxyBaseUrl.Contains(queryMarker, StringComparison.Ordinal))
        {
            var queryStart = proxyBaseUrl.IndexOf(queryMarker, StringComparison.Ordinal);
            if (queryStart >= 0)
            {
                itemIdParam = proxyBaseUrl[queryStart..];
                proxyBaseUrl = proxyBaseUrl[..queryStart]; // Remove query from base URL
            }
        }

        // Pattern to match .m3u8 and .ts/.mp4 segment references
        var pattern = @"(?:^|\n)([^#\n][^\n]*\.(?:m3u8|ts|mp4|m4s|aac)[^\n]*)";

        var rewritten = Regex.Replace(manifestContent, pattern, match =>
        {
            var url = match.Groups[1].Value.Trim();

            // Skip if it's already an absolute URL
            if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
                url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
            {
                // Rewrite absolute URLs to proxy
                var relativePath = url.Replace(baseUrl + "/", string.Empty, StringComparison.Ordinal);
                return $"\n{proxyBaseUrl}/{relativePath}{itemIdParam}";
            }

            // Relative URL - rewrite to proxy
            return $"\n{proxyBaseUrl}/{url}{itemIdParam}";
        });

        return rewritten;
    }

    /// <summary>
    /// Extracts the token expiry time from a stream URL with hdnts parameter.
    /// </summary>
    /// <param name="url">The authenticated stream URL.</param>
    /// <returns>The expiry time, or null if not found.</returns>
    private DateTime? ExtractTokenExpiry(string url)
    {
        try
        {
            var uri = new Uri(url);
            var query = uri.Query;

            // Parse the hdnts parameter (e.g., "exp=1763282021")
            var match = Regex.Match(query, @"exp=(\d+)");
            if (match.Success && long.TryParse(match.Groups[1].Value, out var unixTimestamp))
            {
                // Convert Unix timestamp to DateTime
                var expiry = DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime;
                _logger.LogDebug("Extracted token expiry from URL: {Expiry} UTC (unix: {Unix})", expiry, unixTimestamp);
                return expiry;
            }

            _logger.LogDebug("No token expiry found in URL: {Url}", url);
            return null;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to extract token expiry from URL: {Url}", url);
            return null;
        }
    }

    /// <summary>
    /// Cleans up old and expired stream mappings.
    /// </summary>
    public void CleanupOldMappings()
    {
        var cutoff = DateTime.UtcNow.AddHours(-24);
        var now = DateTime.UtcNow;
        var keysToRemove = new System.Collections.Generic.List<string>();

        foreach (var kvp in _streamMappings)
        {
            var shouldRemove = false;

            // Remove if registered more than 24 hours ago
            if (kvp.Value.RegisteredAt < cutoff)
            {
                shouldRemove = true;
                _logger.LogDebug("Marking item {ItemId} for cleanup (old registration)", kvp.Key);
            }

            // Remove if token has expired
            if (kvp.Value.TokenExpiresAt.HasValue && kvp.Value.TokenExpiresAt.Value <= now)
            {
                shouldRemove = true;
                _logger.LogDebug("Marking item {ItemId} for cleanup (expired token)", kvp.Key);
            }

            if (shouldRemove)
            {
                keysToRemove.Add(kvp.Key);
            }
        }

        foreach (var key in keysToRemove)
        {
            _streamMappings.TryRemove(key, out _);
        }

        if (keysToRemove.Count > 0)
        {
            _logger.LogInformation("Cleaned up {Count} old/expired stream mappings", keysToRemove.Count);
        }
    }

    /// <summary>
    /// Disposes the service.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Disposes the service.
    /// </summary>
    /// <param name="disposing">True if disposing.</param>
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        if (disposing)
        {
            _httpClient?.Dispose();
        }

        _disposed = true;
    }

    /// <summary>
    /// Stream information.
    /// </summary>
    private sealed class StreamInfo
    {
        public string AuthenticatedUrl { get; set; } = string.Empty;

        public string UnauthenticatedUrl { get; set; } = string.Empty;

        public DateTime RegisteredAt { get; set; }

        public DateTime? TokenExpiresAt { get; set; }
    }
}
