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

namespace CoolGameX
{
    /// <summary>Unlock and query achievements for the current game.</summary>
    public class Achievements
    {
        /// <summary>
        /// Fires when an achievement is unlocked for the FIRST time (slug). Use it for
        /// your own banner or sound.
        /// </summary>
        public event Action<string> OnAchievementUnlocked;

        /// <summary>Like <see cref="OnAchievementUnlocked"/> but with name, points and icon when known.</summary>
        public event Action<AchievementInfo> Unlocked;

        private readonly Dictionary<string, AchievementInfo> cache = new Dictionary<string, AchievementInfo>();
        private readonly HashSet<string> announced = new HashSet<string>();

        /// <summary>True once definitions have been loaded (see <see cref="GetAll"/>).</summary>
        public bool IsLoaded { get; private set; }

        /// <summary>Unlocks an achievement. Safe to call repeatedly.</summary>
        /// <returns>True if recorded (or queued while offline).</returns>
        public async Task<bool> Unlock(string achievementSlug)
        {
            var result = await UnlockDetailed(achievementSlug);
            return result.Success;
        }

        /// <summary>Unlock and learn whether it was the first time, or queued offline.</summary>
        public async Task<UnlockResult> UnlockDetailed(string achievementSlug)
        {
            if (string.IsNullOrEmpty(achievementSlug))
            {
                Log.Warn("Unlock called with an empty slug.");
                return new UnlockResult { Error = "Empty slug" };
            }
            if (!CoolAPI.IsAuthenticated)
            {
                Log.Warn($"Can't unlock '{achievementSlug}' — no player is signed in.");
                return new UnlockResult { Error = "Not signed in" };
            }

            var body = Json.Of(new Dto.Unlock { gameSlug = CoolAPI.GameSlug, achievementSlug = achievementSlug });
            var res = await Net.PostJson("/api/achievements/unlock", body, auth: true, retry: true);

            if (!res.Success)
            {
                if (res.IsTransient && CoolAPI.Queue.Enabled)
                {
                    CoolAPI.Queue.Enqueue(new OfflineQueue.Op { type = "unlock", slug = achievementSlug });
                    if (!IsUnlocked(achievementSlug)) Announce(achievementSlug);
                    return new UnlockResult { Success = true, Queued = true, NewlyUnlocked = !IsUnlocked(achievementSlug) };
                }
                if (res.StatusCode == 404) Log.Warn($"No achievement '{achievementSlug}' exists for '{CoolAPI.GameSlug}' — check the slug in the Creator dashboard.");
                return new UnlockResult { Error = res.Error };
            }

            bool newly = Json.Parse<Dto.UnlockResponse>(res.Body)?.newlyUnlocked ?? true;
            bool knownBefore = IsUnlocked(achievementSlug);
            MarkUnlocked(achievementSlug);
            if (newly && !knownBefore) Announce(achievementSlug);
            return new UnlockResult { Success = true, NewlyUnlocked = newly };
        }

        /// <summary>
        /// Incremental achievement ("kill 100 enemies"): stores progress as a stat and
        /// unlocks automatically when <paramref name="current"/> reaches <paramref name="target"/>.
        /// </summary>
        public async Task<UnlockResult> Progress(string achievementSlug, int current, int target)
        {
            if (target <= 0) return await UnlockDetailed(achievementSlug);
            await CoolAPI.Stats.Set(ProgressKey(achievementSlug), Mathf.Clamp(current, 0, target));
            if (current >= target) return await UnlockDetailed(achievementSlug);
            return new UnlockResult { Success = true };
        }

        /// <summary>Saved progress for an incremental achievement (see <see cref="Progress"/>).</summary>
        public Task<int> GetProgress(string achievementSlug) => CoolAPI.Stats.Get(ProgressKey(achievementSlug));

        private static string ProgressKey(string slug) => "ach." + slug;

        /// <summary>
        /// All achievements for this game with the player's unlock state. Cached after
        /// the first call; pass refresh:true to reload.
        /// </summary>
        public async Task<AchievementInfo[]> GetAll(bool refresh = false)
        {
            if (IsLoaded && !refresh) return Snapshot();

            if (CoolAPI.IsAuthenticated)
            {
                var res = await Net.Get($"/api/sdk/achievements?gameSlug={Json.Q(CoolAPI.GameSlug)}", auth: true);
                if (res.Success)
                {
                    Fill(Json.Parse<Dto.AchievementList>(res.Body)?.achievements);
                    return Snapshot();
                }
                if (!(res.StatusCode == 404 && !res.IsNetworkError)) return Snapshot();
            }

            // Not signed in, or an older server: definitions only.
            Fill(await GetDefinitions(), keepUnlockState: true);
            return Snapshot();
        }

        /// <summary>True if the player has this achievement (from the last <see cref="GetAll"/> or unlock).</summary>
        public bool IsUnlocked(string achievementSlug) =>
            achievementSlug != null && cache.TryGetValue(achievementSlug, out var a) && a.unlocked;

        /// <summary>The cached definition for a slug, if loaded.</summary>
        public AchievementInfo Find(string achievementSlug) =>
            achievementSlug != null && cache.TryGetValue(achievementSlug, out var a) ? a : null;

        /// <summary>This game's achievement definitions (no unlock state).</summary>
        public async Task<AchievementInfo[]> GetDefinitions()
        {
            var res = await Net.Get($"/api/games/{Json.Q(CoolAPI.GameSlug)}");
            if (!res.Success) return Array.Empty<AchievementInfo>();
            return Json.Parse<Dto.AchievementList>(res.Body)?.achievements ?? Array.Empty<AchievementInfo>();
        }

        /// <summary>Raw JSON of the game detail payload. Prefer <see cref="GetAll"/>.</summary>
        public async Task<string> List()
        {
            var res = await Net.Get($"/api/games/{Json.Q(CoolAPI.GameSlug)}");
            return res.Success ? res.Body : null;
        }

        // ---------------------------------------------------------------

        internal void OnQueuedUnlockSynced(string slug, string body)
        {
            MarkUnlocked(slug);
        }

        private void Fill(AchievementInfo[] list, bool keepUnlockState = false)
        {
            if (list == null) return;
            foreach (var a in list)
            {
                if (a == null || string.IsNullOrEmpty(a.slug)) continue;
                if (keepUnlockState && cache.TryGetValue(a.slug, out var old)) { a.unlocked = old.unlocked; a.unlocked_at = old.unlocked_at; }
                cache[a.slug] = a;
            }
            IsLoaded = true;
        }

        private AchievementInfo[] Snapshot()
        {
            var arr = new AchievementInfo[cache.Count];
            cache.Values.CopyTo(arr, 0);
            Array.Sort(arr, (x, y) => x.points.CompareTo(y.points));
            return arr;
        }

        private void MarkUnlocked(string slug)
        {
            if (!cache.TryGetValue(slug, out var a))
            {
                a = new AchievementInfo { slug = slug, name = slug };
                cache[slug] = a;
            }
            if (!a.unlocked)
            {
                a.unlocked = true;
                a.unlocked_at = DateTime.UtcNow.ToString("o");
            }
        }

        private void Announce(string slug)
        {
            if (!announced.Add(slug)) return;
            var info = Find(slug) ?? new AchievementInfo { slug = slug, name = slug };
            Log.Info($"Achievement unlocked: {info.name}");
            try { OnAchievementUnlocked?.Invoke(slug); } catch (Exception e) { Debug.LogException(e); }
            try { Unlocked?.Invoke(info); } catch (Exception e) { Debug.LogException(e); }
        }
    }
}
