// Copyright (c) 2026 EzAdmin. All rights reserved.
// Licensed under the EzAdmin Client License. See LICENSE-CLIENT.txt.

using System;
using System.Collections;
using PlayFab;
using PlayFab.ClientModels;
using UnityEngine;
#if EZADMIN_META
using Oculus.Platform;
using Oculus.Platform.Models;
#endif

namespace EzAdmin.Client
{

    [DisallowMultipleComponent]
    public sealed class EzAdminAuth : MonoBehaviour
    {
        public enum Method { None, Meta, Steam, Xbox, PlayStation, Device }

        [EzAdminGroup("Meta Quest")]
        [Tooltip("Quit if the player does not own the app on the Meta store (piracy check). Builds on a headset only.")]
#pragma warning disable 0414
        [SerializeField] private bool _requireEntitlement = true;
#pragma warning restore 0414

        public const string DefaultMetaPrefix = "META_";

        [Tooltip("Put before the Meta user id to make the PlayFab account id. If players already signed in with " +
                 "Meta before, use the prefix your old login used so they keep their accounts.")]
        [SerializeField] private string _metaPrefix = DefaultMetaPrefix;
        [Tooltip("EzAdmin checks the player with Meta, then signs them in to PlayFab from its server. Nobody can " +
                 "sign in as another player. Needs your Meta app token on the EzAdmin dashboard (Connect games).")]
        [SerializeField] private bool _secureMeta = true;
        [Tooltip("Insecure, off on purpose. If EzAdmin is not set up for secure sign-in yet, sign in the old way " +
                 "instead of failing. The old way trusts the headset's word for who the player is, so a changed copy " +
                 "can sign in as someone else and get past a ban. Leave this off and set up the Meta app token on the " +
                 "EzAdmin dashboard (Connect games) so EzAdmin checks the player with Meta from its own server.")]
        [SerializeField] private bool _allowOldMetaLogin = false;

        [EzAdminGroup("Everything else")]
        [Tooltip("Where the device's random id is kept between launches. Use your old key to keep old accounts.")]
        [SerializeField] private string _savedIdKey = "EzAdmin.PlayerId";
        [Tooltip("Prefix for device accounts made by this script.")]
        [SerializeField] private string _devicePrefix = "DEVICE_";

        [EzAdminGroup("Behaviour")]
        [Tooltip("Start signing in as soon as the game starts.")]
        [SerializeField] private bool _signInOnStart = true;
        [Tooltip("Quit the game if the account is banned.")]
        [SerializeField] private bool _quitIfBanned = true;

        [EzAdminGroup("After sign-in")]
        [Tooltip("Turned on once the player is signed in.")]
        [SerializeField] private GameObject[] _objectsToEnable = new GameObject[0];
        [Tooltip("Turned off once the player is signed in. A loading screen, a Press to start sign.")]
        [SerializeField] private GameObject[] _objectsToDisable = new GameObject[0];

        [EzAdminGroup("Items (object name = PlayFab item id)")]
        [Tooltip("Each object is turned on only if the player owns the item with the same id as the object's name.")]
        [SerializeField] private GameObject[] _enableIfOwned = new GameObject[0];
        [Tooltip("Each object is turned off if the player owns the item with the same id as the object's name.")]
        [SerializeField] private GameObject[] _disableIfOwned = new GameObject[0];

        [EzAdminGroup("Staff")]
        [Tooltip("Only on for staff. Staff come from your Discord roles through EzAdmin, so nobody can fake it in game.")]
        [SerializeField] private GameObject[] _staffObjects = new GameObject[0];

        [EzAdminGroup("Ban and errors")]
        [Tooltip("Turned on when sign-in fails, the player is banned or kicked.")]
        [SerializeField] private GameObject[] _errorObjectsEnable = new GameObject[0];
        [Tooltip("Turned off at the same time.")]
        [SerializeField] private GameObject[] _errorObjectsDisable = new GameObject[0];
        [Tooltip("Text (UI Text or TextMeshPro) that shows the reason.")]
        [SerializeField] private Component[] _errorTexts = new Component[0];

        [EzAdminGroup("Keep between scenes")]
        [SerializeField] private GameObject[] _dontDestroyOnLoad = new GameObject[0];

        [EzAdminGroup("AFK")]
        [Tooltip("Seconds without moving before the player is removed. 0 turns it off.")]
        [SerializeField] private float _afkSeconds = 0f;

        public static Func<string> SteamTicket;

        public static Func<string> XboxToken;

        public static Func<string> PlayStationAuthCode;

        public static string PlayStationRedirectUri = "orbis://games";

        public static event Action<string, Method> SignedIn;

        public static event Action<string> Failed;

        public static event Action<DateTime> Banned;

        public static Method SignedInWith { get; private set; }

        public static string LastError { get; private set; }

        public static string MetaUserId { get; private set; } = "";
        public static string MetaProof { get; private set; } = "";

        private static bool _busy;

        private void Awake()
        {
            foreach (var go in _dontDestroyOnLoad)
                if (go != null && go.transform.parent == null) DontDestroyOnLoad(go);
            Toggle(_objectsToEnable, false);
            Toggle(_enableIfOwned, false);
            Toggle(_staffObjects, EzAdminClient.IsStaff);
            Toggle(_errorObjectsEnable, false);
        }

        private void OnEnable()
        {
            EzAdminClient.StaffChanged += OnStaff;
            EzAdminClient.Kicked += ShowError;
            EzAdminItems.Changed += ApplyItems;
        }

        private void OnDisable()
        {
            EzAdminClient.StaffChanged -= OnStaff;
            EzAdminClient.Kicked -= ShowError;
            EzAdminItems.Changed -= ApplyItems;
        }

        private void Start()
        {
            if (_signInOnStart) SignIn();
        }

        private UnityEngine.Vector3 _lastSpot;
        private float _stillSince;
        private bool _afkDone;

        private void Update()
        {
            if (_afkSeconds <= 0f || _afkDone || !PlayFabClientAPI.IsClientLoggedIn()) return;
            var cam = UnityEngine.Camera.main;
            if (cam == null) return;
            UnityEngine.Vector3 spot = cam.transform.position + cam.transform.forward * 0.5f;
            if ((spot - _lastSpot).sqrMagnitude > 0.0004f)
            {
                _lastSpot = spot;
                _stillSince = Time.unscaledTime;
                return;
            }
            if (Time.unscaledTime - _stillSince < _afkSeconds) return;
            _afkDone = true;
            ShowError("Removed for being idle.");
            QuitGame();
        }

        private void AfterSignIn()
        {
            Toggle(_errorObjectsEnable, false);
            Toggle(_errorObjectsDisable, true);
            Write(_errorTexts, "");
            Toggle(_objectsToEnable, true);
            Toggle(_objectsToDisable, false);
            _stillSince = Time.unscaledTime;
            EzAdminItems.Start();
            if (EzAdminItems.Loaded) ApplyItems();
        }

        private void ApplyItems()
        {
            if (this == null || !EzAdminItems.Loaded) return;
            foreach (var go in _enableIfOwned)
                if (go != null && go != gameObject) go.SetActive(EzAdminItems.Has(go.name));
            foreach (var go in _disableIfOwned)
                if (go != null && go != gameObject) go.SetActive(!EzAdminItems.Has(go.name));
        }

        private void OnStaff(bool staff)
        {
            Toggle(_staffObjects, staff);
        }

        private void ShowError(string message)
        {
            Toggle(_errorObjectsEnable, true);
            Toggle(_errorObjectsDisable, false);
            Write(_errorTexts, message ?? "");
        }

        private void Toggle(GameObject[] objects, bool active)
        {
            if (objects == null) return;
            foreach (var go in objects)
                if (go != null && go != gameObject && go.activeSelf != active) go.SetActive(active);
        }

        private static void Write(Component[] targets, string text)
        {
            if (targets == null) return;
            foreach (var target in targets)
            {
                if (target == null) continue;
                var property = target.GetType().GetProperty("text");
                if (property != null && property.CanWrite && property.PropertyType == typeof(string)) property.SetValue(target, text);
            }
        }

        public void SignIn()
        {
            if (_busy || PlayFabClientAPI.IsClientLoggedIn()) return;
            if (string.IsNullOrEmpty(PlayFabSettings.staticSettings.TitleId))
            {
                Fail("No PlayFab Title ID. Search the Project window for PlayFabSharedSettings and type it in.");
                return;
            }
            _busy = true;
            StartCoroutine(Run());
        }

        private IEnumerator Run()
        {
            Method method = Choose();
            float[] backoff = { 2f, 5f, 10f, 20f, 30f };
            for (int attempt = 0; ; attempt++)
            {
                bool done = false, retry = false;
                yield return StartCoroutine(Attempt(method, (ok, canRetry) => { done = ok; retry = canRetry; }));
                if (done || !retry) break;
                yield return new WaitForSecondsRealtime(backoff[Math.Min(attempt, backoff.Length - 1)]);
            }
            _busy = false;
        }

        private Method Choose()
        {
            if (XboxToken != null) return Method.Xbox;
            if (PlayStationAuthCode != null) return Method.PlayStation;
            if (SteamTicket != null) return Method.Steam;
#if EZADMIN_META && UNITY_ANDROID && !UNITY_EDITOR
            return Method.Meta;
#else
            return Method.Device;
#endif
        }

        private IEnumerator Attempt(Method method, Action<bool, bool> finished)
        {
            switch (method)
            {
                case Method.Meta:
                    yield return StartCoroutine(MetaIdentity());
                    if (string.IsNullOrEmpty(MetaUserId))
                    {
                        finished(false, LastError == null);
                        yield break;
                    }
                    if (_secureMeta && !string.IsNullOrEmpty(MetaProof))
                    {
                        string code = "";
                        yield return StartCoroutine(SecureMeta(finished, c => code = c));
                        if (code == null) yield break;
                        if (code != "meta_not_set" || !_allowOldMetaLogin)
                        {
                            finished(false, code == "retry");
                            yield break;
                        }
                        Debug.LogWarning("EzAdmin: secure Quest sign-in is not set up for this game yet, so the old " +
                                         "sign-in was used. Add the Meta app token on the EzAdmin dashboard (Connect games).", this);
                    }
                    yield return Login(method, cb => PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest
                    {
                        CustomId = _metaPrefix + MetaUserId,
                        CreateAccount = true,
                        InfoRequestParameters = Info()
                    }, cb, e => OnError(e, finished)), finished);
                    break;

                case Method.Steam:
                    yield return Login(method, cb => PlayFabClientAPI.LoginWithSteam(new LoginWithSteamRequest
                    {
                        SteamTicket = SteamTicket(),
                        CreateAccount = true,
                        InfoRequestParameters = Info()
                    }, cb, e => OnError(e, finished)), finished);
                    break;

                case Method.Xbox:
                    yield return Login(method, cb => PlayFabClientAPI.LoginWithXbox(new LoginWithXboxRequest
                    {
                        XboxToken = XboxToken(),
                        CreateAccount = true,
                        InfoRequestParameters = Info()
                    }, cb, e => OnError(e, finished)), finished);
                    break;

                case Method.PlayStation:
                    yield return Login(method, cb => PlayFabClientAPI.LoginWithPSN(new LoginWithPSNRequest
                    {
                        AuthCode = PlayStationAuthCode(),
                        RedirectUri = PlayStationRedirectUri,
                        CreateAccount = true,
                        InfoRequestParameters = Info()
                    }, cb, e => OnError(e, finished)), finished);
                    break;

                default:
                    yield return Login(Method.Device, cb => PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest
                    {
                        CustomId = DeviceId(),
                        CreateAccount = true,
                        InfoRequestParameters = Info()
                    }, cb, e => OnError(e, finished)), finished);
                    break;
            }
        }

        private IEnumerator Login(Method method, Action<Action<LoginResult>> call, Action<bool, bool> finished)
        {
            _answered = false;
            call(result => OnLoggedIn(result, method, finished));

            float until = Time.realtimeSinceStartup + 30f;
            while (!_answered && Time.realtimeSinceStartup < until) yield return null;
            if (!_answered)
            {
                _answered = true;
                finished(false, true);
            }
        }

        private bool _answered;

        public static bool SignedInSecurely { get; private set; }

        [Serializable]
        private class SecureRequest
        {
            public string titleId;
            public string userId;
            public string proof;
            public string legacyPrefix;
        }

        [Serializable]
        private class SecureReply
        {
            public bool ok;
            public string code;
            public string message;
            public string playFabId;
            public string sessionTicket;
            public string entityToken;
            public string entityId;
            public string entityType;
            public bool newlyCreated;
            public bool migrated;
            public string bannedUntil;
        }

        private IEnumerator SecureMeta(Action<bool, bool> finished, Action<string> report)
        {
            var body = new SecureRequest
            {
                titleId = PlayFabSettings.staticSettings.TitleId,
                userId = MetaUserId,
                proof = MetaProof,
                legacyPrefix = _metaPrefix ?? ""
            };
            string url = EzAdminClient.SiteAddress + "/unity/v1/auth/meta";
            string text = "";
            using (var request = new UnityEngine.Networking.UnityWebRequest(url, "POST"))
            {
                request.uploadHandler = new UnityEngine.Networking.UploadHandlerRaw(
                    System.Text.Encoding.UTF8.GetBytes(JsonUtility.ToJson(body)));
                request.downloadHandler = new UnityEngine.Networking.DownloadHandlerBuffer();
                request.SetRequestHeader("Content-Type", "application/json");
                request.timeout = 25;
                yield return request.SendWebRequest();
                text = request.downloadHandler != null ? request.downloadHandler.text : "";
            }
            SecureReply reply = null;
            try { reply = string.IsNullOrEmpty(text) ? null : JsonUtility.FromJson<SecureReply>(text); }
            catch (Exception) { reply = null; }
            if (reply == null)
            {
                LastError = null;
                Debug.LogWarning("EzAdmin: could not reach EzAdmin to sign in. Retrying.", this);
                report("retry");
                yield break;
            }
            if (!reply.ok)
            {
                if (reply.code == "meta_not_set")
                {
                    report("meta_not_set");
                    yield break;
                }
                if (reply.code == "banned")
                {
                    Banned?.Invoke(DateTime.MaxValue);
                    Fail("This account is banned.");
                    if (_quitIfBanned) QuitGame();
                    finished(false, false);
                    report(null);
                    yield break;
                }
                bool retry = reply.code == "meta_unreachable" || reply.code == "playfab_unreachable" || reply.code == "slow_down";
                Fail("Sign-in failed: " + reply.message);
                if (retry) LastError = null;
                report(retry ? "retry" : "failed");
                yield break;
            }

            PlayFabSettings.staticPlayer.ClientSessionTicket = reply.sessionTicket;
            PlayFabSettings.staticPlayer.PlayFabId = reply.playFabId;
            PlayFabSettings.staticPlayer.EntityToken = reply.entityToken;
            PlayFabSettings.staticPlayer.EntityId = reply.entityId;
            PlayFabSettings.staticPlayer.EntityType = reply.entityType;
            SignedInSecurely = true;
            LastError = null;
            SignedInWith = Method.Meta;
            if (Debug.isDebugBuild)
                Debug.Log("EzAdmin: signed in securely through EzAdmin" + (reply.migrated ? " (old account moved over)" : "") + ".");

            DateTime until;
            if (!string.IsNullOrEmpty(reply.bannedUntil) && DateTime.TryParse(reply.bannedUntil, out until) && until > DateTime.UtcNow)
            {
                Banned?.Invoke(until);
                Fail("This account is banned until " + until.ToString("u") + ".");
                if (_quitIfBanned) QuitGame();
                finished(false, false);
                report(null);
                yield break;
            }
            SignedIn?.Invoke(reply.playFabId, Method.Meta);
            AfterSignIn();
            finished(true, false);
            report(null);
        }

        private static GetPlayerCombinedInfoRequestParams Info()
        {
            return new GetPlayerCombinedInfoRequestParams { GetPlayerProfile = true };
        }

        private void OnLoggedIn(LoginResult result, Method method, Action<bool, bool> finished)
        {
            if (_answered) return;
            _answered = true;
            LastError = null;
            SignedInWith = method;
            if (Debug.isDebugBuild) Debug.Log("EzAdmin: signed in to PlayFab with " + method + " as " + result.PlayFabId);

            var bannedUntil = result.InfoResultPayload?.PlayerProfile?.BannedUntil;
            if (bannedUntil != null)
            {
                Banned?.Invoke(bannedUntil.Value);
                Fail("This account is banned until " + bannedUntil.Value.ToString("u") + ".");
                if (_quitIfBanned) QuitGame();
                finished(false, false);
                return;
            }
            SignedIn?.Invoke(result.PlayFabId, method);
            AfterSignIn();
            finished(true, false);
        }

        private void OnError(PlayFabError error, Action<bool, bool> finished)
        {
            if (_answered) return;
            _answered = true;
            string said = string.IsNullOrEmpty(error.ErrorMessage) ? error.Error.ToString() : error.ErrorMessage;
            if (error.Error == PlayFabErrorCode.AccountBanned)
            {
                Banned?.Invoke(DateTime.MaxValue);
                Fail("This account is banned.");
                if (_quitIfBanned) QuitGame();
                finished(false, false);
                return;
            }
            if (said.IndexOf("not authorized", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                Fail("PlayFab title " + PlayFabSettings.staticSettings.TitleId + " blocks game sign-ins (\"" + said + "\"). " +
                     "Its API rules deny client calls: in PlayFab Game Manager remove the rule that disables Client " +
                     "API access. No sign-in can work until then.");
                finished(false, false);
                return;
            }

            bool retry = error.Error == PlayFabErrorCode.ServiceUnavailable || error.Error == PlayFabErrorCode.ConnectionError
                         || error.HttpCode == 0 || error.HttpCode >= 500;
            Fail("PlayFab sign-in failed: " + said);
            if (retry) LastError = null;
            finished(false, retry);
        }

        private void Fail(string message)
        {
            ShowError(message);
            LastError = message;
            Debug.LogWarning("EzAdmin: " + message, this);
            Failed?.Invoke(message);
        }

        private string DeviceId()
        {

            string id = PlayerPrefs.GetString(_savedIdKey, "");
            if (string.IsNullOrEmpty(id))
            {
                id = _devicePrefix + Guid.NewGuid().ToString("N");
                PlayerPrefs.SetString(_savedIdKey, id);
                PlayerPrefs.Save();
            }
            return id;
        }

        private static void QuitGame()
        {
#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false;
#else
            UnityEngine.Application.Quit();
#endif
        }

#if EZADMIN_META
        private IEnumerator MetaIdentity()
        {
            MetaUserId = "";
            MetaProof = "";
            bool step = false, ok = false;

            Core.AsyncInitialize().OnComplete(msg => { ok = !msg.IsError; step = true; });
            while (!step) yield return null;
            if (!ok)
            {
                Fail("The Meta platform did not start. Check the App ID in Oculus › Platform › Edit Settings.");
                yield break;
            }

            if (_requireEntitlement)
            {
                step = false;
                Entitlements.IsUserEntitledToApplication().OnComplete(msg => { ok = !msg.IsError; step = true; });
                while (!step) yield return null;
                if (!ok)
                {
                    Fail("This copy of the game is not owned on the Meta store.");
                    QuitGame();
                    yield break;
                }
            }

            step = false;
            Users.GetLoggedInUser().OnComplete((Message<User> msg) =>
            {
                if (!msg.IsError && msg.Data != null) MetaUserId = msg.Data.ID.ToString();
                step = true;
            });
            while (!step) yield return null;
            if (string.IsNullOrEmpty(MetaUserId))
            {
                Fail("Could not read the Meta account. Is the headset signed in?");
                yield break;
            }

            step = false;
            Users.GetUserProof().OnComplete((Message<UserProof> msg) =>
            {
                if (!msg.IsError && msg.Data != null) MetaProof = msg.Data.Value;
                step = true;
            });
            while (!step) yield return null;
        }
#else
        private IEnumerator MetaIdentity()
        {
            Fail("Meta sign-in needs the Meta Platform SDK. Run the Setup Wizard to switch it on.");
            yield break;
        }
#endif
    }
}
