using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine.Networking;

namespace MajacloudSdk.Internal
{
    internal enum HttpAuth { ClientKey, Bearer, BearerIfAvailable }

    internal sealed class HttpRequest
    {
        public string Method = "GET";
        public string Path;
        public string Query;
        public string JsonBody;
        public HttpAuth Auth = HttpAuth.ClientKey;
        public bool Idempotent;
        public string IfNoneMatch;
        public float? TimeoutSeconds;
        public int? MaxRetries;
    }

    internal sealed class HttpResponse
    {
        public long Status;
        public string Text = string.Empty;
        public bool IsTransportError;
        public bool IsTimeout;
        public bool IsCancelled;
        public string TransportError;
        public string ETag;
        public string RequestId;
        public float? RetryAfterSeconds;
        public string ConfigVersionHeader;
        public MajacloudError SyntheticError;
        private JsonNode _json;
        private bool _parsed;

        public bool IsSuccess => SyntheticError == null && !IsTransportError && ((Status >= 200 && Status < 300) || Status == 304);

        public JsonNode Json
        {
            get
            {
                if (_parsed) return _json;
                _parsed = true;
                if (string.IsNullOrEmpty(Text)) return null;
                _json = JsonNode.TryParse(Text, out var node, out _) ? node : null;
                return _json;
            }
        }
    }

    /// <summary>
    /// UnityWebRequest wrapper: headers, timeouts, retries with backoff, one
    /// automatic token refresh on 401, structured error mapping.
    /// </summary>
    internal sealed class MajacloudHttp
    {
        private readonly MajacloudSettings _settings;
        private readonly string _deviceId;
        private readonly MajacloudLog _log;

        internal MajacloudAuth Auth { get; set; }

        public MajacloudHttp(MajacloudSettings settings, string deviceId, MajacloudLog log)
        {
            _settings = settings;
            _deviceId = deviceId;
            _log = log;
        }

        public async Task<HttpResponse> SendAsync(HttpRequest request, CancellationToken cancellationToken = default)
        {
            if (_settings == null)
            {
                return Synthetic(new MajacloudError(MajacloudErrorCode.NotConfigured, "Majacloud is not configured. Open Tools > Majacloud > Setup."));
            }
            if (!_settings.TryValidate(out var configError))
            {
                return Synthetic(new MajacloudError(MajacloudErrorCode.NotConfigured, configError ?? "Majacloud is not configured."));
            }
            string token = null;
            if (request.Auth != HttpAuth.ClientKey)
            {
                if (Auth == null) return Synthetic(new MajacloudError(MajacloudErrorCode.NotSignedIn, "No player session. Call Majacloud.Auth.SignInAnonymouslyAsync() first."));
                var tokenResult = await Auth.GetValidAccessTokenAsync(false, cancellationToken);
                if (tokenResult.Ok) token = tokenResult.Value;
                else if (request.Auth == HttpAuth.Bearer) return Synthetic(tokenResult.Error);
            }

            int maxRetries = request.MaxRetries ?? _settings.MaxRetries;
            bool refreshed = false;
            for (int attempt = 0; ; attempt++)
            {
                if (cancellationToken.IsCancellationRequested) return Cancelled();
                var response = await ExecuteAsync(request, token, cancellationToken);
                if (response.Status == 401 && token != null && !refreshed && Auth != null)
                {
                    refreshed = true;
                    var again = await Auth.GetValidAccessTokenAsync(true, cancellationToken);
                    if (again.Ok)
                    {
                        token = again.Value;
                        continue;
                    }
                    if (request.Auth == HttpAuth.Bearer) return Synthetic(again.Error);
                    return response;
                }
                if (ShouldRetry(request, response, attempt, maxRetries))
                {
                    var delay = BackoffSeconds(attempt, response.RetryAfterSeconds);
                    _log.Verbose("Retrying " + request.Method + " " + request.Path + " in " + delay.ToString("0.0", CultureInfo.InvariantCulture) + "s (attempt " + (attempt + 1) + ")");
                    try
                    {
                        await MajacloudTask.Delay(delay, cancellationToken);
                    }
                    catch (TaskCanceledException)
                    {
                        return Cancelled();
                    }
                    continue;
                }
                return response;
            }
        }

        /// <summary>Send and require an `{"ok":true}` JSON body (304 is passed through with an empty object).</summary>
        public async Task<MajacloudResult<JsonNode>> SendJsonAsync(HttpRequest request, CancellationToken cancellationToken = default)
        {
            var response = await SendAsync(request, cancellationToken);
            if (!response.IsSuccess) return MajacloudResult<JsonNode>.Fail(MapError(response));
            if (response.Status == 304) return MajacloudResult<JsonNode>.Success(JsonNode.Object());
            var json = response.Json;
            if (json == null || !json.IsObject) return MajacloudResult<JsonNode>.Fail(new MajacloudError(MajacloudErrorCode.ParseError, "The server returned an unreadable response.", (int)response.Status, "parse_error", response.RequestId));
            if (!json.GetBool("ok")) return MajacloudResult<JsonNode>.Fail(MapError(response));
            return MajacloudResult<JsonNode>.Success(json);
        }

        private async Task<HttpResponse> ExecuteAsync(HttpRequest request, string token, CancellationToken cancellationToken)
        {
            var url = _settings.BaseUrl + request.Path + (string.IsNullOrEmpty(request.Query) ? string.Empty : "?" + request.Query);
            var started = DateTime.UtcNow;
            var response = new HttpResponse();
            using (var www = new UnityWebRequest(url, request.Method))
            {
                www.downloadHandler = new DownloadHandlerBuffer();
                if (!string.IsNullOrEmpty(request.JsonBody))
                {
                    www.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(request.JsonBody)) { contentType = "application/json" };
                    www.SetRequestHeader("Content-Type", "application/json");
                }
                www.SetRequestHeader("Accept", "application/json");
                www.SetRequestHeader("X-Project-ID", _settings.ProjectId);
                www.SetRequestHeader("X-Client-Key", _settings.ClientKey);
                www.SetRequestHeader("X-Majacloud-SDK", MajacloudEndpoints.SdkHeaderValue);
                if (!string.IsNullOrEmpty(_deviceId)) www.SetRequestHeader("X-Device-ID", _deviceId);
                if (!string.IsNullOrEmpty(token)) www.SetRequestHeader("Authorization", "Bearer " + token);
                if (!string.IsNullOrEmpty(request.IfNoneMatch)) www.SetRequestHeader("If-None-Match", request.IfNoneMatch);
                www.timeout = (int)Math.Ceiling(request.TimeoutSeconds ?? _settings.RequestTimeoutSeconds);

                CancellationTokenRegistration registration = default;
                if (cancellationToken.CanBeCanceled)
                {
                    var captured = www;
                    registration = cancellationToken.Register(() =>
                    {
                        try { captured.Abort(); } catch (Exception) { }
                    });
                }
                try
                {
                    await www.SendWebRequest();
                }
                catch (Exception e)
                {
                    response.IsTransportError = true;
                    response.TransportError = e.Message;
                }
                finally
                {
                    registration.Dispose();
                }

                response.Status = www.responseCode;
                response.Text = www.downloadHandler != null ? www.downloadHandler.text ?? string.Empty : string.Empty;
                response.ETag = www.GetResponseHeader("ETag");
                response.RequestId = www.GetResponseHeader("X-Request-ID");
                response.ConfigVersionHeader = www.GetResponseHeader("X-Majacloud-Config-Version");
                response.RetryAfterSeconds = ParseRetryAfter(www.GetResponseHeader("Retry-After"));
                if (cancellationToken.IsCancellationRequested)
                {
                    response.IsCancelled = true;
                }
                else if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.DataProcessingError)
                {
                    response.IsTransportError = true;
                    response.TransportError = www.error;
                    response.IsTimeout = (www.error ?? string.Empty).IndexOf("timeout", StringComparison.OrdinalIgnoreCase) >= 0 || (www.error ?? string.Empty).IndexOf("timed out", StringComparison.OrdinalIgnoreCase) >= 0;
                }
            }
            if (_log.LogRequests)
            {
                var ms = (DateTime.UtcNow - started).TotalMilliseconds;
                _log.Info(request.Method + " " + request.Path + " -> " + (response.IsTransportError ? "transport error: " + response.TransportError : response.Status.ToString()) + " (" + ms.ToString("0", CultureInfo.InvariantCulture) + " ms" + (string.IsNullOrEmpty(response.RequestId) ? string.Empty : ", " + response.RequestId) + ")");
            }
            return response;
        }

        public static bool ShouldRetry(HttpRequest request, HttpResponse response, int attempt, int maxRetries)
        {
            if (attempt >= maxRetries || response.IsCancelled || response.SyntheticError != null) return false;
            if (response.IsTransportError) return request.Idempotent;
            if (response.Status == 429 || response.Status == 503) return true;
            if (response.Status >= 500) return request.Idempotent;
            return false;
        }

        public static float BackoffSeconds(int attempt, float? retryAfter)
        {
            if (retryAfter.HasValue && retryAfter.Value > 0f) return Math.Min(retryAfter.Value, 30f);
            var seconds = Math.Min(0.5f * (float)Math.Pow(2, attempt), 8f);
            return seconds + UnityEngine.Random.Range(0f, 0.25f);
        }

        public static MajacloudError MapError(HttpResponse response)
        {
            if (response.SyntheticError != null) return response.SyntheticError;
            if (response.IsCancelled) return new MajacloudError(MajacloudErrorCode.Cancelled, "The request was cancelled.");
            if (response.IsTransportError)
            {
                return response.IsTimeout
                    ? new MajacloudError(MajacloudErrorCode.Timeout, "The request timed out. Check your connection and try again.", 0, "timeout")
                    : new MajacloudError(MajacloudErrorCode.Network, "Could not reach the server: " + (response.TransportError ?? "network error"), 0, "network");
            }
            var json = response.Json;
            var error = json != null ? json["error"] : JsonNode.Null;
            var rawCode = error.GetString("code") ?? string.Empty;
            var message = error.GetString("message") ?? ("HTTP " + response.Status);
            var requestId = error.GetString("request_id") ?? response.RequestId;
            var code = MapCode(rawCode, (int)response.Status);
            return new MajacloudError(code, message, (int)response.Status, string.IsNullOrEmpty(rawCode) ? null : rawCode, requestId, response.RetryAfterSeconds, json);
        }

        private static MajacloudErrorCode MapCode(string raw, int status)
        {
            switch (raw)
            {
                case "validation": return MajacloudErrorCode.Validation;
                case "unauthorized": return MajacloudErrorCode.Unauthorized;
                case "forbidden": case "https_required": return MajacloudErrorCode.Forbidden;
                case "not_found": return MajacloudErrorCode.NotFound;
                case "conflict": return MajacloudErrorCode.Conflict;
                case "revision_mismatch": return MajacloudErrorCode.RevisionMismatch;
                case "rate_limited": return MajacloudErrorCode.RateLimited;
                case "quota_exceeded": return MajacloudErrorCode.QuotaExceeded;
                case "plan_limit": return MajacloudErrorCode.PlanLimit;
                case "server_error": case "payment_unavailable": return MajacloudErrorCode.ServerError;
                case "csrf": case "method_not_allowed": return MajacloudErrorCode.Validation;
            }
            switch (status)
            {
                case 400: case 422: return MajacloudErrorCode.Validation;
                case 401: return MajacloudErrorCode.Unauthorized;
                case 403: return MajacloudErrorCode.Forbidden;
                case 404: return MajacloudErrorCode.NotFound;
                case 409: return MajacloudErrorCode.Conflict;
                case 429: return MajacloudErrorCode.RateLimited;
            }
            return status >= 500 ? MajacloudErrorCode.ServerError : MajacloudErrorCode.Unknown;
        }

        private static float? ParseRetryAfter(string header)
        {
            if (string.IsNullOrEmpty(header)) return null;
            if (float.TryParse(header, NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds)) return Math.Max(0f, seconds);
            if (DateTime.TryParse(header, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var date))
            {
                return (float)Math.Max(0, (date - DateTime.UtcNow).TotalSeconds);
            }
            return null;
        }

        private static HttpResponse Synthetic(MajacloudError error) => new HttpResponse { SyntheticError = error };

        private static HttpResponse Cancelled() => new HttpResponse { IsCancelled = true, SyntheticError = new MajacloudError(MajacloudErrorCode.Cancelled, "The request was cancelled.") };

        public static string Query(params KeyValuePair<string, string>[] pairs)
        {
            var sb = new StringBuilder();
            foreach (var pair in pairs)
            {
                if (string.IsNullOrEmpty(pair.Value)) continue;
                if (sb.Length > 0) sb.Append('&');
                sb.Append(UnityWebRequest.EscapeURL(pair.Key)).Append('=').Append(UnityWebRequest.EscapeURL(pair.Value));
            }
            return sb.ToString();
        }
    }
}
