using System;
using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using MajacloudSdk.Internal;
using UnityEngine;

namespace MajacloudSdk
{
    public enum MajacloudLinkStatus { Pending, Completed, Consumed, Expired, Cancelled }

    public sealed class MajacloudLinkOptions
    {
        /// <summary>Where the web page sends the player afterwards: an https URL on your MAJACLOUD host or a custom scheme allowlisted in the dashboard (e.g. mygame://auth).</summary>
        public string ReturnUrl;
        public float PollIntervalSeconds = 2f;
        /// <summary>Open the browser automatically. On WebGL call <see cref="MajacloudLinkFlow.OpenInBrowser"/> yourself inside a click handler.</summary>
        public bool OpenBrowser = true;
        /// <summary>Custom opener (e.g. an in-app browser). Defaults to Application.OpenURL.</summary>
        public Action<string> OpenUrl;
    }

    /// <summary>A running web sign-in. Open the URL, then await completion.</summary>
    public sealed class MajacloudLinkFlow
    {
        private readonly MajacloudAuth _auth;
        private readonly string _flowToken;
        private readonly string _verifier;
        private readonly MajacloudLinkOptions _options;
        private readonly CancellationTokenSource _cancel = new CancellationTokenSource();

        public string FlowId { get; }
        public string AuthUrl { get; }
        public DateTime ExpiresAt { get; }
        public MajacloudLinkStatus Status { get; private set; } = MajacloudLinkStatus.Pending;

        internal MajacloudLinkFlow(MajacloudAuth auth, string flowId, string flowToken, string authUrl, DateTime expiresAt, string verifier, MajacloudLinkOptions options)
        {
            _auth = auth;
            FlowId = flowId;
            _flowToken = flowToken;
            AuthUrl = authUrl;
            ExpiresAt = expiresAt;
            _verifier = verifier;
            _options = options;
        }

        public void OpenInBrowser()
        {
            if (_options.OpenUrl != null) _options.OpenUrl(AuthUrl);
            else Application.OpenURL(AuthUrl);
        }

        public void Cancel() => _cancel.Cancel();

        /// <summary>Poll until the player finishes on the web, then exchange for a session.</summary>
        public async Task<MajacloudResult<MajacloudPlayer>> WaitForCompletionAsync(CancellationToken cancellationToken = default)
        {
            using (var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancel.Token))
            {
                var token = linked.Token;
                while (true)
                {
                    if (token.IsCancellationRequested)
                    {
                        Status = MajacloudLinkStatus.Cancelled;
                        return MajacloudResult<MajacloudPlayer>.Fail(MajacloudErrorCode.LinkCancelled, "Sign-in was cancelled.");
                    }
                    if (DateTime.UtcNow > ExpiresAt.AddMinutes(1))
                    {
                        Status = MajacloudLinkStatus.Expired;
                        return MajacloudResult<MajacloudPlayer>.Fail(MajacloudErrorCode.LinkExpired, "The sign-in page expired. Start again.");
                    }
                    var status = await _auth.Http.SendJsonAsync(new HttpRequest
                    {
                        Method = "GET",
                        Path = MajacloudEndpoints.AuthLinkStatus,
                        Query = MajacloudHttp.Query(new System.Collections.Generic.KeyValuePair<string, string>("flow_id", FlowId), new System.Collections.Generic.KeyValuePair<string, string>("flow_token", _flowToken)),
                        Idempotent = true,
                    }, token);
                    if (!status.Ok)
                    {
                        if (status.Error.Code == MajacloudErrorCode.NotFound)
                        {
                            Status = MajacloudLinkStatus.Expired;
                            return MajacloudResult<MajacloudPlayer>.Fail(MajacloudErrorCode.LinkExpired, "The sign-in page expired. Start again.");
                        }
                        if (!status.Error.IsRetryable) return MajacloudResult<MajacloudPlayer>.Fail(status.Error);
                    }
                    else
                    {
                        var state = status.Value.GetString("status") ?? "pending";
                        if (state == "completed")
                        {
                            var exchange = await _auth.ExchangeLinkAsync(FlowId, status.Value.GetString("exchange_code"), _verifier, token);
                            Status = exchange.Ok ? MajacloudLinkStatus.Consumed : MajacloudLinkStatus.Expired;
                            return exchange;
                        }
                        if (state == "expired" || state == "consumed")
                        {
                            Status = MajacloudLinkStatus.Expired;
                            return MajacloudResult<MajacloudPlayer>.Fail(MajacloudErrorCode.LinkExpired, "The sign-in page expired. Start again.");
                        }
                    }
                    try
                    {
                        await MajacloudTask.Delay(Mathf.Max(1f, _options.PollIntervalSeconds), token);
                    }
                    catch (TaskCanceledException)
                    {
                        Status = MajacloudLinkStatus.Cancelled;
                        return MajacloudResult<MajacloudPlayer>.Fail(MajacloudErrorCode.LinkCancelled, "Sign-in was cancelled.");
                    }
                }
            }
        }
    }

    /// <summary>Player identity: guest sessions, token refresh, web account linking, sign-out.</summary>
    public sealed class MajacloudAuth
    {
        private readonly SessionStore _store;
        private readonly MajacloudLog _log;
        private SessionData _session;
        private Task<MajacloudResult<string>> _refreshInFlight;

        internal MajacloudHttp Http { get; }

        public MajacloudPlayer CurrentPlayer { get; private set; }
        public bool IsSignedIn => _session != null && CurrentPlayer != null && !_session.RefreshExpired;
        /// <summary>The current access token (for custom requests). May be expired; the SDK refreshes before its own calls.</summary>
        public string AccessToken => _session?.AccessToken;

        public event Action<MajacloudPlayer> PlayerChanged;
        public event Action SignedOut;
        /// <summary>The stored session can no longer be refreshed; call SignInAnonymouslyAsync again.</summary>
        public event Action SessionExpired;

        internal MajacloudAuth(MajacloudHttp http, SessionStore store, MajacloudLog log)
        {
            Http = http;
            _store = store;
            _log = log;
            _session = store.Load();
            if (_session != null && _session.RefreshExpired)
            {
                _session = null;
                store.Clear();
            }
            if (_session != null) CurrentPlayer = MajacloudPlayer.FromJson(_session.PlayerJson != null ? JsonNode.Parse(_session.PlayerJson) : null);
        }

        // --- Sign in ----------------------------------------------------------------

        /// <summary>Restore the stored session (refreshing if needed) or create a new guest.</summary>
        public async Task<MajacloudResult<MajacloudPlayer>> SignInAnonymouslyAsync(CancellationToken cancellationToken = default)
        {
            if (_session != null && !_session.RefreshExpired)
            {
                var token = await GetValidAccessTokenAsync(false, cancellationToken);
                if (token.Ok && CurrentPlayer != null) return MajacloudResult<MajacloudPlayer>.Success(CurrentPlayer);
                if (!token.Ok && token.Error.Code != MajacloudErrorCode.NotSignedIn && token.Error.Code != MajacloudErrorCode.Unauthorized) return MajacloudResult<MajacloudPlayer>.Fail(token.Error);
            }
            var result = await Http.SendJsonAsync(new HttpRequest
            {
                Method = "POST",
                Path = MajacloudEndpoints.AuthGuest,
                JsonBody = JsonNode.Object().Set("device_id", Majacloud.DeviceId).ToJson(),
                Auth = HttpAuth.ClientKey,
                Idempotent = false,
            }, cancellationToken);
            if (!result.Ok) return MajacloudResult<MajacloudPlayer>.Fail(result.Error);
            ApplySession(SessionData.FromResponse(result.Value));
            return MajacloudResult<MajacloudPlayer>.Success(CurrentPlayer);
        }

        public async Task<MajacloudResult<MajacloudPlayer>> RefreshAsync(CancellationToken cancellationToken = default)
        {
            var token = await GetValidAccessTokenAsync(true, cancellationToken);
            if (!token.Ok) return MajacloudResult<MajacloudPlayer>.Fail(token.Error);
            return MajacloudResult<MajacloudPlayer>.Success(CurrentPlayer);
        }

        public async Task<MajacloudResult<MajacloudPlayer>> GetMeAsync(CancellationToken cancellationToken = default)
        {
            var result = await Http.SendJsonAsync(new HttpRequest { Method = "GET", Path = MajacloudEndpoints.PlayersMe, Auth = HttpAuth.Bearer, Idempotent = true }, cancellationToken);
            if (!result.Ok) return MajacloudResult<MajacloudPlayer>.Fail(result.Error);
            UpdatePlayer(result.Value.GetObject("player"));
            return MajacloudResult<MajacloudPlayer>.Success(CurrentPlayer);
        }

        public async Task<MajacloudResult<MajacloudPlayer>> SetDisplayNameAsync(string displayName, CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(displayName)) return MajacloudResult<MajacloudPlayer>.Fail(MajacloudErrorCode.Validation, "Display name is required.");
            var result = await Http.SendJsonAsync(new HttpRequest { Method = "PATCH", Path = MajacloudEndpoints.PlayersMe, Auth = HttpAuth.Bearer, Idempotent = true, JsonBody = JsonNode.Object().Set("display_name", displayName.Trim()).ToJson() }, cancellationToken);
            if (!result.Ok) return MajacloudResult<MajacloudPlayer>.Fail(result.Error);
            UpdatePlayer(result.Value.GetObject("player"));
            return MajacloudResult<MajacloudPlayer>.Success(CurrentPlayer);
        }

        // --- Web link flow (PKCE) --------------------------------------------------------

        /// <summary>Start the web sign-in. Open <see cref="MajacloudLinkFlow.AuthUrl"/>, then await <see cref="MajacloudLinkFlow.WaitForCompletionAsync"/>.</summary>
        public async Task<MajacloudResult<MajacloudLinkFlow>> StartLinkAsync(MajacloudLinkOptions options = null, CancellationToken cancellationToken = default)
        {
            options = options ?? new MajacloudLinkOptions();
            var verifier = Pkce.CreateVerifier();
            var body = JsonNode.Object().Set("code_challenge", Pkce.CreateChallenge(verifier)).Set("code_challenge_method", "S256");
            if (!string.IsNullOrEmpty(options.ReturnUrl)) body.Set("return_url", options.ReturnUrl);
            var result = await Http.SendJsonAsync(new HttpRequest { Method = "POST", Path = MajacloudEndpoints.AuthLinkStart, Auth = HttpAuth.BearerIfAvailable, JsonBody = body.ToJson() }, cancellationToken);
            if (!result.Ok) return MajacloudResult<MajacloudLinkFlow>.Fail(result.Error);
            var json = result.Value;
            var flow = new MajacloudLinkFlow(this, json.GetString("flow_id"), json.GetString("flow_token"), json.GetString("auth_url"), json.GetDateTime("expires_at") ?? DateTime.UtcNow.AddMinutes(15), verifier, options);
            if (options.PollIntervalSeconds <= 0f) options.PollIntervalSeconds = 2f;
            var serverInterval = json.GetDouble("poll_interval_seconds", 0);
            if (serverInterval > options.PollIntervalSeconds) options.PollIntervalSeconds = (float)serverInterval;
            return MajacloudResult<MajacloudLinkFlow>.Success(flow);
        }

        /// <summary>Start, open the browser and wait. The guest's progress is merged into the linked account.</summary>
        public async Task<MajacloudResult<MajacloudPlayer>> LinkAccountAsync(MajacloudLinkOptions options = null, CancellationToken cancellationToken = default)
        {
            options = options ?? new MajacloudLinkOptions();
            var started = await StartLinkAsync(options, cancellationToken);
            if (!started.Ok) return MajacloudResult<MajacloudPlayer>.Fail(started.Error);
            if (options.OpenBrowser) started.Value.OpenInBrowser();
            return await started.Value.WaitForCompletionAsync(cancellationToken);
        }

        internal async Task<MajacloudResult<MajacloudPlayer>> ExchangeLinkAsync(string flowId, string exchangeCode, string verifier, CancellationToken cancellationToken)
        {
            var result = await Http.SendJsonAsync(new HttpRequest
            {
                Method = "POST",
                Path = MajacloudEndpoints.AuthLinkExchange,
                Auth = HttpAuth.ClientKey,
                JsonBody = JsonNode.Object().Set("flow_id", flowId).Set("exchange_code", exchangeCode).Set("code_verifier", verifier).ToJson(),
            }, cancellationToken);
            if (!result.Ok) return MajacloudResult<MajacloudPlayer>.Fail(result.Error);
            ApplySession(SessionData.FromResponse(result.Value));
            return MajacloudResult<MajacloudPlayer>.Success(CurrentPlayer);
        }

        // --- Sign out --------------------------------------------------------------------

        public async Task<MajacloudResult> SignOutAsync(CancellationToken cancellationToken = default)
        {
            var session = _session;
            ClearLocalSession();
            if (session != null)
            {
                var body = JsonNode.Object().Set("refresh_token", session.RefreshToken).ToJson();
                await Http.SendAsync(new HttpRequest { Method = "POST", Path = MajacloudEndpoints.AuthLogout, Auth = HttpAuth.ClientKey, JsonBody = body }, cancellationToken);
            }
            return MajacloudResult.Success();
        }

        /// <summary>Forget the stored session without calling the server.</summary>
        public void ClearLocalSession()
        {
            _session = null;
            CurrentPlayer = null;
            _store.Clear();
            SignedOut?.Invoke();
        }

        // --- Tokens ----------------------------------------------------------------------

        /// <summary>A usable access token, refreshing (single-flight) when it is about to expire or when forced.</summary>
        internal Task<MajacloudResult<string>> GetValidAccessTokenAsync(bool forceRefresh, CancellationToken cancellationToken)
        {
            if (_session == null) return Task.FromResult(MajacloudResult<string>.Fail(MajacloudErrorCode.NotSignedIn, "No player session. Call Majacloud.Auth.SignInAnonymouslyAsync() first."));
            if (!forceRefresh && !_session.AccessExpiresWithin(60f) && !string.IsNullOrEmpty(_session.AccessToken)) return Task.FromResult(MajacloudResult<string>.Success(_session.AccessToken));
            if (_refreshInFlight != null && !_refreshInFlight.IsCompleted) return _refreshInFlight;
            _refreshInFlight = RefreshInternalAsync(cancellationToken);
            return _refreshInFlight;
        }

        private async Task<MajacloudResult<string>> RefreshInternalAsync(CancellationToken cancellationToken)
        {
            var session = _session;
            if (session == null) return MajacloudResult<string>.Fail(MajacloudErrorCode.NotSignedIn, "No player session.");
            var result = await Http.SendJsonAsync(new HttpRequest
            {
                Method = "POST",
                Path = MajacloudEndpoints.AuthRefresh,
                Auth = HttpAuth.ClientKey,
                JsonBody = JsonNode.Object().Set("refresh_token", session.RefreshToken).ToJson(),
                Idempotent = false,
            }, cancellationToken);
            if (!result.Ok)
            {
                if (result.Error.Code == MajacloudErrorCode.Unauthorized)
                {
                    _log.Warning("Session can no longer be refreshed (" + result.Error.Message + ").");
                    _session = null;
                    CurrentPlayer = null;
                    _store.Clear();
                    SessionExpired?.Invoke();
                    return MajacloudResult<string>.Fail(MajacloudErrorCode.NotSignedIn, "Your session expired. Sign in again.");
                }
                return MajacloudResult<string>.Fail(result.Error);
            }
            var data = SessionData.FromResponse(result.Value);
            if (data.PlayerJson == null) data.PlayerJson = session.PlayerJson;
            data.AuthMethod = data.AuthMethod ?? session.AuthMethod;
            ApplySession(data, false);
            return MajacloudResult<string>.Success(data.AccessToken);
        }

        private void ApplySession(SessionData data, bool announce = true)
        {
            var previousId = CurrentPlayer?.Id;
            _session = data;
            _store.Save(data);
            CurrentPlayer = MajacloudPlayer.FromJson(data.PlayerJson != null ? JsonNode.Parse(data.PlayerJson) : null) ?? CurrentPlayer;
            if (announce || previousId != CurrentPlayer?.Id) PlayerChanged?.Invoke(CurrentPlayer);
        }

        private void UpdatePlayer(JsonNode playerNode)
        {
            var player = MajacloudPlayer.FromJson(playerNode);
            if (player == null) return;
            CurrentPlayer = player;
            if (_session != null)
            {
                _session.PlayerJson = playerNode.ToJson();
                _store.Save(_session);
            }
            PlayerChanged?.Invoke(player);
        }

        // --- Coroutine variants -------------------------------------------------------------

        public IEnumerator SignInAnonymously(Action<MajacloudResult<MajacloudPlayer>> callback) => MajacloudCoroutine.Run(SignInAnonymouslyAsync(), callback);
        public IEnumerator LinkAccount(Action<MajacloudResult<MajacloudPlayer>> callback, MajacloudLinkOptions options = null) => MajacloudCoroutine.Run(LinkAccountAsync(options), callback);
        public IEnumerator SignOut(Action<MajacloudResult> callback) => MajacloudCoroutine.Run(SignOutAsync(), callback);
        public IEnumerator SetDisplayName(string displayName, Action<MajacloudResult<MajacloudPlayer>> callback) => MajacloudCoroutine.Run(SetDisplayNameAsync(displayName), callback);
        public IEnumerator GetMe(Action<MajacloudResult<MajacloudPlayer>> callback) => MajacloudCoroutine.Run(GetMeAsync(), callback);
    }
}
