using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions;
using YMhut.Box.Core.Net;

namespace YMhut.Box.Core.Media;

public interface IRemoteMediaResolver
{
    Task<RemoteMediaResolution> ResolveMediaAsync(
        string apiUrl,
        RemoteMediaKind expectedKind = RemoteMediaKind.Unknown,
        bool cacheBust = true,
        IProgress<double>? progress = null,
        CancellationToken cancellationToken = default);

    Task<Uri> ResolveMediaUriAsync(string apiUrl, bool cacheBust = true, IProgress<double>? progress = null, CancellationToken cancellationToken = default);

    Task<byte[]> DownloadBytesAsync(Uri uri, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
}

public sealed record RemoteMediaResolution(
    Uri Uri,
    string ContentType,
    long? ContentLength,
    bool IsDirectMedia,
    string SuggestedExtension);

public sealed class RemoteMediaResolver : IRemoteMediaResolver
{
    private const int MaxMediaRedirects = 8;
    private const long MaxTextProbeLength = 2 * 1024 * 1024;
    private static readonly Regex AbsoluteUrlRegex = new(@"https?://[^\s""'<>\\]+", RegexOptions.IgnoreCase | RegexOptions.Compiled);

    private readonly Func<HttpMessageHandler>? _handlerFactory;

    public RemoteMediaResolver(Func<HttpMessageHandler>? handlerFactory = null)
    {
        _handlerFactory = handlerFactory;
    }

    public async Task<RemoteMediaResolution> ResolveMediaAsync(
        string apiUrl,
        RemoteMediaKind expectedKind = RemoteMediaKind.Unknown,
        bool cacheBust = true,
        IProgress<double>? progress = null,
        CancellationToken cancellationToken = default)
    {
        if (!Uri.TryCreate(cacheBust ? AddCacheBuster(apiUrl) : apiUrl, UriKind.Absolute, out var uri))
        {
            throw new InvalidOperationException("The remote media source URL is invalid.");
        }

        return await ResolveMediaAsync(uri, expectedKind, progress, cancellationToken).ConfigureAwait(false);
    }

    public async Task<Uri> ResolveMediaUriAsync(string apiUrl, bool cacheBust = true, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
    {
        var resolution = await ResolveMediaAsync(apiUrl, RemoteMediaKind.Unknown, cacheBust, progress, cancellationToken).ConfigureAwait(false);
        return resolution.Uri;
    }

    public async Task<byte[]> DownloadBytesAsync(Uri uri, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
    {
        using var client = CreateMediaHttpClient();
        using var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
        response.EnsureSuccessStatusCode();
        var total = response.Content.Headers.ContentLength;
        await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
        using var buffer = new MemoryStream();
        var bytes = new byte[64 * 1024];
        long received = 0;
        int read;
        while ((read = await stream.ReadAsync(bytes.AsMemory(0, bytes.Length), cancellationToken).ConfigureAwait(false)) > 0)
        {
            buffer.Write(bytes, 0, read);
            received += read;
            if (total is > 0)
            {
                progress?.Report(45 + Math.Min(50, received * 50d / total.Value));
            }
        }

        progress?.Report(98);
        return buffer.ToArray();
    }

    private async Task<RemoteMediaResolution> ResolveMediaAsync(
        Uri uri,
        RemoteMediaKind expectedKind,
        IProgress<double>? progress,
        CancellationToken cancellationToken)
    {
        var current = uri;
        RemoteMediaResolution? lastResolution = null;

        for (var depth = 0; depth < MaxMediaRedirects; depth++)
        {
            progress?.Report(Math.Min(30, 6 + depth * 4));
            try
            {
                using var redirectClient = CreateRedirectHttpClient();
                current = await HttpRedirectResolver.ResolveAsync(current, redirectClient, MaxMediaRedirects, cancellationToken).ConfigureAwait(false);

                using var probeClient = CreateMediaHttpClient(timeout: TimeSpan.FromSeconds(20));
                using var response = await probeClient.GetAsync(current, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                response.EnsureSuccessStatusCode();

                var finalUri = response.RequestMessage?.RequestUri ?? current;
                var contentType = NormalizeContentType(response.Content.Headers.ContentType?.MediaType);
                var length = response.Content.Headers.ContentLength;
                var direct = IsDirectMedia(finalUri, contentType, length, expectedKind);
                var suggestedExtension = SuggestedExtension(finalUri, contentType, expectedKind);
                lastResolution = new RemoteMediaResolution(finalUri, contentType, length, direct, suggestedExtension);
                if (direct)
                {
                    return lastResolution;
                }

                if (!CanReadAsText(contentType, length))
                {
                    return lastResolution;
                }

                var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
                if (TryExtractMediaUri(text, finalUri, expectedKind, out var next) && !UriEquals(finalUri, next))
                {
                    current = next;
                    continue;
                }

                return lastResolution;
            }
            catch when (!cancellationToken.IsCancellationRequested)
            {
                return lastResolution ?? FromUriOnly(current, expectedKind);
            }
        }

        return lastResolution ?? FromUriOnly(current, expectedKind);
    }

    private HttpClient CreateRedirectHttpClient()
    {
        if (_handlerFactory is not null)
        {
            return CreateMediaHttpClient(_handlerFactory(), TimeSpan.FromSeconds(20));
        }

        return CreateMediaHttpClient(new HttpClientHandler { AllowAutoRedirect = false }, TimeSpan.FromSeconds(20));
    }

    private HttpClient CreateMediaHttpClient(HttpMessageHandler? handler = null, TimeSpan? timeout = null)
    {
        var effectiveHandler = handler ?? _handlerFactory?.Invoke();
        var client = effectiveHandler is null
            ? new HttpClient()
            : new HttpClient(effectiveHandler, disposeHandler: true);
        client.Timeout = timeout ?? TimeSpan.FromMinutes(5);
        AddHeader(client.DefaultRequestHeaders.UserAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36");
        AddHeader(client.DefaultRequestHeaders.UserAgent, "YMhutBox/2.0");
        client.DefaultRequestHeaders.Accept.ParseAdd("image/*, video/*, audio/*, application/json, text/plain, text/html, application/octet-stream, */*");
        client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("zh-CN,zh;q=0.9,en;q=0.8");
        return client;
    }

    private static void AddHeader(HttpHeaderValueCollection<ProductInfoHeaderValue> headers, string value)
    {
        if (!headers.Any(header => string.Equals(header.ToString(), value, StringComparison.OrdinalIgnoreCase)))
        {
            headers.ParseAdd(value);
        }
    }

    private static string AddCacheBuster(string url)
    {
        if (string.IsNullOrWhiteSpace(url))
        {
            return url;
        }

        var separator = url.Contains('?', StringComparison.Ordinal) ? '&' : '?';
        return $"{url}{separator}_={DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
    }

    private static RemoteMediaResolution FromUriOnly(Uri uri, RemoteMediaKind expectedKind)
    {
        var extension = SuggestedExtension(uri, string.Empty, expectedKind);
        return new RemoteMediaResolution(
            uri,
            string.Empty,
            null,
            LooksLikeDirectMediaUri(uri, expectedKind),
            extension);
    }

    private static bool IsDirectMedia(Uri uri, string contentType, long? length, RemoteMediaKind expectedKind)
    {
        if (IsMediaContentType(contentType) || LooksLikeDirectMediaUri(uri, expectedKind))
        {
            return true;
        }

        if (contentType.Equals("application/octet-stream", StringComparison.OrdinalIgnoreCase))
        {
            return expectedKind != RemoteMediaKind.Unknown || KnownMediaExtension(Path.GetExtension(uri.AbsolutePath));
        }

        return expectedKind != RemoteMediaKind.Unknown &&
            string.IsNullOrWhiteSpace(contentType) &&
            length is > MaxTextProbeLength;
    }

    private static bool LooksLikeDirectMediaUri(Uri uri, RemoteMediaKind expectedKind)
    {
        var extension = Path.GetExtension(uri.AbsolutePath).TrimStart('.').ToLowerInvariant();
        if (string.IsNullOrWhiteSpace(extension))
        {
            return false;
        }

        if (expectedKind == RemoteMediaKind.Image && IsImageExtension(extension))
        {
            return true;
        }

        if (expectedKind == RemoteMediaKind.Video && IsVideoExtension(extension))
        {
            return true;
        }

        if (expectedKind == RemoteMediaKind.Audio && IsAudioExtension(extension))
        {
            return true;
        }

        return expectedKind == RemoteMediaKind.Unknown && KnownMediaExtension(extension);
    }

    private static bool KnownMediaExtension(string extension)
    {
        var normalized = extension.TrimStart('.').ToLowerInvariant();
        return IsVideoExtension(normalized) || IsAudioExtension(normalized) || IsImageExtension(normalized);
    }

    private static bool IsVideoExtension(string extension)
        => extension is "mp4" or "mkv" or "webm" or "avi" or "mov" or "wmv" or "m4v";

    private static bool IsAudioExtension(string extension)
        => extension is "mp3" or "wav" or "flac" or "aac" or "m4a" or "ogg" or "wma";

    private static bool IsImageExtension(string extension)
        => extension is "png" or "jpg" or "jpeg" or "bmp" or "gif" or "webp" or "tif" or "tiff";

    private static bool IsMediaContentType(string contentType)
    {
        return contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) ||
            contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase) ||
            contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase);
    }

    private static bool CanReadAsText(string contentType, long? length)
    {
        if (length is > MaxTextProbeLength)
        {
            return false;
        }

        return string.IsNullOrWhiteSpace(contentType) ||
            contentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) ||
            contentType.Contains("json", StringComparison.OrdinalIgnoreCase) ||
            contentType.Contains("xml", StringComparison.OrdinalIgnoreCase) ||
            contentType.Contains("javascript", StringComparison.OrdinalIgnoreCase);
    }

    internal static bool TryExtractMediaUri(string text, Uri baseUri, RemoteMediaKind expectedKind, out Uri uri)
    {
        if (TryExtractUriFromJson(text, baseUri, expectedKind, out uri))
        {
            return true;
        }

        var trimmed = (text ?? string.Empty).Trim();
        if (TryCreateCandidateUri(trimmed, baseUri, expectedKind, out uri))
        {
            return true;
        }

        Uri? best = null;
        var bestScore = int.MinValue;
        var index = 0;
        foreach (Match match in AbsoluteUrlRegex.Matches(text ?? string.Empty))
        {
            if (!TryCreateCandidateUri(match.Value.TrimEnd(',', ';', ')', ']', '}'), baseUri, expectedKind, out var candidate))
            {
                continue;
            }

            var score = ScoreUriCandidate(candidate, string.Empty, expectedKind, index++);
            if (score > bestScore)
            {
                best = candidate;
                bestScore = score;
            }
        }

        if (best is not null)
        {
            uri = best;
            return true;
        }

        uri = baseUri;
        return false;
    }

    private static bool TryExtractUriFromJson(string text, Uri baseUri, RemoteMediaKind expectedKind, out Uri uri)
    {
        uri = baseUri;
        if (string.IsNullOrWhiteSpace(text))
        {
            return false;
        }

        try
        {
            using var document = JsonDocument.Parse(text, new JsonDocumentOptions
            {
                AllowTrailingCommas = true,
                CommentHandling = JsonCommentHandling.Skip
            });
            return TryFindBestUri(document.RootElement, baseUri, expectedKind, out uri);
        }
        catch
        {
            return false;
        }
    }

    private static bool TryFindBestUri(JsonElement element, Uri baseUri, RemoteMediaKind expectedKind, out Uri uri)
    {
        var candidates = new List<(Uri Uri, string Field, int Order)>();
        CollectJsonUriCandidates(element, baseUri, candidates, string.Empty);
        var best = candidates
            .Select(candidate => (candidate.Uri, Score: ScoreUriCandidate(candidate.Uri, candidate.Field, expectedKind, candidate.Order)))
            .OrderByDescending(candidate => candidate.Score)
            .FirstOrDefault();

        if (best.Uri is not null)
        {
            uri = best.Uri;
            return true;
        }

        uri = baseUri;
        return false;
    }

    private static void CollectJsonUriCandidates(
        JsonElement element,
        Uri baseUri,
        List<(Uri Uri, string Field, int Order)> candidates,
        string fieldName)
    {
        if (element.ValueKind == JsonValueKind.String)
        {
            var value = element.GetString();
            if (TryCreateCandidateUri(value, baseUri, RemoteMediaKind.Unknown, out var candidate))
            {
                candidates.Add((candidate, fieldName, candidates.Count));
            }
            return;
        }

        if (element.ValueKind == JsonValueKind.Object)
        {
            foreach (var property in element.EnumerateObject())
            {
                CollectJsonUriCandidates(property.Value, baseUri, candidates, property.Name);
            }
        }
        else if (element.ValueKind == JsonValueKind.Array)
        {
            foreach (var item in element.EnumerateArray())
            {
                CollectJsonUriCandidates(item, baseUri, candidates, fieldName);
            }
        }
    }

    private static bool TryCreateCandidateUri(string? value, Uri baseUri, RemoteMediaKind expectedKind, out Uri uri)
    {
        uri = baseUri;
        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        var trimmed = value.Trim().Trim('"', '\'');
        if (!trimmed.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
            !trimmed.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
            !trimmed.StartsWith("//", StringComparison.Ordinal) &&
            !trimmed.StartsWith("/", StringComparison.Ordinal) &&
            !trimmed.StartsWith("./", StringComparison.Ordinal) &&
            !trimmed.StartsWith("../", StringComparison.Ordinal))
        {
            return false;
        }

        if (trimmed.StartsWith("//", StringComparison.Ordinal))
        {
            trimmed = $"{baseUri.Scheme}:{trimmed}";
        }

        if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var absoluteUri) &&
            !Uri.TryCreate(baseUri, trimmed, out absoluteUri))
        {
            return false;
        }

        uri = absoluteUri;
        return true;
    }

    private static int ScoreUriCandidate(Uri candidate, string fieldName, RemoteMediaKind expectedKind, int order)
    {
        var field = fieldName.ToLowerInvariant();
        var score = Math.Max(0, 1000 - order);
        if (field is "url" or "src" or "media" or "file" or "uri")
        {
            score += 500;
        }

        if (field is "video" or "mp4" or "image" or "img" or "audio")
        {
            score += 420;
        }

        if (expectedKind != RemoteMediaKind.Unknown && LooksLikeDirectMediaUri(candidate, expectedKind))
        {
            score += 900;
        }
        else if (LooksLikeDirectMediaUri(candidate, RemoteMediaKind.Unknown))
        {
            score += 240;
        }

        return score;
    }

    private static string SuggestedExtension(Uri uri, string contentType, RemoteMediaKind expectedKind)
    {
        var fromContentType = ExtensionFromContentType(contentType);
        if (!string.IsNullOrWhiteSpace(fromContentType))
        {
            return fromContentType;
        }

        var fromPath = Path.GetExtension(uri.AbsolutePath);
        if (!string.IsNullOrWhiteSpace(fromPath) && fromPath.Length <= 8)
        {
            return fromPath.StartsWith('.') ? fromPath : "." + fromPath;
        }

        return expectedKind switch
        {
            RemoteMediaKind.Video => ".mp4",
            RemoteMediaKind.Audio => ".mp3",
            RemoteMediaKind.Image => ".jpg",
            _ => ".bin"
        };
    }

    private static string ExtensionFromContentType(string contentType)
    {
        return NormalizeContentType(contentType) switch
        {
            "image/jpeg" => ".jpg",
            "image/png" => ".png",
            "image/webp" => ".webp",
            "image/gif" => ".gif",
            "image/bmp" => ".bmp",
            "video/mp4" => ".mp4",
            "video/webm" => ".webm",
            "video/quicktime" => ".mov",
            "audio/mpeg" => ".mp3",
            "audio/mp3" => ".mp3",
            "audio/wav" => ".wav",
            "audio/x-wav" => ".wav",
            "audio/mp4" => ".m4a",
            "audio/aac" => ".aac",
            "audio/ogg" => ".ogg",
            _ => string.Empty
        };
    }

    private static string NormalizeContentType(string? contentType)
        => (contentType ?? string.Empty).Trim().ToLowerInvariant();

    private static bool UriEquals(Uri left, Uri right)
        => string.Equals(left.AbsoluteUri, right.AbsoluteUri, StringComparison.OrdinalIgnoreCase);
}
