using System;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

namespace CoolGameX
{
    /// <summary>
    /// The player's session. When launched from Cool GameX the game gets a
    /// game-scoped token that the SDK refreshes automatically, so sessions don't
    /// expire mid-play.
    /// </summary>
    public class Auth
    {
        [Serializable] private class JwtPayload { public string sub; public string typ; public string slug; public long exp; }

        /// <summary>The verified player, once signed in.</summary>
        public PlayerProfile Player { get; private set; }

        /// <summary>True for a launcher/editor session token (refreshable).</summary>
        public bool IsGameToken { get; private set; }

        /// <summary>When the current token expires (UTC), if known.</summary>
        public DateTime? ExpiresAtUtc { get; private set; }

        /// <summary>Time left on the current token, if known.</summary>
        public TimeSpan? TimeLeft => ExpiresAtUtc.HasValue ? ExpiresAtUtc.Value - DateTime.UtcNow : (TimeSpan?)null;

        private Task<bool> refreshing;

        /// <summary>
        /// Use <paramref name="token"/> and verify it with the server. Returns false only
        /// when the server rejects it — if the device is offline the token is kept so
        /// progress can sync later.
        /// </summary>
        public async Task<bool> LoginWithToken(string token)
        {
            if (string.IsNullOrEmpty(token))
            {
                Log.Warn("LoginWithToken called with an empty token.");
                return false;
            }

            CoolAPI.Token = token;
            Inspect(token);

            var res = await Net.Get("/api/sdk/me", auth: true);

            // Server older than SDK 2.0 — validate the 1.x way.
            if (res.StatusCode == 404 && !res.IsNetworkError)
            {
                var legacy = await Net.Get("/api/library", auth: true);
                if (!legacy.Success) return Reject(legacy);
                SetPlayer(new PlayerProfile { username = "Player", tokenType = IsGameToken ? "game" : "account" });
                return true;
            }

            if (res.IsNetworkError)
            {
                Log.Warn("Offline — keeping your session and syncing when the connection returns.");
                return true;
            }

            if (!res.Success) return Reject(res);

            SetPlayer(Json.Parse<PlayerProfile>(res.Body) ?? new PlayerProfile { username = "Player" });
            return true;
        }

        private bool Reject(ApiResult res)
        {
            if (res.StatusCode == 401 || res.StatusCode == 403)
            {
                Log.Warn($"Session rejected by the server ({res.Error}). " +
                         (Application.isEditor ? "Sign in again from Tools > Cool GameX > Setup." : "Relaunch from Cool GameX."));
                Logout();
                return false;
            }
            Log.Warn($"Couldn't verify the session right now ({res.Error}) — will keep trying.");
            return true;
        }

        internal void SetPlayer(PlayerProfile profile)
        {
            Player = profile;
            if (profile != null && !string.IsNullOrEmpty(profile.id))
            {
                Log.Info($"Signed in as {profile.username}.");
                CoolAPI.RaiseSignedIn(profile);
            }
        }

        /// <summary>
        /// Swap the current game token for a fresh one. Concurrent callers share one
        /// request. Returns false for account tokens (they can't be refreshed).
        /// </summary>
        public Task<bool> Refresh()
        {
            if (refreshing != null && !refreshing.IsCompleted) return refreshing;
            refreshing = DoRefresh();
            return refreshing;
        }

        private async Task<bool> DoRefresh()
        {
            if (!IsGameToken || string.IsNullOrEmpty(CoolAPI.Token)) return false;
            var res = await Net.Raw("POST", "/api/sdk/refresh", "{}", auth: true);
            if (!res.Success)
            {
                if (!res.IsNetworkError) Log.Warn($"Couldn't refresh the session: {res.Error}");
                return false;
            }
            var r = Json.Parse<Dto.Refresh>(res.Body);
            if (r == null || string.IsNullOrEmpty(r.token)) return false;
            CoolAPI.Token = r.token;
            Inspect(r.token);
            EditorSession.UpdateToken(r.token, r.expiresAt);
            Log.Verbose("Session refreshed.");
            return true;
        }

        /// <summary>Refresh if the token expires within <paramref name="margin"/>.</summary>
        public async Task<bool> RefreshIfNeeded(TimeSpan margin)
        {
            if (!IsGameToken || !ExpiresAtUtc.HasValue) return true;
            if (ExpiresAtUtc.Value - DateTime.UtcNow > margin) return true;
            return await Refresh();
        }

        /// <summary>Forget the session.</summary>
        public void Logout()
        {
            CoolAPI.Token = null;
            Player = null;
            IsGameToken = false;
            ExpiresAtUtc = null;
        }

        /// <summary>Read type and expiry from the token (no verification — the server does that).</summary>
        internal void Inspect(string token)
        {
            IsGameToken = false;
            ExpiresAtUtc = null;
            var p = Decode(token);
            if (p == null) return;
            IsGameToken = p.typ == "cgx-game";
            if (p.exp > 0) ExpiresAtUtc = DateTimeOffset.FromUnixTimeSeconds(p.exp).UtcDateTime;
        }

        private static JwtPayload Decode(string token)
        {
            try
            {
                var parts = token.Split('.');
                if (parts.Length != 3) return null;
                string b64 = parts[1].Replace('-', '+').Replace('_', '/');
                b64 = b64.PadRight(b64.Length + (4 - b64.Length % 4) % 4, '=');
                return JsonUtility.FromJson<JwtPayload>(Encoding.UTF8.GetString(Convert.FromBase64String(b64)));
            }
            catch { return null; }
        }
    }

    /// <summary>The signed-in player's public profile.</summary>
    public class Player
    {
        /// <summary>The player, if signed in.</summary>
        public PlayerProfile Current => CoolAPI.Auth?.Player;

        /// <summary>Display name ("Player" when unknown).</summary>
        public string Name => string.IsNullOrEmpty(Current?.username) ? "Player" : Current.username;

        /// <summary>Fetch (or return the cached) profile.</summary>
        public async Task<PlayerProfile> GetProfile(bool refresh = false)
        {
            if (!refresh && !string.IsNullOrEmpty(Current?.id)) return Current;
            var res = await Net.Get("/api/sdk/me", auth: true);
            if (!res.Success) return Current;
            var p = Json.Parse<PlayerProfile>(res.Body);
            if (p != null) CoolAPI.Auth.SetPlayer(p);
            return p ?? Current;
        }
    }

    /// <summary>Does the player own this game? Useful for demo vs. full-game content.</summary>
    public class Ownership
    {
        /// <summary>The last known answer, or null if never checked.</summary>
        public bool? Cached { get; private set; }

        /// <summary>
        /// True if the game is in the player's library. Cached after the first call;
        /// on network errors returns the last known answer (false if unknown).
        /// </summary>
        public async Task<bool> OwnsGame(bool refresh = false)
        {
            if (!refresh && Cached.HasValue) return Cached.Value;
            var res = await Net.Get($"/api/sdk/ownership?gameSlug={Json.Q(CoolAPI.GameSlug)}", auth: true);
            if (!res.Success) return Cached ?? false;
            var p = Json.Parse<Dto.Ownership>(res.Body);
            Cached = p != null && p.owns;
            if (p?.dlc != null) CoolAPI.DLC.SetCache(p.dlc);
            return Cached.Value;
        }
    }
}
