using System;
using System.Threading.Tasks;

namespace CoolGameX
{
    /// <summary>
    /// Per-player, per-game numeric stats — enemies killed, levels cleared, coins…
    /// Stored on the backend and follow the player across machines. No setup:
    /// a stat exists the first time you set it.
    /// <code>
    /// await CoolAPI.Stats.Add("enemies_killed");
    /// await CoolAPI.Stats.Set("deepest_floor", 12);
    /// int kills = await CoolAPI.Stats.Get("enemies_killed");
    /// </code>
    /// </summary>
    public class Stats
    {
        /// <summary>Set a stat to an exact value. Queued while offline.</summary>
        public async Task<bool> Set(string key, int value)
        {
            if (!Valid(key, nameof(Set))) return false;
            var res = await Net.PostJson("/api/stats/set", Json.Of(new Dto.StatSet { gameSlug = CoolAPI.GameSlug, key = key, value = value }), auth: true, retry: true);
            if (res.Success) return true;
            if (res.IsTransient && CoolAPI.Queue.Enabled)
            {
                CoolAPI.Queue.Enqueue(new OfflineQueue.Op { type = "stat_set", key = key, value = value });
                return true;
            }
            return false;
        }

        /// <summary>
        /// Add <paramref name="amount"/> (default 1) and return the new total, or -1 when
        /// the total isn't known (failed, or queued while offline).
        /// </summary>
        public async Task<int> Add(string key, int amount = 1)
        {
            if (!Valid(key, nameof(Add))) return -1;
            // Not retried automatically: a retry after a lost response could double-count.
            var res = await Net.PostJson("/api/stats/add", Json.Of(new Dto.StatAdd { gameSlug = CoolAPI.GameSlug, key = key, amount = amount }), auth: true, retry: false);
            if (res.Success) return Json.Parse<Dto.StatValue>(res.Body)?.value ?? -1;
            if (res.IsNetworkError && CoolAPI.Queue.Enabled)
                CoolAPI.Queue.Enqueue(new OfflineQueue.Op { type = "stat_add", key = key, value = amount });
            return -1;
        }

        /// <summary>Read a stat (0 if it has never been set or the request fails).</summary>
        public async Task<int> Get(string key)
        {
            if (!Valid(key, nameof(Get))) return 0;
            var res = await Net.Get($"/api/stats?gameSlug={Json.Q(CoolAPI.GameSlug)}&key={Json.Q(key)}", auth: true);
            return res.Success ? Json.Parse<Dto.StatValue>(res.Body)?.value ?? 0 : 0;
        }

        /// <summary>All of the player's stats for this game.</summary>
        public async Task<StatEntry[]> GetAll()
        {
            var res = await Net.Get($"/api/stats?gameSlug={Json.Q(CoolAPI.GameSlug)}", auth: true);
            if (!res.Success) return Array.Empty<StatEntry>();
            return Json.Parse<Dto.StatList>(res.Body)?.stats ?? Array.Empty<StatEntry>();
        }

        /// <summary>Raw JSON of all stats: <c>{"stats":[{"key":"…","value":0}]}</c>. Prefer <see cref="GetAll"/>.</summary>
        public async Task<string> GetAllJson()
        {
            var res = await Net.Get($"/api/stats?gameSlug={Json.Q(CoolAPI.GameSlug)}", auth: true);
            return res.Success ? res.Body : null;
        }

        private static bool Valid(string key, string method)
        {
            if (!string.IsNullOrEmpty(key)) return true;
            Log.Warn($"Stats.{method} called with an empty key.");
            return false;
        }
    }

    /// <summary>
    /// Leaderboards. A board is created the first time you submit to it — no
    /// dashboard setup needed.
    /// <code>
    /// var r = await CoolAPI.Leaderboards.Submit("high_score", 9001);
    /// if (r.Improved) ShowNewBest(r.Rank);
    /// var top = await CoolAPI.Leaderboards.GetTop("high_score", 10);
    /// </code>
    /// </summary>
    public class Leaderboards
    {
        /// <summary>
        /// Submit a score. By default only a better score replaces the player's entry.
        /// Use <see cref="SortOrder.Ascending"/> for boards where lower wins (speedruns).
        /// The sort order is fixed when the board is first created.
        /// </summary>
        public async Task<SubmitResult> Submit(string board, long score, SortOrder sort = SortOrder.Descending, bool keepBest = true, string metadata = null)
        {
            if (string.IsNullOrEmpty(board)) return new SubmitResult { Error = "Empty board name" };
            var body = Json.Of(new Dto.Submit
            {
                gameSlug = CoolAPI.GameSlug,
                board = board,
                score = score,
                sort = sort == SortOrder.Ascending ? "asc" : "desc",
                keepBest = keepBest,
                metadata = metadata ?? "",
            });
            var res = await Net.PostJson("/api/leaderboards/submit", body, auth: true, retry: true);
            if (!res.Success) return new SubmitResult { Error = res.Error };
            var r = Json.Parse<Dto.SubmitResponse>(res.Body);
            if (r == null) return new SubmitResult { Error = "Bad response" };
            return new SubmitResult { Success = true, Improved = r.improved, Score = r.score, Rank = r.rank };
        }

        /// <summary>The top of a board. Works without a signed-in player.</summary>
        public async Task<LeaderboardPage> GetTop(string board, int limit = 10, int offset = 0)
        {
            var res = await Net.Get($"/api/leaderboards/top?gameSlug={Json.Q(CoolAPI.GameSlug)}&board={Json.Q(board)}&limit={limit}&offset={offset}");
            return Page(res);
        }

        /// <summary>The player's entry with <paramref name="range"/> neighbours above and below.</summary>
        public async Task<LeaderboardPage> GetAroundMe(string board, int range = 3)
        {
            var res = await Net.Get($"/api/leaderboards/me?gameSlug={Json.Q(CoolAPI.GameSlug)}&board={Json.Q(board)}&range={range}", auth: true);
            return Page(res);
        }

        private static LeaderboardPage Page(ApiResult res)
        {
            var page = res.Success ? Json.Parse<LeaderboardPage>(res.Body) : null;
            if (page == null) return new LeaderboardPage { ok = false };
            if (page.entries == null) page.entries = Array.Empty<LeaderboardEntry>();
            page.ok = true;
            return page;
        }
    }

    /// <summary>
    /// Cloud saves: named slots (up to 50, 512 KB each) stored per player, per game.
    /// <code>
    /// await CoolAPI.CloudSave.Save("slot1", mySaveData);      // any [Serializable] class
    /// var data = await CoolAPI.CloudSave.Load&lt;MySave&gt;("slot1");
    /// </code>
    /// </summary>
    public class CloudSave
    {
        /// <summary>Save text (usually JSON) to a slot. Returns false on failure.</summary>
        public async Task<bool> Save(string key, string data)
        {
            if (string.IsNullOrEmpty(key)) { Log.Warn("CloudSave.Save called with an empty key."); return false; }
            var res = await Net.PutJson("/api/cloudsave", Json.Of(new Dto.Save { gameSlug = CoolAPI.GameSlug, key = key, data = data ?? "" }), auth: true, retry: true);
            return res.Success;
        }

        /// <summary>Save any <c>[Serializable]</c> object (via JsonUtility).</summary>
        public Task<bool> Save<T>(string key, T value) => Save(key, UnityEngine.JsonUtility.ToJson(value));

        /// <summary>Load a slot's text, or null if the slot is empty or the request fails.</summary>
        public async Task<string> Load(string key)
        {
            var res = await Net.Get($"/api/cloudsave?gameSlug={Json.Q(CoolAPI.GameSlug)}&key={Json.Q(key)}", auth: true);
            return res.Success ? Json.Parse<Dto.SaveResponse>(res.Body)?.data : null;
        }

        /// <summary>Load a slot into <typeparamref name="T"/>, or default when empty.</summary>
        public async Task<T> Load<T>(string key)
        {
            string json = await Load(key);
            if (string.IsNullOrEmpty(json)) return default;
            try { return UnityEngine.JsonUtility.FromJson<T>(json); }
            catch (Exception e) { Log.Warn($"Save slot '{key}' couldn't be read as {typeof(T).Name}: {e.Message}"); return default; }
        }

        /// <summary>Delete a slot.</summary>
        public async Task<bool> Delete(string key)
        {
            var res = await Net.Delete($"/api/cloudsave?gameSlug={Json.Q(CoolAPI.GameSlug)}&key={Json.Q(key)}", auth: true);
            return res.Success;
        }

        /// <summary>All of the player's slots (without data), newest first.</summary>
        public async Task<SaveSlot[]> List()
        {
            var res = await Net.Get($"/api/cloudsave/list?gameSlug={Json.Q(CoolAPI.GameSlug)}", auth: true);
            if (!res.Success) return Array.Empty<SaveSlot>();
            return Json.Parse<Dto.SaveList>(res.Body)?.saves ?? Array.Empty<SaveSlot>();
        }
    }
}
