using System.Net;
using System.Text;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Settings;

namespace YMhut.Box.Core.Net;

public sealed record HttpServiceResult(
    HttpStatusCode StatusCode,
    string Content,
    IReadOnlyDictionary<string, string[]> Headers,
    TimeSpan Elapsed);

public sealed record HttpRequestPolicy(
    TimeSpan Timeout,
    int MaxRetries = 0,
    TimeSpan? RetryDelay = null)
{
    public static HttpRequestPolicy Default { get; } = new(TimeSpan.FromSeconds(30));

    public static HttpRequestPolicy RemoteTool { get; } = new(TimeSpan.FromSeconds(45), 1, TimeSpan.FromMilliseconds(650));

    public static HttpRequestPolicy Diagnostics { get; } = new(TimeSpan.FromSeconds(15));

    public static HttpRequestPolicy LongDownload { get; } = new(TimeSpan.FromMinutes(5));
}

public sealed class HttpRequestTimeoutException(Uri uri, TimeSpan timeout, Exception? innerException = null)
    : TimeoutException($"HTTP request timed out after {timeout.TotalSeconds:0.#} seconds.", innerException)
{
    public Uri Uri { get; } = uri;

    public TimeSpan Timeout { get; } = timeout;
}

public interface IHttpService
{
    Task<HttpServiceResult> GetAsync(Uri uri, CancellationToken cancellationToken = default);

    Task<string> GetStringAsync(Uri uri, CancellationToken cancellationToken = default);

    Task<HttpServiceResult> SendAsync(
        Uri uri,
        string method = "GET",
        string? body = null,
        IReadOnlyDictionary<string, string>? headers = null,
        bool ensureSuccess = true,
        HttpRequestPolicy? policy = null,
        CancellationToken cancellationToken = default);
}

public sealed class HttpService : IHttpService, IDisposable
{
    private readonly HttpClient _client;
    private readonly ILogService? _logService;
    private readonly ISettingsService? _settingsService;
    private readonly bool _disposeClient;

    public HttpService(HttpClient? client = null, ILogService? logService = null, ISettingsService? settingsService = null)
    {
        _client = client ?? new HttpClient();
        _client.Timeout = Timeout.InfiniteTimeSpan;
        ConfigureDefaultHeaders(_client);
        _logService = logService;
        _settingsService = settingsService;
        _disposeClient = client is null;
    }

    public async Task<HttpServiceResult> GetAsync(Uri uri, CancellationToken cancellationToken = default)
        => await SendAsync(uri, cancellationToken: cancellationToken).ConfigureAwait(false);

    public async Task<HttpServiceResult> SendAsync(
        Uri uri,
        string method = "GET",
        string? body = null,
        IReadOnlyDictionary<string, string>? headers = null,
        bool ensureSuccess = true,
        HttpRequestPolicy? policy = null,
        CancellationToken cancellationToken = default)
    {
        policy ??= HttpRequestPolicy.Default;
        var started = DateTimeOffset.UtcNow;
        using var proxyClient = CreateProxyClientIfNeeded();
        var client = proxyClient ?? _client;
        var normalizedMethod = string.IsNullOrWhiteSpace(method) ? "GET" : method.ToUpperInvariant();
        Exception? lastException = null;

        for (var attempt = 0; attempt <= Math.Max(0, policy.MaxRetries); attempt++)
        {
            using var timeoutCts = policy.Timeout == Timeout.InfiniteTimeSpan
                ? null
                : new CancellationTokenSource(policy.Timeout);
            using var linkedCts = timeoutCts is null
                ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
                : CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
            using var request = CreateRequest(uri, normalizedMethod, body, headers);

            try
            {
                using var response = await client.SendAsync(request, linkedCts.Token).ConfigureAwait(false);
                var content = await response.Content.ReadAsStringAsync(linkedCts.Token).ConfigureAwait(false);
                var elapsed = DateTimeOffset.UtcNow - started;
                var responseHeaders = response.Headers
                    .Concat(response.Content.Headers)
                    .GroupBy(header => header.Key, StringComparer.OrdinalIgnoreCase)
                    .ToDictionary(group => group.Key, group => group.SelectMany(header => header.Value).ToArray(), StringComparer.OrdinalIgnoreCase);

                await WriteLogAsync(
                    response.IsSuccessStatusCode ? "Information" : "Warning",
                    "http",
                    $"HTTP request completed: {(int)response.StatusCode} {response.ReasonPhrase}",
                    cancellationToken).ConfigureAwait(false);

                if (ensureSuccess)
                {
                    response.EnsureSuccessStatusCode();
                }

                return new HttpServiceResult(response.StatusCode, content, responseHeaders, elapsed);
            }
            catch (OperationCanceledException exception) when (!cancellationToken.IsCancellationRequested)
            {
                lastException = new HttpRequestTimeoutException(uri, policy.Timeout, exception);
                if (!ShouldRetry(normalizedMethod, lastException, attempt, policy))
                {
                    throw lastException;
                }
            }
            catch (Exception exception) when (ShouldRetry(normalizedMethod, exception, attempt, policy))
            {
                lastException = exception;
            }

            if (attempt < policy.MaxRetries)
            {
                await Task.Delay(policy.RetryDelay ?? TimeSpan.FromMilliseconds(500), cancellationToken).ConfigureAwait(false);
            }
        }

        throw lastException ?? new InvalidOperationException("HTTP request failed before a response was available.");
    }

    public async Task<string> GetStringAsync(Uri uri, CancellationToken cancellationToken = default)
        => (await GetAsync(uri, cancellationToken).ConfigureAwait(false)).Content;

    public void Dispose()
    {
        if (_disposeClient)
        {
            _client.Dispose();
        }
    }

    private Task WriteLogAsync(string level, string category, string message, CancellationToken cancellationToken)
    {
        return _logService?.WriteAsync(level, category, message, cancellationToken: cancellationToken) ?? Task.CompletedTask;
    }

    private HttpClient? CreateProxyClientIfNeeded()
    {
        var settings = _settingsService?.Current;
        if (settings is not { ProxyEnabled: true } ||
            string.Equals(settings.ProxyMode, "system", StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        if (string.Equals(settings.ProxyMode, "direct", StringComparison.OrdinalIgnoreCase))
        {
            var directClient = new HttpClient(new HttpClientHandler { UseProxy = false }, disposeHandler: true)
            {
                Timeout = Timeout.InfiniteTimeSpan
            };
            ConfigureDefaultHeaders(directClient);
            return directClient;
        }

        if (string.IsNullOrWhiteSpace(settings.ProxyHost))
        {
            return null;
        }

        var builder = UriBuilderCache.Create(settings.ProxyHost, settings.ProxyPort);
        var handler = new HttpClientHandler
        {
            Proxy = new WebProxy(builder.Uri),
            UseProxy = true
        };
        var client = new HttpClient(handler, disposeHandler: true)
        {
            Timeout = Timeout.InfiniteTimeSpan
        };
        ConfigureDefaultHeaders(client);
        return client;
    }

    private static HttpRequestMessage CreateRequest(
        Uri uri,
        string method,
        string? body,
        IReadOnlyDictionary<string, string>? headers)
    {
        var request = new HttpRequestMessage(new HttpMethod(method), uri);
        if (body is not null && request.Method != HttpMethod.Get && request.Method != HttpMethod.Head)
        {
            request.Content = new StringContent(body, Encoding.UTF8, "text/plain");
        }

        ApplyRequestHeaders(request);
        ApplyCustomHeaders(request, headers);
        return request;
    }

    private static bool ShouldRetry(string method, Exception exception, int attempt, HttpRequestPolicy policy)
    {
        if (attempt >= policy.MaxRetries || method is not ("GET" or "HEAD"))
        {
            return false;
        }

        return exception is HttpRequestTimeoutException ||
            exception is TimeoutException ||
            exception is IOException ||
            exception is HttpRequestException { StatusCode: null };
    }

    private static void ConfigureDefaultHeaders(HttpClient client)
    {
        if (!client.DefaultRequestHeaders.UserAgent.Any())
        {
            client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36");
            client.DefaultRequestHeaders.UserAgent.ParseAdd("YMhutBox/2.0");
        }

        if (!client.DefaultRequestHeaders.Accept.Any())
        {
            client.DefaultRequestHeaders.Accept.ParseAdd("application/json, text/plain, application/xml, text/xml, */*");
        }

        if (!client.DefaultRequestHeaders.AcceptLanguage.Any())
        {
            client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("zh-CN,zh;q=0.9,en;q=0.8");
        }
    }

    private static void ApplyRequestHeaders(HttpRequestMessage request)
    {
        if (request.RequestUri is null)
        {
            return;
        }

        var host = request.RequestUri.Host;
        request.Headers.TryAddWithoutValidation("Cache-Control", "no-cache");
        request.Headers.TryAddWithoutValidation("Pragma", "no-cache");
        request.Headers.TryAddWithoutValidation("Upgrade-Insecure-Requests", "1");

        if (host.Contains("12306.cn", StringComparison.OrdinalIgnoreCase))
        {
            request.Headers.Referrer = new Uri("https://kyfw.12306.cn/otn/leftTicket/init");
            request.Headers.TryAddWithoutValidation("X-Requested-With", "XMLHttpRequest");
        }

        if (host.Contains("zhihu.com", StringComparison.OrdinalIgnoreCase))
        {
            request.Headers.Referrer = new Uri("https://www.zhihu.com/hot");
            request.Headers.TryAddWithoutValidation("X-Requested-With", "fetch");
            request.Headers.TryAddWithoutValidation("Sec-Fetch-Site", "same-origin");
        }

        if (host.Contains("bilibili.com", StringComparison.OrdinalIgnoreCase))
        {
            request.Headers.Referrer = new Uri("https://www.bilibili.com/v/popular/all");
            request.Headers.TryAddWithoutValidation("Origin", "https://www.bilibili.com");
            request.Headers.TryAddWithoutValidation("X-Requested-With", "XMLHttpRequest");
            request.Headers.TryAddWithoutValidation("Sec-Fetch-Site", "same-site");
        }

        if (host.Contains("baidu.com", StringComparison.OrdinalIgnoreCase))
        {
            request.Headers.Referrer = new Uri("https://top.baidu.com/board?tab=realtime");
            request.Headers.TryAddWithoutValidation("Sec-Fetch-Site", "same-origin");
        }

        if (host.Contains("ndrc.gov.cn", StringComparison.OrdinalIgnoreCase))
        {
            request.Headers.Referrer = new Uri("https://www.ndrc.gov.cn/");
            request.Headers.TryAddWithoutValidation("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
        }

        if (host.Contains("cbooo.cn", StringComparison.OrdinalIgnoreCase))
        {
            request.Headers.Referrer = new Uri("https://www.cbooo.cn/");
            request.Headers.TryAddWithoutValidation("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
        }

        if (host.Contains("36kr.com", StringComparison.OrdinalIgnoreCase))
        {
            request.Headers.Referrer = new Uri("https://36kr.com/");
        }

        if (host.Contains("cctv.com", StringComparison.OrdinalIgnoreCase))
        {
            request.Headers.Referrer = new Uri("https://news.cctv.com/");
        }
    }

    private static void ApplyCustomHeaders(HttpRequestMessage request, IReadOnlyDictionary<string, string>? headers)
    {
        if (headers is null || headers.Count == 0)
        {
            return;
        }

        foreach (var (name, value) in headers)
        {
            if (string.IsNullOrWhiteSpace(name) || IsBlockedHeader(name))
            {
                continue;
            }

            if (request.Content is not null && request.Content.Headers.TryAddWithoutValidation(name, value))
            {
                continue;
            }

            request.Headers.TryAddWithoutValidation(name, value);
        }
    }

    private static bool IsBlockedHeader(string name)
    {
        return name.Equals("host", StringComparison.OrdinalIgnoreCase) ||
            name.Equals("content-length", StringComparison.OrdinalIgnoreCase) ||
            name.Equals("connection", StringComparison.OrdinalIgnoreCase) ||
            name.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase);
    }

    private static class UriBuilderCache
    {
        public static UriBuilder Create(string host, int port)
        {
            var normalized = host.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
                host.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
                ? host
                : "http://" + host;
            var builder = new UriBuilder(normalized);
            if (port > 0)
            {
                builder.Port = port;
            }

            return builder;
        }
    }
}
