using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
using Jellyfin.Plugin.SRFPlay.Utilities;
using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace Jellyfin.Plugin.SRFPlay.Controllers;

/// <summary>
/// Controller for proxying SRF Play streams.
/// </summary>
[ApiController]
[Route("Plugins/SRFPlay/Proxy")]
public class StreamProxyController : ControllerBase
{
    private readonly ILogger<StreamProxyController> _logger;
    private readonly IStreamProxyService _proxyService;
    private readonly IHttpClientFactory _httpClientFactory;

    /// <summary>
    /// Initializes a new instance of the <see cref="StreamProxyController"/> class.
    /// </summary>
    /// <param name="logger">The logger.</param>
    /// <param name="proxyService">The proxy service.</param>
    /// <param name="httpClientFactory">The HTTP client factory.</param>
    public StreamProxyController(
        ILogger<StreamProxyController> logger,
        IStreamProxyService proxyService,
        IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _proxyService = proxyService;
        _httpClientFactory = httpClientFactory;
    }

    /// <summary>
    /// Adds CORS headers to allow cross-origin requests from hls.js in browsers.
    /// </summary>
    private void AddCorsHeaders()
    {
        Response.Headers["Access-Control-Allow-Origin"] = "*";
        Response.Headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS";
        Response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Range, Accept, Origin";
        Response.Headers["Access-Control-Expose-Headers"] = "Content-Length, Content-Range, Accept-Ranges";
        Response.Headers["Access-Control-Max-Age"] = "86400"; // Cache preflight for 24 hours
    }

    /// <summary>
    /// Adds Cache-Control headers appropriate for the stream type.
    /// Livestreams need frequent manifest refresh, VOD can be cached longer.
    /// </summary>
    /// <param name="itemId">The item ID to check.</param>
    /// <param name="isVariantManifest">Whether this is a variant (sub) manifest with segment lists.</param>
    private void AddManifestCacheHeaders(string itemId, bool isVariantManifest = false)
    {
        var metadata = _proxyService.GetStreamMetadata(itemId);
        var isLiveStream = metadata?.IsLiveStream ?? false;

        if (isLiveStream)
        {
            if (isVariantManifest)
            {
                // Variant manifests contain the segment list which changes every target duration.
                // Use no-cache to ensure the player always gets the freshest segment list,
                // preventing requests for segments that have been rotated out of the sliding window.
                Response.Headers["Cache-Control"] = "no-cache, no-store";
            }
            else
            {
                // Master manifest is relatively stable (just lists quality variants)
                Response.Headers["Cache-Control"] = "max-age=2, must-revalidate";
            }

            _logger.LogDebug("Setting livestream cache headers for {ItemId} (variant={IsVariant})", itemId, isVariantManifest);
        }
        else
        {
            // VOD manifests are static, can cache longer
            Response.Headers["Cache-Control"] = "max-age=3600";
        }
    }

    /// <summary>
    /// Handles CORS preflight OPTIONS requests for all proxy endpoints.
    /// </summary>
    /// <returns>Empty response with CORS headers.</returns>
    [HttpOptions("{itemId}/master.m3u8")]
    [HttpOptions("{itemId}/{manifestPath}.m3u8")]
    [HttpOptions("{itemId}/{*segmentPath}")]
    [AllowAnonymous]
    public IActionResult HandleOptions()
    {
        AddCorsHeaders();
        return Ok();
    }

    /// <summary>
    /// Proxies HLS master manifest requests.
    /// </summary>
    /// <param name="itemId">The item ID from URL path.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The HLS manifest with rewritten URLs.</returns>
    [HttpGet("{itemId}/master.m3u8")]
    [AllowAnonymous] // Allow anonymous since Jellyfin handles auth upstream
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetMasterManifest(
        [FromRoute] string itemId,
        CancellationToken cancellationToken)
    {
        AddCorsHeaders();
        _logger.LogInformation("Proxy request for master manifest - Path ItemId: {PathItemId}, Query params: {QueryString}", itemId, Request.QueryString);

        // Try to resolve the actual item ID (path ID might be a session ID during transcoding)
        var actualItemId = ResolveItemId(itemId);

        try
        {
            // Get the correct scheme (https if configured, otherwise use request scheme)
            var scheme = GetProxyScheme();

            // Build the base proxy URL for this item
            // Preserve query parameters (token or itemId) from the original request
            string baseProxyUrl;
            if (Request.Query.TryGetValue("token", out var token) && !string.IsNullOrEmpty(token))
            {
                baseProxyUrl = $"{scheme}://{Request.Host}/Plugins/SRFPlay/Proxy/{itemId}?token={token}";
                _logger.LogDebug("Using token-based proxy URL with token: {Token}", token.ToString());
            }
            else if (actualItemId != itemId)
            {
                // Legacy: If path ID differs from resolved ID, add itemId query parameter
                baseProxyUrl = $"{scheme}://{Request.Host}/Plugins/SRFPlay/Proxy/{itemId}?itemId={actualItemId}";
                _logger.LogInformation("Path itemId {PathId} differs from resolved itemId {ResolvedId}, adding query parameter", itemId, actualItemId);
            }
            else
            {
                // Simple case: no query parameters needed
                baseProxyUrl = $"{scheme}://{Request.Host}/Plugins/SRFPlay/Proxy/{itemId}";
                _logger.LogDebug("Path itemId matches resolved itemId: {ItemId}", itemId);
            }

            var manifestContent = await _proxyService.GetRewrittenManifestAsync(actualItemId, baseProxyUrl, cancellationToken).ConfigureAwait(false);

            if (manifestContent == null)
            {
                _logger.LogWarning("Manifest not found for path itemId {PathItemId}, resolved itemId {ResolvedItemId} - stream may not be registered", itemId, actualItemId);
                return NotFound();
            }

            // Set cache headers based on stream type (live vs VOD)
            AddManifestCacheHeaders(actualItemId);

            _logger.LogDebug("Returning master manifest for item {ItemId} ({Length} bytes)", itemId, manifestContent.Length);
            // Use application/x-mpegURL for broader compatibility (Samsung AVPlay requires it)
            return Content(manifestContent, "application/x-mpegURL; charset=utf-8");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error proxying master manifest for item {ItemId}", itemId);
            return StatusCode(StatusCodes.Status500InternalServerError);
        }
    }

    /// <summary>
    /// Proxies HLS variant manifest requests.
    /// </summary>
    /// <param name="itemId">The item ID.</param>
    /// <param name="manifestPath">The manifest path (e.g., "index_0_av").</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The HLS manifest with rewritten segment URLs.</returns>
    [HttpGet("{itemId}/{manifestPath}.m3u8")]
    [AllowAnonymous]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetVariantManifest(
        [FromRoute] string itemId,
        [FromRoute] string manifestPath,
        CancellationToken cancellationToken)
    {
        AddCorsHeaders();
        var fullPath = $"{manifestPath}.m3u8";
        _logger.LogInformation("Proxy request for variant manifest - ItemId: {ItemId}, Path: {Path}", itemId, fullPath);

        // Try to resolve the actual item ID
        var actualItemId = ResolveItemId(itemId);

        try
        {
            // Fetch the variant manifest as a segment
            var queryString = Request.QueryString.HasValue ? Request.QueryString.Value : null;
            var manifestData = await _proxyService.GetSegmentAsync(actualItemId, fullPath, queryString, cancellationToken).ConfigureAwait(false);

            if (manifestData == null)
            {
                _logger.LogWarning("Variant manifest not found - ItemId: {ItemId}, Path: {Path}", itemId, fullPath);
                return NotFound();
            }

            // Convert to string and rewrite URLs
            var manifestContent = System.Text.Encoding.UTF8.GetString(manifestData);
            var scheme = GetProxyScheme();
            var baseProxyUrl = $"{scheme}://{Request.Host}/Plugins/SRFPlay/Proxy/{itemId}";

            // Extract query parameters from the current request to propagate them
            string queryParams;
            if (Request.Query.TryGetValue("token", out var tokenVal) && !string.IsNullOrEmpty(tokenVal))
            {
                queryParams = $"?token={tokenVal}";
            }
            else if (Request.Query.TryGetValue("itemId", out var itemIdVal) && !string.IsNullOrEmpty(itemIdVal))
            {
                queryParams = $"?itemId={itemIdVal}";
            }
            else
            {
                queryParams = string.Empty;
            }

            var isLiveStream = _proxyService.GetStreamMetadata(actualItemId)?.IsLiveStream ?? false;
            var rewrittenContent = _proxyService.RewriteVariantManifestUrls(manifestContent, baseProxyUrl, queryParams, isLiveStream);

            // Set cache headers based on stream type (live vs VOD)
            // Variant manifests use stricter no-cache for live streams
            AddManifestCacheHeaders(actualItemId, isVariantManifest: true);

            _logger.LogDebug("Returning variant manifest for item {ItemId} ({Length} bytes)", itemId, rewrittenContent.Length);
            return Content(rewrittenContent, "application/x-mpegURL; charset=utf-8");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error proxying variant manifest - ItemId: {ItemId}, Path: {Path}", itemId, fullPath);
            return StatusCode(StatusCodes.Status500InternalServerError);
        }
    }

    /// <summary>
    /// Proxies HLS segment requests (.ts, .mp4, .m4s, .aac files).
    /// </summary>
    /// <param name="itemId">The item ID.</param>
    /// <param name="segmentPath">The segment path.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The segment data.</returns>
    [HttpGet("{itemId}/{*segmentPath}")]
    [AllowAnonymous]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetSegment(
        [FromRoute] string itemId,
        [FromRoute] string segmentPath,
        CancellationToken cancellationToken)
    {
        AddCorsHeaders();
        _logger.LogDebug("Proxy request for segment - ItemId: {ItemId}, Path: {SegmentPath}", itemId, segmentPath);

        // Try to resolve the actual item ID
        var actualItemId = ResolveItemId(itemId);

        try
        {
            // Pass the original query string to preserve segment-specific parameters (e.g., ?m=timestamp)
            var queryString = Request.QueryString.HasValue ? Request.QueryString.Value : null;

            // Use streaming proxy: starts forwarding data to the client before the full segment
            // is downloaded from the CDN, reducing time-to-first-byte for live streams
            using var upstreamResponse = await _proxyService.GetSegmentStreamAsync(actualItemId, segmentPath, queryString, cancellationToken).ConfigureAwait(false);

            if (upstreamResponse == null)
            {
                _logger.LogWarning("Segment not found - ItemId: {ItemId}, Path: {SegmentPath}", itemId, segmentPath);
                return NotFound();
            }

            // Determine content type based on file extension
            var contentType = MimeTypeHelper.GetSegmentContentType(segmentPath);
            Response.ContentType = contentType;

            if (upstreamResponse.Content.Headers.ContentLength.HasValue)
            {
                Response.ContentLength = upstreamResponse.Content.Headers.ContentLength.Value;
            }

            // Stream directly from CDN to client without buffering the entire segment in memory
            await upstreamResponse.Content.CopyToAsync(Response.Body, cancellationToken).ConfigureAwait(false);

            _logger.LogDebug("Streamed segment {SegmentPath} ({ContentType})", segmentPath, contentType);
            return new EmptyResult();
        }
        catch (OperationCanceledException)
        {
            // Client disconnected during streaming (e.g., FFmpeg stopped, player seeked).
            // This is expected behavior, not an error.
            _logger.LogDebug("Segment streaming canceled - ItemId: {ItemId}, Path: {SegmentPath}", itemId, segmentPath);
            return new EmptyResult();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error proxying segment - ItemId: {ItemId}, Path: {SegmentPath}", itemId, segmentPath);

            // If we already started streaming data to the client, we can't change the status code
            if (Response.HasStarted)
            {
                return new EmptyResult();
            }

            return StatusCode(StatusCodes.Status500InternalServerError);
        }
    }

    /// <summary>
    /// Gets the correct scheme for proxy URLs (https if public URL is configured with https).
    /// </summary>
    /// <returns>The scheme to use (http or https).</returns>
    private string GetProxyScheme()
    {
        // Check if PublicServerUrl is configured and uses HTTPS
        var config = Plugin.Instance?.Configuration;
        if (config != null && !string.IsNullOrWhiteSpace(config.PublicServerUrl))
        {
            if (config.PublicServerUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
            {
                return "https";
            }
        }

        // Fall back to request scheme, but prefer https if forwarded headers indicate it
        if (Request.Headers.TryGetValue("X-Forwarded-Proto", out var forwardedProto))
        {
            return forwardedProto.ToString().ToLowerInvariant();
        }

        return Request.Scheme;
    }

    /// <summary>
    /// Resolves the actual item ID from the request.
    /// </summary>
    /// <param name="pathItemId">The item ID from the URL path.</param>
    /// <returns>The resolved item ID.</returns>
    private string ResolveItemId(string pathItemId)
    {
        // Check for token parameter first (preferred method)
        if (Request.Query.TryGetValue("token", out var token) && !string.IsNullOrEmpty(token))
        {
            // Try to resolve the original item ID from the token via the proxy service
            // We'll need to add a method to StreamProxyService to look up by token
            _logger.LogInformation("Found token parameter: {Token}, will use path ID {PathItemId} for lookup", token.ToString(), pathItemId);
            return pathItemId; // Use path ID for now; token prevents Jellyfin from rewriting the URL
        }

        // Check if there's an itemId query parameter (legacy fallback)
        if (Request.Query.TryGetValue("itemId", out var queryItemId) && !string.IsNullOrEmpty(queryItemId))
        {
            _logger.LogInformation("Using itemId from query parameter: {QueryItemId} (path had: {PathItemId})", queryItemId.ToString(), pathItemId);
            return queryItemId.ToString();
        }

        // No query parameters - use path ID as-is (normal case for segments and transcoding sessions)
        _logger.LogDebug("No query parameters, using path ID as-is: {PathItemId}", pathItemId);
        return pathItemId;
    }

    /// <summary>
    /// Proxies image requests from SRF CDN, fixing Content-Type headers.
    /// </summary>
    /// <param name="url">The original image URL (base64 encoded).</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The image data with correct Content-Type.</returns>
    [HttpGet("Image")]
    [AllowAnonymous]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetImage(
        [FromQuery] string url,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrEmpty(url))
        {
            return BadRequest("URL parameter is required");
        }

        string decodedUrl;
        try
        {
            // Decode base64 URL
            var bytes = Convert.FromBase64String(url);
            decodedUrl = System.Text.Encoding.UTF8.GetString(bytes);
        }
        catch (FormatException)
        {
            _logger.LogWarning("Invalid base64 URL parameter: {Url}", url);
            return BadRequest("Invalid URL encoding");
        }

        _logger.LogDebug("Proxying image from: {Url}", decodedUrl);

        try
        {
            var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);

            // Create request with proper headers
            var request = new HttpRequestMessage(HttpMethod.Get, new Uri(decodedUrl));
            request.Headers.UserAgent.ParseAdd(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
            request.Headers.Accept.ParseAdd("image/jpeg, image/png, image/webp, image/*;q=0.8");

            var response = await httpClient.SendAsync(
                request,
                HttpCompletionOption.ResponseHeadersRead,
                cancellationToken).ConfigureAwait(false);

            if (!response.IsSuccessStatusCode)
            {
                _logger.LogWarning(
                    "Image fetch failed with status {StatusCode} for {Url}",
                    response.StatusCode,
                    decodedUrl);
                return NotFound();
            }

            var imageData = await response.Content.ReadAsByteArrayAsync(cancellationToken)
                .ConfigureAwait(false);

            // Determine correct content type
            var contentType = MimeTypeHelper.GetImageContentType(decodedUrl);

            _logger.LogDebug(
                "Returning proxied image ({Length} bytes, {ContentType})",
                imageData.Length,
                contentType);

            return File(imageData, contentType);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Error fetching image from {Url}", decodedUrl);
            return StatusCode(StatusCodes.Status502BadGateway);
        }
    }

    /// <summary>
    /// Generates a placeholder image with the given text centered.
    /// </summary>
    /// <param name="text">The text to display (base64 encoded).</param>
    /// <returns>A PNG image with the text centered.</returns>
    [HttpGet("Placeholder")]
    [AllowAnonymous]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public IActionResult GetPlaceholder([FromQuery] string text)
    {
        if (string.IsNullOrEmpty(text))
        {
            return BadRequest("Text parameter is required");
        }

        string decodedText;
        try
        {
            var bytes = Convert.FromBase64String(text);
            decodedText = System.Text.Encoding.UTF8.GetString(bytes);
        }
        catch (FormatException)
        {
            _logger.LogWarning("Invalid base64 text parameter: {Text}", text);
            return BadRequest("Invalid text encoding");
        }

        _logger.LogDebug("Generating placeholder image for: {Text}", decodedText);

        var imageStream = PlaceholderImageGenerator.GeneratePlaceholder(decodedText);
        return File(imageStream, "image/png");
    }
}
