#nullable enable
using System;
using System.Globalization;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using IGP.UnitySDK.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace IGP.UnitySDK
{
    public partial class IGPRuntimeManager
    {
        private static readonly TimeSpan DefaultDirectRequestTimeout = TimeSpan.FromSeconds(15);

        private readonly object directSessionSync = new object();
        private readonly SemaphoreSlim directSessionGate = new SemaphoreSlim(1, 1);
        private readonly SemaphoreSlim directRefreshGate = new SemaphoreSlim(1, 1);
        private IIGPDirectHttpTransport? directHttpTransport;
        private TimeSpan directRequestTimeout = DefaultDirectRequestTimeout;
        private string directApiBaseUrl = string.Empty;
        private string directAccessToken = string.Empty;
        private string directRefreshToken = string.Empty;
        private IGPUserProfile? directUserProfile;

        public bool HasUserSession
        {
            get
            {
                lock (directSessionSync)
                {
                    return !string.IsNullOrWhiteSpace(directAccessToken) && directUserProfile != null;
                }
            }
        }

        /// <summary>
        /// Sends a Curio phone challenge for sign-in or automatic account creation.
        /// This Direct capability does not require Desktop attach or InitializeAsync().
        /// </summary>
        public async Task<IGPPhoneCodeSendResult> SendPhoneSignInCodeAsync(
            string phone,
            string? captchaVerifyParam = null,
            CancellationToken cancellationToken = default)
        {
            EnsureDirectCapabilityAvailable();
            var normalizedPhone = NormalizePhone(phone);
            var body = new JObject
            {
                ["phone"] = normalizedPhone,
                ["usage"] = "signin",
            };
            if (!string.IsNullOrWhiteSpace(captchaVerifyParam))
            {
                body["captchaVerifyParam"] = captchaVerifyParam!.Trim();
            }

            var root = await SendDirectObjectAsync(
                HttpMethod.Post,
                "/auth/signin/phone/sms/send",
                body,
                authenticated: false,
                cancellationToken).ConfigureAwait(false);
            return new IGPPhoneCodeSendResult
            {
                Succeeded = root.Value<bool?>("ok") ?? false,
                DevelopmentCode = root.Value<string>("code") ?? string.Empty,
                IsTestAccount = root.Value<bool?>("testAccount") ?? false,
            };
        }

        /// <summary>
        /// Signs in with a phone challenge. Curio creates the user when no account exists.
        /// Credentials remain internal to Core.
        /// </summary>
        public async Task<IGPUserSession> SignInWithPhoneCodeAsync(
            string phone,
            string verificationCode,
            string? inviteCode = null,
            CancellationToken cancellationToken = default)
        {
            EnsureDirectCapabilityAvailable();
            var normalizedPhone = NormalizePhone(phone);
            if (!IsSixDigitCode(verificationCode))
            {
                throw new ArgumentException("Verification code must contain exactly six digits", nameof(verificationCode));
            }

            await directSessionGate.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                var body = new JObject
                {
                    ["phone"] = normalizedPhone,
                    ["smsCode"] = verificationCode,
                    ["clientKind"] = "unity-sdk",
                    ["clientVersion"] = IGPSdkVersion.PackageVersion,
                };
                if (!string.IsNullOrWhiteSpace(inviteCode))
                {
                    body["inviteCode"] = inviteCode!.Trim();
                }

                var root = await SendDirectObjectAsync(
                    HttpMethod.Post,
                    "/auth/signin/phone/sms",
                    body,
                    authenticated: false,
                    cancellationToken).ConfigureAwait(false);
                var result = ParseAuthenticationResponse(root);
                EnsureIdentityCompatibleWithDesktop(result.Profile.Id);
                ApplyDirectAuthentication(result);
                return new IGPUserSession(result.Profile);
            }
            finally
            {
                directSessionGate.Release();
            }
        }

        /// <summary>
        /// Explicitly refreshes the in-memory Direct user session.
        /// </summary>
        public async Task<IGPUserSession> RefreshUserSessionAsync(
            CancellationToken cancellationToken = default)
        {
            EnsureDirectCapabilityAvailable();
            await directSessionGate.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                EnsureDirectUserSession();
                try
                {
                    await RefreshDirectSessionCoreAsync(null, cancellationToken).ConfigureAwait(false);
                    var profile = GetDirectProfileSnapshot()
                        ?? throw new IGPSDKException(
                            "Direct user profile is unavailable",
                            IGPErrorCodes.ERR_DIRECT_SESSION_REQUIRED);
                    return new IGPUserSession(profile);
                }
                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
                {
                    throw;
                }
                catch
                {
                    ClearDirectSession();
                    throw;
                }
            }
            finally
            {
                directSessionGate.Release();
            }
        }

        /// <summary>
        /// Clears only the in-memory Direct user session. Desktop login is unchanged.
        /// </summary>
        public void SignOut()
        {
            ClearDirectSession();
        }

        private async Task<JToken> SendDirectJsonAsync(
            HttpMethod method,
            string path,
            JToken? body,
            bool authenticated,
            CancellationToken cancellationToken)
        {
            EnsureDirectCapabilityAvailable();
            if (authenticated)
            {
                EnsureDirectUserSession();
            }

            var staleAccessToken = authenticated ? GetDirectAccessToken() : string.Empty;
            var retriedAfterRefresh = false;
            var response = await SendDirectRawAsync(
                method,
                BuildDirectApiUri(path),
                staleAccessToken,
                body,
                cancellationToken).ConfigureAwait(false);
            if (authenticated && response.StatusCode == IGPErrorCodes.HTTP_UNAUTHORIZED)
            {
                try
                {
                    await RefreshDirectSessionCoreAsync(staleAccessToken, cancellationToken).ConfigureAwait(false);
                    retriedAfterRefresh = true;
                    response = await SendDirectRawAsync(
                        method,
                        BuildDirectApiUri(path),
                        GetDirectAccessToken(),
                        body,
                        cancellationToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
                {
                    throw;
                }
                catch
                {
                    ClearDirectSession();
                    throw;
                }
            }

            if (authenticated && response.StatusCode == IGPErrorCodes.HTTP_UNAUTHORIZED)
            {
                var error = ParseDirectHttpError(response);
                ClearDirectSession();
                throw error;
            }

            if (response.StatusCode < 200 || response.StatusCode >= 300)
            {
                var error = ParseDirectHttpError(response);
                if (retriedAfterRefresh) ClearDirectSession();
                throw error;
            }

            try
            {
                return JToken.Parse(response.Body);
            }
            catch (JsonException exception)
            {
                if (retriedAfterRefresh) ClearDirectSession();
                throw DirectProtocolError("Curio response is invalid", exception);
            }
        }

        private async Task<JObject> SendDirectObjectAsync(
            HttpMethod method,
            string path,
            JToken? body,
            bool authenticated,
            CancellationToken cancellationToken)
        {
            var token = await SendDirectJsonAsync(
                method,
                path,
                body,
                authenticated,
                cancellationToken).ConfigureAwait(false);
            return token as JObject ?? throw DirectProtocolError("Curio response is invalid");
        }

        private async Task RefreshDirectSessionCoreAsync(
            string? staleAccessToken,
            CancellationToken cancellationToken)
        {
            await directRefreshGate.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                string refreshToken;
                lock (directSessionSync)
                {
                    if (staleAccessToken != null &&
                        !string.Equals(staleAccessToken, directAccessToken, StringComparison.Ordinal))
                    {
                        return;
                    }

                    refreshToken = directRefreshToken;
                }

                if (string.IsNullOrWhiteSpace(refreshToken))
                {
                    throw new IGPSDKException(
                        "Direct user session cannot be refreshed",
                        IGPErrorCodes.ERR_DIRECT_REFRESH_UNAVAILABLE);
                }

                var response = await SendDirectRawAsync(
                    HttpMethod.Post,
                    BuildDirectApiUri("/auth/session/refresh"),
                    string.Empty,
                    new JObject
                    {
                        ["refreshToken"] = refreshToken,
                        ["clientKind"] = "unity-sdk",
                        ["clientVersion"] = IGPSdkVersion.PackageVersion,
                    },
                    cancellationToken).ConfigureAwait(false);
                if (response.StatusCode < 200 || response.StatusCode >= 300)
                {
                    throw ParseDirectHttpError(response);
                }

                JObject root;
                try
                {
                    root = JObject.Parse(response.Body);
                }
                catch (JsonException exception)
                {
                    throw DirectProtocolError("Curio refresh response is invalid", exception);
                }

                var result = ParseAuthenticationResponse(root);
                EnsureIdentityCompatibleWithDesktop(result.Profile.Id);
                ApplyDirectAuthentication(result);
            }
            finally
            {
                directRefreshGate.Release();
            }
        }

        private async Task<IGPDirectHttpResponse> SendDirectRawAsync(
            HttpMethod method,
            Uri uri,
            string bearerToken,
            JToken? body,
            CancellationToken cancellationToken)
        {
            using var requestCts = CancellationTokenSource.CreateLinkedTokenSource(
                cancellationToken,
                RuntimeCancellationToken);
            requestCts.CancelAfter(directRequestTimeout);
            try
            {
                return await GetDirectHttpTransport().SendAsync(
                    method,
                    uri,
                    bearerToken,
                    body?.ToString(Formatting.None),
                    requestCts.Token).ConfigureAwait(false);
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                throw;
            }
            catch (OperationCanceledException) when (RuntimeCancellationToken.IsCancellationRequested)
            {
                throw new ObjectDisposedException(nameof(IGPRuntimeManager));
            }
            catch (OperationCanceledException exception)
            {
                throw new IGPSDKException(
                    "Direct API request timed out",
                    IGPErrorCodes.ERR_DIRECT_REQUEST_TIMEOUT,
                    exception);
            }
            catch (PlatformNotSupportedException exception)
            {
                throw new IGPSDKException(
                    exception.Message,
                    IGPErrorCodes.ERR_DIRECT_PLATFORM_UNSUPPORTED,
                    exception);
            }
            catch (IGPSDKException)
            {
                throw;
            }
            catch (Exception)
            {
                throw new IGPSDKException(
                    "Direct API request failed",
                    IGPErrorCodes.ERR_DIRECT_REQUEST_FAILED);
            }
        }

        private void ApplyDirectAuthentication(DirectAuthenticationResult result)
        {
            lock (directSessionSync)
            {
                directAccessToken = result.AccessToken;
                directRefreshToken = result.RefreshToken;
                directUserProfile = result.Profile.Clone();
            }
        }

        private static DirectAuthenticationResult ParseAuthenticationResponse(JObject root)
        {
            var accessToken = root.Value<string>("accessToken") ?? string.Empty;
            var refreshToken = root.Value<string>("refreshToken") ?? string.Empty;
            var user = root["user"] as JObject;
            if (string.IsNullOrWhiteSpace(accessToken) ||
                string.IsNullOrWhiteSpace(refreshToken) ||
                user == null)
            {
                throw DirectProtocolError("Curio authentication response is incomplete");
            }

            return new DirectAuthenticationResult(
                accessToken,
                refreshToken,
                ParseDirectUserProfile(user));
        }

        private static IGPUserProfile ParseDirectUserProfile(JObject root)
        {
            var id = root.Value<string>("id") ?? string.Empty;
            if (string.IsNullOrWhiteSpace(id))
            {
                throw DirectProtocolError("Curio user profile is incomplete");
            }

            var profile = root["profile"] as JObject;
            var avatar = profile?["avatar"] as JObject;
            var avatarFrame = profile?["avatarFrame"] as JObject;
            var nickname = root.Value<string>("nickname") ?? string.Empty;
            var discriminator = root.Value<int?>("discriminator") ?? 0;
            var displayTag = root.Value<string>("displayTag") ?? string.Empty;
            if (string.IsNullOrWhiteSpace(displayTag) &&
                !string.IsNullOrWhiteSpace(nickname) &&
                discriminator > 0)
            {
                displayTag = nickname + "#" + discriminator.ToString(CultureInfo.InvariantCulture);
            }

            return new IGPUserProfile
            {
                Id = id.Trim(),
                Nickname = nickname,
                Discriminator = discriminator,
                DisplayTag = displayTag,
                AvatarUrl = profile?.Value<string>("avatarUrl")
                    ?? avatar?.Value<string>("imageUrl")
                    ?? root.Value<string>("avatarUrl")
                    ?? string.Empty,
                AvatarFrameUrl = profile?.Value<string>("avatarFrameUrl")
                    ?? avatarFrame?.Value<string>("imageUrl")
                    ?? root.Value<string>("avatarFrameUrl")
                    ?? string.Empty,
            };
        }

        private void EnsureIdentityCompatibleWithDesktop(string directUserId)
        {
            var desktopUserId = GetActiveDesktopUserId();
            if (!string.IsNullOrWhiteSpace(desktopUserId) &&
                !string.Equals(desktopUserId, directUserId, StringComparison.Ordinal))
            {
                throw new IGPSDKException(
                    "Direct sign-in user does not match the attached Desktop user",
                    IGPErrorCodes.ERR_ACCOUNT_CONTEXT_CONFLICT);
            }
        }

        private string GetActiveDesktopUserId()
        {
            if (!GetCapabilityHostGateway().IsAttached ||
                currentDesktopUserContext == null ||
                !string.Equals(
                    currentDesktopUserContext.loginState,
                    "signedIn",
                    StringComparison.OrdinalIgnoreCase))
            {
                return string.Empty;
            }

            return currentDesktopUserContext.userId?.Trim() ?? string.Empty;
        }

        private void HandleDesktopIdentityAttached(string userId, string loginState)
        {
            if (!string.Equals(loginState, "signedIn", StringComparison.OrdinalIgnoreCase) ||
                string.IsNullOrWhiteSpace(userId))
            {
                return;
            }

            var directProfile = GetDirectProfileSnapshot();
            if (directProfile == null ||
                string.Equals(directProfile.Id, userId.Trim(), StringComparison.Ordinal))
            {
                return;
            }

            ClearDirectSession();
            LogSdkWarning(
                "account",
                "identity-conflict",
                "event=direct-session-cleared reason=desktop-user-mismatch");
        }

        private IIGPDirectHttpTransport GetDirectHttpTransport()
        {
            lock (directSessionSync)
            {
                directHttpTransport ??= new IGPSystemDirectHttpTransport();
                return directHttpTransport;
            }
        }

        private string GetDirectAccessToken()
        {
            lock (directSessionSync)
            {
                return directAccessToken;
            }
        }

        private IGPUserProfile? GetDirectProfileSnapshot()
        {
            lock (directSessionSync)
            {
                return directUserProfile?.Clone();
            }
        }

        private void UpdateDirectProfile(IGPUserProfile profile)
        {
            lock (directSessionSync)
            {
                if (directUserProfile == null ||
                    !string.Equals(directUserProfile.Id, profile.Id, StringComparison.Ordinal))
                {
                    throw DirectProtocolError("Curio session user does not match the active direct session");
                }

                directUserProfile = profile.Clone();
            }
        }

        private void EnsureDirectUserSession()
        {
            if (!HasUserSession)
            {
                throw new IGPSDKException(
                    "Direct user sign-in is required",
                    IGPErrorCodes.ERR_DIRECT_SESSION_REQUIRED);
            }
        }

        private void EnsureDirectCapabilityAvailable()
        {
            if (isDestroyed || RuntimeCancellationToken.IsCancellationRequested)
            {
                throw new ObjectDisposedException(nameof(IGPRuntimeManager));
            }

#if UNITY_WEBGL && !UNITY_EDITOR
            throw new IGPSDKException(
                "IGP Core direct API does not support Unity WebGL",
                IGPErrorCodes.ERR_DIRECT_PLATFORM_UNSUPPORTED);
#endif
            if (string.IsNullOrWhiteSpace(directApiBaseUrl))
            {
                directApiBaseUrl = IGPDirectApiEnvironment.ResolveBaseUrl(
                    SdkEnvironment,
                    config?.curioApiBaseUrlDebugOverride);
            }
        }

        private Uri BuildDirectApiUri(string path)
        {
            EnsureDirectCapabilityAvailable();
            var normalizedPath = string.IsNullOrEmpty(path) || path[0] == '/' ? path : "/" + path;
            return new Uri(directApiBaseUrl + normalizedPath, UriKind.Absolute);
        }

        private void ClearDirectSession()
        {
            lock (directSessionSync)
            {
                directAccessToken = string.Empty;
                directRefreshToken = string.Empty;
                directUserProfile = null;
            }
        }

        private void DisposeDirectCapabilities()
        {
            ClearDirectSession();
            lock (directSessionSync)
            {
                directHttpTransport?.Dispose();
                directHttpTransport = null;
            }
        }

        internal void ConfigureDirectHttpForTests(
            IIGPDirectHttpTransport transport,
            TimeSpan? requestTimeout = null)
        {
            if (transport == null)
            {
                throw new ArgumentNullException(nameof(transport));
            }

            var timeout = requestTimeout.GetValueOrDefault(DefaultDirectRequestTimeout);
            if (timeout <= TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(nameof(requestTimeout));
            }

            lock (directSessionSync)
            {
                directHttpTransport?.Dispose();
                directHttpTransport = transport;
                directRequestTimeout = timeout;
                directApiBaseUrl = string.Empty;
            }
        }

        private static string NormalizePhone(string phone)
        {
            var normalized = (phone ?? string.Empty).Trim();
            if (normalized.Length < 5 || normalized.Length > 32)
            {
                throw new ArgumentException("Phone must contain 5 to 32 characters", nameof(phone));
            }

            return normalized;
        }

        private static bool IsSixDigitCode(string value)
        {
            if (value == null || value.Length != 6)
            {
                return false;
            }

            for (var index = 0; index < value.Length; index++)
            {
                if (value[index] < '0' || value[index] > '9')
                {
                    return false;
                }
            }

            return true;
        }

        private IGPSDKException ParseDirectHttpError(IGPDirectHttpResponse response)
        {
            var code = response.StatusCode == IGPErrorCodes.HTTP_UNAUTHORIZED
                ? IGPErrorCodes.ERR_UNAUTHORIZED
                : "DIRECT_HTTP_" + response.StatusCode.ToString(CultureInfo.InvariantCulture);
            var message = "Direct API request failed with HTTP " +
                response.StatusCode.ToString(CultureInfo.InvariantCulture);
            try
            {
                var root = JObject.Parse(response.Body);
                code = ReadJsonString(root["code"])
                    ?? ReadJsonString((root["error"] as JObject)?["code"])
                    ?? code;
                var responseMessage = ReadJsonMessage(root["message"])
                    ?? ReadJsonMessage((root["error"] as JObject)?["message"]);
                if (!string.IsNullOrWhiteSpace(responseMessage))
                {
                    message = responseMessage!;
                }
            }
            catch (JsonException)
            {
            }

            return new IGPSDKException(
                RedactDirectSecrets(message),
                RedactDirectSecrets(code));
        }

        private static string? ReadJsonString(JToken? token)
        {
            return token?.Type == JTokenType.String ? token.Value<string>() : null;
        }

        private static string? ReadJsonMessage(JToken? token)
        {
            if (token?.Type == JTokenType.String)
            {
                return token.Value<string>();
            }

            if (!(token is JArray array))
            {
                return null;
            }

            var messages = new System.Collections.Generic.List<string>();
            foreach (var item in array)
            {
                var message = ReadJsonString(item);
                if (!string.IsNullOrWhiteSpace(message))
                {
                    messages.Add(message!);
                }
            }

            return messages.Count > 0 ? string.Join("; ", messages) : null;
        }

        private string RedactDirectSecrets(string value)
        {
            var redacted = value ?? string.Empty;
            lock (directSessionSync)
            {
                if (!string.IsNullOrEmpty(directAccessToken))
                {
                    redacted = redacted.Replace(directAccessToken, "[REDACTED]");
                }

                if (!string.IsNullOrEmpty(directRefreshToken))
                {
                    redacted = redacted.Replace(directRefreshToken, "[REDACTED]");
                }
            }

            return redacted;
        }

        private static IGPSDKException DirectProtocolError(string message, Exception? inner = null)
        {
            return inner == null
                ? new IGPSDKException(message, IGPErrorCodes.ERR_DIRECT_PROTOCOL)
                : new IGPSDKException(message, IGPErrorCodes.ERR_DIRECT_PROTOCOL, inner);
        }

        private sealed class DirectAuthenticationResult
        {
            internal string AccessToken { get; }
            internal string RefreshToken { get; }
            internal IGPUserProfile Profile { get; }

            internal DirectAuthenticationResult(
                string accessToken,
                string refreshToken,
                IGPUserProfile profile)
            {
                AccessToken = accessToken;
                RefreshToken = refreshToken;
                Profile = profile;
            }
        }
    }
}
