#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using IGP.UnitySDK.Models;
using Newtonsoft.Json;

namespace IGP.UnitySDK.GameKit
{
    internal enum IGPLeaderboardHttpMethod
    {
        Get = 0,
        Post = 1,
    }

    [Serializable]
    internal sealed class IGPLeaderboardRouteSpec
    {
        internal IGPLeaderboardRouteSpec(
            IGPLeaderboardHttpMethod method,
            string route,
            params string[] pathParamNames)
        {
            Method = method;
            Route = route ?? string.Empty;
            PathParamNames = pathParamNames ?? Array.Empty<string>();
        }

        public IGPLeaderboardHttpMethod Method { get; }
        public string Route { get; }
        public string[] PathParamNames { get; }
    }

    public sealed class IGPLeaderboardException : Exception
    {
        public IGPLeaderboardException(
            string code,
            string message,
            string? upstreamJson = null)
            : base(message)
        {
            Code = code ?? string.Empty;
            UpstreamJson = upstreamJson;
        }

        public string Code { get; }
        public string? UpstreamJson { get; }

        public int? HttpStatus()
        {
            var match = Regex.Match(Code, "^LEADERBOARD_HTTP_(\\d+)$");
            return match.Success && int.TryParse(match.Groups[1].Value, out var status)
                ? status
                : null;
        }
    }

    internal static class IGPLeaderboardRoutes
    {
        public const string ListByGame = "games/:gameId";
        public const string Leaderboard = ":leaderboardId";
        public const string SubmitScore = ":leaderboardId/score";
        public const string QueryScores = ":leaderboardId/scores/query";

        public static readonly IGPLeaderboardRouteSpec ListByGameSpec =
            Get(ListByGame, "gameId");
        public static readonly IGPLeaderboardRouteSpec LeaderboardSpec =
            Get(Leaderboard, "leaderboardId");
        public static readonly IGPLeaderboardRouteSpec SubmitScoreSpec =
            Post(SubmitScore, "leaderboardId");
        public static readonly IGPLeaderboardRouteSpec QueryScoresSpec =
            Post(QueryScores, "leaderboardId");

        public static readonly IGPLeaderboardRouteSpec[] PlayerRoutes =
        {
            ListByGameSpec,
            LeaderboardSpec,
            SubmitScoreSpec,
            QueryScoresSpec,
        };

        public static IGPLeaderboardRouteSpec Find(
            IGPLeaderboardHttpMethod method,
            string route)
        {
            foreach (var spec in PlayerRoutes)
            {
                if (spec.Method == method &&
                    string.Equals(spec.Route, route, StringComparison.Ordinal))
                {
                    return spec;
                }
            }

            throw new ArgumentException($"Unsupported Leaderboard route: {method} {route}");
        }

        private static IGPLeaderboardRouteSpec Get(
            string route,
            params string[] pathParamNames)
        {
            return new IGPLeaderboardRouteSpec(
                IGPLeaderboardHttpMethod.Get,
                route,
                pathParamNames);
        }

        private static IGPLeaderboardRouteSpec Post(
            string route,
            params string[] pathParamNames)
        {
            return new IGPLeaderboardRouteSpec(
                IGPLeaderboardHttpMethod.Post,
                route,
                pathParamNames);
        }
    }

    public static class IGPLeaderboard
    {
        // leaderboardId basic local validation (backend remains authoritative).
        private const int MaxLeaderboardIdLength = 96;
        private const int DefaultLeaderboardStart = 0;
        private const int DefaultLeaderboardCount = 20;
        private const int MaxLeaderboardCount = 100;
        private const int MaxQueryUserIds = 50;

        private static async Task<TResponse> CallAsync<TResponse>(
            IGPRuntimeManager runtimeManager,
            int gameId,
            IGPLeaderboardRouteSpec spec,
            IDictionary<string, string>? pathParams = null,
            IDictionary<string, string>? query = null)
        {
            return await CallAsync<object, TResponse>(
                runtimeManager,
                gameId,
                spec,
                pathParams,
                body: null,
                query);
        }

        private static async Task<TResponse> CallAsync<TRequest, TResponse>(
            IGPRuntimeManager runtimeManager,
            int gameId,
            IGPLeaderboardRouteSpec spec,
            IDictionary<string, string>? pathParams = null,
            TRequest? body = default,
            IDictionary<string, string>? query = null)
        {
            if (runtimeManager == null)
            {
                throw new ArgumentNullException(nameof(runtimeManager));
            }

            ValidateRouteSpec(spec);
            ValidateGameId(gameId);
            ValidatePathParams(spec, pathParams);
            ValidateQuery(query);

            var payload = new Dictionary<string, object?>
            {
                ["method"] = spec.Method == IGPLeaderboardHttpMethod.Get ? "GET" : "POST",
                ["appId"] = gameId,
                ["route"] = spec.Route,
            };

            if (pathParams != null && pathParams.Count > 0)
            {
                payload["pathParams"] = pathParams;
            }

            if (query != null && query.Count > 0)
            {
                payload["query"] = query;
            }

            if (spec.Method == IGPLeaderboardHttpMethod.Post && body != null)
            {
                payload["body"] = body;
            }

            var requestJson = JsonConvert.SerializeObject(
                payload,
                new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
            IGPDesktopSessionCommandResult result;
            try
            {
                result = await runtimeManager.LeaderboardForwardAsync(
                    requestJson,
                    gameId);
            }
            catch (IGPSDKException ex)
            {
                throw new IGPLeaderboardException(
                    string.IsNullOrWhiteSpace(ex.ErrorCode)
                        ? IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED
                        : ex.ErrorCode!,
                    string.IsNullOrWhiteSpace(ex.Message)
                        ? "Leaderboard command failed"
                        : ex.Message);
            }

            return ParseResult<TResponse>(result);
        }

        public static async Task<IGPListLeaderboardsByGameResult> ListLeaderboardsByGameAsync(
            IGPRuntimeManager runtimeManager,
            IGPListLeaderboardsByGameOptions? options = null)
        {
            var gameId = ResolveGameId(runtimeManager);
            ValidateGameId(gameId);
            var period = NormalizePeriod(options?.period);
            if (options?.leaderboardId != null)
            {
                ValidateLeaderboardId(options.leaderboardId);
            }

            var queryParams = new Dictionary<string, string>();
            if (!string.IsNullOrWhiteSpace(options?.leaderboardId))
            {
                AddString(queryParams, "leaderboardId", options!.leaderboardId!.Trim());
            }

            AddString(queryParams, "period", period);

            return await CallAsync<IGPListLeaderboardsByGameResult>(
                runtimeManager,
                gameId,
                IGPLeaderboardRoutes.ListByGameSpec,
                PathParams("gameId", gameId.ToString(CultureInfo.InvariantCulture)),
                query: queryParams);
        }

        public static async Task<IGPGetLeaderboardResult> GetLeaderboardAsync(
            IGPRuntimeManager runtimeManager,
            string leaderboardId,
            IGPGetLeaderboardQuery? query = null)
        {
            var gameId = ResolveGameId(runtimeManager);
            ValidateLeaderboardId(leaderboardId);
            var period = NormalizePeriod(query?.period);
            var start = NormalizeStart(query?.start);
            var count = NormalizeCount(query?.count);

            var queryParams = new Dictionary<string, string>();
            AddString(queryParams, "gameId", gameId.ToString(CultureInfo.InvariantCulture));
            AddString(queryParams, "period", period);
            AddString(queryParams, "start", start.ToString(CultureInfo.InvariantCulture));
            AddString(queryParams, "count", count.ToString(CultureInfo.InvariantCulture));
            if (query?.includeSelf == true)
            {
                AddString(queryParams, "includeSelf", "true");
            }

            return await CallAsync<IGPGetLeaderboardResult>(
                runtimeManager,
                gameId,
                IGPLeaderboardRoutes.LeaderboardSpec,
                PathParams("leaderboardId", leaderboardId),
                query: queryParams);
        }

        public static async Task<IGPSubmitLeaderboardScoreResult> SubmitLeaderboardScoreAsync(
            IGPRuntimeManager runtimeManager,
            string leaderboardId,
            long value,
            IGPSubmitLeaderboardScoreOptions? options = null)
        {
            var gameId = ResolveGameId(runtimeManager);
            ValidateLeaderboardId(leaderboardId);
            ValidateOptionalToken(options?.eventId, nameof(options.eventId));
            ValidateOptionalToken(options?.sessionId, nameof(options.sessionId));

            var request = new IGPSubmitLeaderboardScoreRequest
            {
                gameId = gameId,
                value = value,
                eventId = string.IsNullOrEmpty(options?.eventId) ? null : options!.eventId,
                sessionId = string.IsNullOrEmpty(options?.sessionId) ? null : options!.sessionId,
            };

            return await CallAsync<IGPSubmitLeaderboardScoreRequest, IGPSubmitLeaderboardScoreResult>(
                runtimeManager,
                gameId,
                IGPLeaderboardRoutes.SubmitScoreSpec,
                PathParams("leaderboardId", leaderboardId),
                request);
        }

        public static async Task<IGPQueryLeaderboardScoresResult> QueryLeaderboardScoresAsync(
            IGPRuntimeManager runtimeManager,
            string leaderboardId,
            string[] userIds,
            IGPLeaderboardPeriodOptions? options = null)
        {
            var gameId = ResolveGameId(runtimeManager);
            ValidateLeaderboardId(leaderboardId);
            ValidateUserIds(userIds);
            var period = NormalizePeriod(options?.period);

            var request = new IGPQueryLeaderboardScoresRequest
            {
                gameId = gameId,
                userIds = userIds,
                period = period,
            };

            return await CallAsync<IGPQueryLeaderboardScoresRequest, IGPQueryLeaderboardScoresResult>(
                runtimeManager,
                gameId,
                IGPLeaderboardRoutes.QueryScoresSpec,
                PathParams("leaderboardId", leaderboardId),
                request);
        }

        private static Dictionary<string, string> PathParams(
            string key,
            string value)
        {
            return new Dictionary<string, string>
            {
                [key] = value,
            };
        }

        private static void AddString(
            IDictionary<string, string> query,
            string key,
            string? value)
        {
            if (!string.IsNullOrEmpty(value))
            {
                query[key] = value!;
            }
        }

        private static string NormalizePeriod(string? period)
        {
            if (string.IsNullOrEmpty(period))
            {
                return IGPLeaderboardPeriod.Permanent;
            }

            if (!string.Equals(period, IGPLeaderboardPeriod.Permanent, StringComparison.Ordinal))
            {
                throw new ArgumentException(
                    $"Unsupported leaderboard period: {period}",
                    nameof(period));
            }

            return IGPLeaderboardPeriod.Permanent;
        }

        private static int ResolveGameId(IGPRuntimeManager runtimeManager)
        {
            if (runtimeManager == null)
            {
                throw new ArgumentNullException(nameof(runtimeManager));
            }

            var gameId = runtimeManager.Config?.appId ?? 0;
            if (gameId <= 0)
            {
                throw new IGPLeaderboardException(
                    IGPErrorCodes.ERR_APP_ID_REQUIRED,
                    "IGPConfig.appId is required before leaderboard can be used");
            }

            return gameId;
        }

        private static void ValidateGameId(int gameId)
        {
            if (gameId <= 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(gameId),
                    "gameId must be a positive appId");
            }
        }

        private static int NormalizeStart(int? start)
        {
            if (!start.HasValue)
            {
                return DefaultLeaderboardStart;
            }

            if (start.Value < 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(start),
                    "Leaderboard query start must be greater than or equal to 0");
            }

            return start.Value;
        }

        private static int NormalizeCount(int? count)
        {
            if (!count.HasValue)
            {
                return DefaultLeaderboardCount;
            }

            if (count.Value < 1 || count.Value > MaxLeaderboardCount)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(count),
                    $"Leaderboard query count must be between 1 and {MaxLeaderboardCount}");
            }

            return count.Value;
        }

        private static void ValidateLeaderboardId(string leaderboardId)
        {
            if (string.IsNullOrWhiteSpace(leaderboardId))
            {
                throw new ArgumentException(
                    "leaderboardId must be a non-empty string",
                    nameof(leaderboardId));
            }

            if (leaderboardId.Trim().Length > MaxLeaderboardIdLength)
            {
                throw new ArgumentException(
                    $"leaderboardId must be at most {MaxLeaderboardIdLength} characters",
                    nameof(leaderboardId));
            }
        }

        private static void ValidateOptionalToken(string? value, string fieldName)
        {
            if (value != null && value.Trim().Length == 0)
            {
                throw new ArgumentException(
                    $"{fieldName} must be a non-empty string when provided",
                    fieldName);
            }
        }

        private static void ValidateUserIds(string[] userIds)
        {
            if (userIds == null || userIds.Length == 0)
            {
                throw new ArgumentException(
                    "userIds must contain at least 1 id",
                    nameof(userIds));
            }

            if (userIds.Length > MaxQueryUserIds)
            {
                throw new ArgumentException(
                    $"userIds must contain at most {MaxQueryUserIds} ids",
                    nameof(userIds));
            }

            foreach (var userId in userIds)
            {
                if (string.IsNullOrWhiteSpace(userId))
                {
                    throw new ArgumentException(
                        "userIds must not contain empty ids",
                        nameof(userIds));
                }
            }
        }

        private static TResponse ParseResult<TResponse>(
            IGPDesktopSessionCommandResult result)
        {
            if (result == null)
            {
                throw new IGPLeaderboardException(
                    IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED,
                    "Desktop session returned no command result");
            }

            if (!result.Success)
            {
                throw new IGPLeaderboardException(
                    string.IsNullOrWhiteSpace(result.Code)
                        ? IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED
                        : result.Code,
                    string.IsNullOrWhiteSpace(result.Message)
                        ? "Leaderboard command failed"
                        : result.Message,
                    result.ContentJson);
            }

            if (typeof(TResponse) == typeof(string))
            {
                return (TResponse)(object)(result.ContentJson ?? string.Empty);
            }

            if (string.IsNullOrWhiteSpace(result.ContentJson))
            {
                throw new IGPLeaderboardException(
                    "LEADERBOARD_INVALID_RESPONSE",
                    "Leaderboard response contentJson is empty");
            }

            try
            {
                var parsed = JsonConvert.DeserializeObject<TResponse>(result.ContentJson);
                if (parsed == null)
                {
                    throw new IGPLeaderboardException(
                        "LEADERBOARD_INVALID_RESPONSE",
                        "Leaderboard response contentJson could not be parsed",
                        result.ContentJson);
                }

                return parsed;
            }
            catch (IGPLeaderboardException)
            {
                throw;
            }
            catch (JsonException ex)
            {
                throw new IGPLeaderboardException(
                    "LEADERBOARD_INVALID_RESPONSE",
                    $"Leaderboard response contentJson is invalid JSON: {ex.Message}",
                    result.ContentJson);
            }
        }

        private static void ValidateRouteSpec(IGPLeaderboardRouteSpec spec)
        {
            if (spec == null)
            {
                throw new ArgumentNullException(nameof(spec));
            }

            foreach (var allowed in IGPLeaderboardRoutes.PlayerRoutes)
            {
                if (allowed.Method == spec.Method &&
                    string.Equals(allowed.Route, spec.Route, StringComparison.Ordinal))
                {
                    return;
                }
            }

            throw new ArgumentException($"Unsupported Leaderboard route: {spec.Method} {spec.Route}");
        }

        private static void ValidatePathParams(
            IGPLeaderboardRouteSpec spec,
            IDictionary<string, string>? pathParams)
        {
            foreach (var paramName in spec.PathParamNames)
            {
                if (pathParams == null ||
                    !pathParams.TryGetValue(paramName, out var value) ||
                    string.IsNullOrWhiteSpace(value))
                {
                    throw new ArgumentException(
                        $"Leaderboard route requires path param: {paramName}",
                        nameof(pathParams));
                }
            }
        }

        private static void ValidateQuery(IDictionary<string, string>? query)
        {
            if (query == null)
            {
                return;
            }

            foreach (var item in query)
            {
                if (string.IsNullOrWhiteSpace(item.Key))
                {
                    throw new ArgumentException("Leaderboard query keys must be non-empty strings", nameof(query));
                }

                if (item.Value == null)
                {
                    throw new ArgumentException("Leaderboard query values must be strings", nameof(query));
                }
            }
        }
    }

    public sealed class IGPSubmitLeaderboardScoreOptions
    {
        public string? eventId;   // reuse across retries of the same settlement
        public string? sessionId; // optional game session id (pass-through)
    }
}
