using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

namespace CoolGameX
{
    /// <summary>
    /// Changes made while offline — achievement unlocks, stat updates and playtime —
    /// saved on the device (PlayerPrefs) and synced automatically once the
    /// connection comes back. Leaderboard scores and cloud saves are not queued:
    /// those calls return false so your game can decide what to do.
    /// </summary>
    public class OfflineQueue
    {
        [Serializable]
        internal class Op
        {
            public string type;   // unlock | stat_set | stat_add | playtime
            public string slug;   // achievement slug
            public string key;    // stat key
            public int value;     // stat value / amount / minutes
            public long ts;
        }

        [Serializable] private class Store { public List<Op> items = new List<Op>(); }

        /// <summary>Queue changes while offline (on by default; set in the settings asset).</summary>
        public bool Enabled { get; set; } = true;

        /// <summary>How many changes are waiting to sync.</summary>
        public int PendingCount => Items.Count;

        /// <summary>Raised whenever the pending count changes.</summary>
        public event Action<int> Changed;

        private List<Op> items;
        private bool flushing;
        private string PrefKey => "CoolGameX.Queue." + CoolAPI.GameSlug;

        private List<Op> Items
        {
            get
            {
                if (items == null) Load();
                return items;
            }
        }

        internal void Enqueue(Op op)
        {
            op.ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            switch (op.type)
            {
                case "unlock":
                    if (Items.Exists(x => x.type == "unlock" && x.slug == op.slug)) return;
                    break;
                case "stat_set":
                    Items.RemoveAll(x => x.type == "stat_set" && x.key == op.key);
                    break;
                case "stat_add":
                    var add = Items.Find(x => x.type == "stat_add" && x.key == op.key);
                    if (add != null) { add.value += op.value; Save(); return; }
                    break;
                case "playtime":
                    var pt = Items.Find(x => x.type == "playtime");
                    if (pt != null) { pt.value += op.value; Save(); return; }
                    break;
            }
            Items.Add(op);
            Save();
            Log.Info($"Offline — saved {Describe(op)} to sync later ({PendingCount} pending).");
        }

        /// <summary>
        /// Send everything that's waiting. Stops at the first network failure and
        /// keeps the rest for later. Returns how many changes were synced.
        /// </summary>
        public async Task<int> Flush()
        {
            if (flushing || Items.Count == 0 || !CoolAPI.IsAuthenticated) return 0;
            flushing = true;
            int synced = 0;
            try
            {
                while (Items.Count > 0)
                {
                    var op = Items[0];
                    var res = await Send(op);
                    if (res.IsTransient || res.StatusCode == 401) break;

                    Items.RemoveAt(0);
                    Save();
                    if (res.Success)
                    {
                        synced++;
                        if (op.type == "unlock") CoolAPI.Achievements.OnQueuedUnlockSynced(op.slug, res.Body);
                    }
                    else
                    {
                        Log.Warn($"Dropped queued {Describe(op)}: {res.Error}");
                    }
                }
            }
            finally
            {
                flushing = false;
            }
            if (synced > 0) Log.Info($"Synced {synced} change(s) made while offline.");
            return synced;
        }

        private static Task<ApiResult> Send(Op op)
        {
            string game = CoolAPI.GameSlug;
            switch (op.type)
            {
                case "unlock":
                    return Net.PostJson("/api/achievements/unlock", Json.Of(new Dto.Unlock { gameSlug = game, achievementSlug = op.slug }), true, retry: false);
                case "stat_set":
                    return Net.PostJson("/api/stats/set", Json.Of(new Dto.StatSet { gameSlug = game, key = op.key, value = op.value }), true, retry: false);
                case "stat_add":
                    return Net.PostJson("/api/stats/add", Json.Of(new Dto.StatAdd { gameSlug = game, key = op.key, amount = op.value }), true, retry: false);
                case "playtime":
                    return Net.PostJson("/api/playtime/update", Json.Of(new Dto.Playtime { gameSlug = game, minutes = op.value }), true, retry: false);
                default:
                    return Task.FromResult(new ApiResult { Success = false, StatusCode = 400, Error = "Unknown queued change" });
            }
        }

        private static string Describe(Op op)
        {
            switch (op.type)
            {
                case "unlock": return $"unlock of '{op.slug}'";
                case "stat_set": return $"stat '{op.key}' = {op.value}";
                case "stat_add": return $"stat '{op.key}' +{op.value}";
                case "playtime": return $"{op.value} min of playtime";
                default: return op.type;
            }
        }

        /// <summary>Throw away everything that's waiting.</summary>
        public void Clear()
        {
            Items.Clear();
            Save();
        }

        private void Load()
        {
            items = new List<Op>();
            string json = PlayerPrefs.GetString(PrefKey, "");
            if (string.IsNullOrEmpty(json)) return;
            try { items = JsonUtility.FromJson<Store>(json)?.items ?? new List<Op>(); }
            catch { items = new List<Op>(); }
        }

        private void Save()
        {
            PlayerPrefs.SetString(PrefKey, JsonUtility.ToJson(new Store { items = items }));
            PlayerPrefs.Save();
            try { Changed?.Invoke(items.Count); } catch (Exception e) { Debug.LogException(e); }
        }
    }
}
