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

using System.Collections.Generic;
using System.Reflection;
using PlayFab;
using PlayFab.ClientModels;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

namespace EzAdmin.Client
{

    public sealed class EzAdminItemUnlock : MonoBehaviour, IPointerClickHandler
    {
        [EzAdminGroup("Item")]
        [Tooltip("Catalog Item Id or display name. Case does not matter.")]
        [SerializeField] private string _item = "";

        [Tooltip("A nice name to show in text labels. Leave empty to use the item id above.")]
        [SerializeField] private string _displayName = "";

        [EzAdminGroup("Buying")]
        [Tooltip("Let the player buy this item. The price comes from the item in your PlayFab catalog.")]
        [SerializeField] private bool _canBuy = false;

        [Tooltip("Buy when a hand or object touches this. This object needs a Collider set to Is Trigger.")]
        [SerializeField] private bool _buyOnTouch = true;

        [Tooltip("Only something with this tag can buy it by touch, for example your hand tag. Empty means anything.")]
        [SerializeField] private string _buyTag = "";

        [Tooltip("Buy when the player clicks this with a mouse or controller. This object needs a Collider.")]
        [SerializeField] private bool _buyOnClick = true;

        [Tooltip("Seconds before a touch can buy again, so a resting hand does not buy over and over.")]
        [SerializeField, Range(0.2f, 5f)] private float _rearmSeconds = 1f;

        [Tooltip("Which virtual currency to spend when the item has more than one price. Empty uses the first one.")]
        [SerializeField] private string _currency = "";

        [EzAdminGroup("Turn objects on or off")]
        [Tooltip("Turned ON when they own the item, OFF when they do not.")]
        [SerializeField] private GameObject[] _showWhenOwned = new GameObject[0];

        [Tooltip("Turned OFF when they own the item, e.g. a lock icon or a Buy button.")]
        [SerializeField] private GameObject[] _hideWhenOwned = new GameObject[0];

        [Tooltip("Hide the owned things until the items have loaded, so nobody sees a flash of a hat they lack.")]
        [SerializeField] private bool _hideUntilLoaded = true;

        [EzAdminGroup("Text labels")]
        [Tooltip("Any text objects to fill in: TextMeshPro UI, a 3D TextMeshPro, or a plain UI Text. " +
                 "The right one is found for you.")]
        [SerializeField] private GameObject[] _labels = new GameObject[0];

        [Tooltip("No labels dragged in above? Fill the first text found on this object or its children.")]
        [SerializeField] private bool _findLabel = true;

        [Tooltip("What the labels say when the player owns it. {owned} {buy} {name} {status} {price} {currency} are filled in.")]
        [SerializeField] private string _ownedText = "{owned}: {name}";

        [Tooltip("What the labels say when the player does not own it. {buy} {name} {status} {price} {currency} are filled in.")]
        [SerializeField] private string _notOwnedText = "{buy}: {name}\n{price} {currency}";

        [Tooltip("What the labels say while the items are still loading.")]
        [SerializeField] private string _loadingText = "{name}";

        [Tooltip("The word {buy} becomes: the call to action before buying, for example Purchase or Buy.")]
        [SerializeField] private string _buyWord = "Purchase";

        [Tooltip("The word {owned} becomes once bought, for example Owned or Equipped.")]
        [SerializeField] private string _ownedLabel = "Owned";

        [Tooltip("The word {status} becomes once bought.")]
        [SerializeField] private string _ownedWord = "Owned";

        [Tooltip("The word {status} becomes before buying.")]
        [SerializeField] private string _notOwnedWord = "Locked";

        [EzAdminGroup("Events - hook your own code here")]
        [SerializeField] private UnityEvent _onOwned = new UnityEvent();
        [SerializeField] private UnityEvent _onNotOwned = new UnityEvent();
        [Tooltip("Bought just now. Wire a sound, a particle, a haptic pulse.")]
        [SerializeField] private UnityEvent _onPurchased = new UnityEvent();
        [Tooltip("A purchase did not go through. The reason is passed.")]
        [SerializeField] private StringEvent _onFailed = new StringEvent();

        [System.Serializable] public class StringEvent : UnityEvent<string> { }

        private int _last = -1;
        private bool _busy;
        private float _nextBuy;
        private static readonly Dictionary<System.Type, PropertyInfo> _textProps =
            new Dictionary<System.Type, PropertyInfo>();

        public UnityEvent OnPurchased => _onPurchased;
        public UnityEvent<string> OnFailed => _onFailed;

        public string Item
        {
            get { return _item; }
            set { _item = value; _last = -1; Apply(); }
        }

        public string DisplayName
        {
            get { return string.IsNullOrEmpty(_displayName) ? _item : _displayName; }
            set { _displayName = value; _last = -1; Apply(); }
        }

        public bool Owned => EzAdminItems.Has(_item);

        private void OnValidate()
        {
            Upgrade();
        }

        private void Upgrade()
        {
            if (_ownedText == "{name}\n{status}" || _ownedText == "{name}" || string.IsNullOrEmpty(_ownedText))
                _ownedText = "{owned}: {name}";
            if (_notOwnedText == "{name}" || string.IsNullOrEmpty(_notOwnedText))
                _notOwnedText = "{buy}: {name}\n{price} {currency}";
            if (_ownedWord == "Purchased") _ownedWord = "Owned";
            if (string.IsNullOrEmpty(_ownedLabel)) _ownedLabel = "Owned";
            if (string.IsNullOrEmpty(_buyWord)) _buyWord = "Purchase";
        }

        private GameObject[] Labels()
        {
            if (_labels != null && _labels.Length > 0) return _labels;
            if (!_findLabel) return _labels;
            if (_foundLabel == null) _foundLabel = FindText();
            return _foundLabel == null ? _labels : new[] { _foundLabel };
        }

        private GameObject _foundLabel;

        private GameObject FindText()
        {
            foreach (var t in GetComponentsInChildren<Component>(true))
            {
                if (t == null || t is Transform) continue;
                if (t is UnityEngine.UI.Text || TextProperty(t.GetType()) != null) return t.gameObject;
            }
            return null;
        }

        private void OnEnable()
        {
            Upgrade();
            EzAdminItems.Changed += Apply;
            if (_hideUntilLoaded && !EzAdminItems.Loaded)
            {
                Set(_showWhenOwned, false);
            }
            EzAdminItems.Start();
            if (_canBuy)
            {
                EnsureTouch();
                EnsurePrices();
                if (_buyOnClick) EnsureClicking();
            }
            Apply();
        }

        private void EnsureTouch()
        {
            if (!_buyOnTouch) return;
            var col = GetComponent<Collider>();
            if (col == null)
            {
                Debug.LogWarning("EzAdminItemUnlock on '" + name + "' has Buy On Touch set but no Collider. "
                                 + "Add one and tick Is Trigger.", this);
                return;
            }
            if (!col.isTrigger)
                Debug.LogWarning("EzAdminItemUnlock on '" + name + "': tick Is Trigger on the Collider so a hand "
                                 + "can touch through it.", this);
            if (GetComponent<Rigidbody>() == null)
            {
                var body = gameObject.AddComponent<Rigidbody>();
                body.isKinematic = true;
                body.useGravity = false;
            }
        }

        private void OnDisable()
        {
            EzAdminItems.Changed -= Apply;
        }

        private void Apply()
        {
            if (this == null) return;
            if (_canBuy) EnsurePrices();
            bool loaded = EzAdminItems.Loaded;
            bool owned = loaded && EzAdminItems.Has(_item);
            if (loaded)
            {
                Set(_showWhenOwned, owned);
                Set(_hideWhenOwned, !owned);
            }
            WriteLabels(owned, loaded);
            if (!loaded) return;
            int now = owned ? 1 : 0;
            if (now == _last) return;
            _last = now;
            if (owned) _onOwned.Invoke(); else _onNotOwned.Invoke();
        }

        public void Buy()
        {
            if (_busy || !_canBuy) return;
            if (EzAdminItems.Loaded && EzAdminItems.Has(_item)) return;
            if (!PlayFabClientAPI.IsClientLoggedIn())
            {
                Fail("Not signed in yet. Try again in a moment.");
                return;
            }
            string itemId, currency;
            int price;
            if (!Resolve(out itemId, out currency, out price))
            {
                EnsurePrices();
                Fail("Still loading the price. Try again in a moment.");
                return;
            }
            _busy = true;
            PlayFabClientAPI.PurchaseItem(new PurchaseItemRequest
            {
                CatalogVersion = string.IsNullOrEmpty(Version) ? null : Version,
                ItemId = itemId,
                VirtualCurrency = currency,
                Price = price
            }, result =>
            {
                if (this == null) return;
                _busy = false;
                var client = EzAdminClient.Instance;
                if (client != null && result.Items != null && result.Items.Count > 0
                    && !string.IsNullOrEmpty(result.Items[0].ItemInstanceId))
                    client.ReportPurchase(result.Items[0].ItemInstanceId);
                _onPurchased.Invoke();
                EzAdminItems.Refresh();
            }, error =>
            {
                if (this == null) return;
                _busy = false;
                Fail(Friendly(error));
            });
        }

        private void Fail(string message)
        {
            _onFailed.Invoke(message);
        }

        private static string Friendly(PlayFabError error)
        {
            if (error == null) return "That did not work.";
            switch (error.Error)
            {
                case PlayFabErrorCode.InsufficientFunds: return "Not enough currency for that.";
                case PlayFabErrorCode.ItemNotFound:
                case PlayFabErrorCode.CatalogItemIdInvalid: return "That item is not in your PlayFab catalog.";
                case PlayFabErrorCode.NotAuthenticated: return "Not signed in yet. Try again in a moment.";
                default: return string.IsNullOrEmpty(error.ErrorMessage) ? "That did not work." : error.ErrorMessage;
            }
        }

        private void OnTriggerEnter(Collider other)
        {
            if (!_canBuy || !_buyOnTouch || other == null) return;
            if (!string.IsNullOrEmpty(_buyTag) && !other.CompareTag(_buyTag)) return;
            TryBuy();
        }

        public void OnPointerClick(PointerEventData eventData)
        {
            if (_canBuy && _buyOnClick) TryBuy();
        }

        private void OnMouseUpAsButton()
        {
            if (_canBuy && _buyOnClick) TryBuy();
        }

        private void TryBuy()
        {
            if (Time.unscaledTime < _nextBuy) return;
            _nextBuy = Time.unscaledTime + _rearmSeconds;
            Buy();
        }

        private void EnsureClicking()
        {
            var graphic = GetComponent<UnityEngine.UI.Graphic>();
            if (graphic == null)
            {
                var cam = Camera.main;
                if (cam != null && cam.GetComponent<PhysicsRaycaster>() == null
                    && cam.GetComponent<Physics2DRaycaster>() == null)
                    cam.gameObject.AddComponent<PhysicsRaycaster>();
            }
            if (EventSystem.current != null || FindObjectOfType<EventSystem>() != null) return;
            var go = new GameObject("EventSystem", typeof(EventSystem));
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
            var module = System.Type.GetType("UnityEngine.InputSystem.UI.InputSystemUIInputModule, Unity.InputSystem");
            if (module != null) go.AddComponent(module);
#else
            go.AddComponent<StandaloneInputModule>();
#endif
        }

        private void WriteLabels(bool owned, bool loaded)
        {
            var labels = Labels();
            if (labels == null || labels.Length == 0) return;
            string template = !loaded ? _loadingText : owned ? _ownedText : _notOwnedText;
            string text = Fill(template, owned).Trim();
            foreach (var go in labels)
                if (go != null) SetText(go, text);
        }

        private string Fill(string template, bool owned)
        {
            if (template == null) return "";
            string currency = _currency;
            string priceText = "";
            string resolvedId, foundCurrency;
            int price;
            if (Resolve(out resolvedId, out foundCurrency, out price))
            {
                currency = foundCurrency;
                priceText = price.ToString();
            }
            if (string.IsNullOrEmpty(priceText))
                template = template.Replace("{price} {currency}", "").Replace("{price}", "");
            return template
                .Replace("{owned}", _ownedLabel ?? "")
                .Replace("{buy}", _buyWord ?? "")
                .Replace("{name}", DisplayName)
                .Replace("{item}", _item)
                .Replace("{status}", owned ? _ownedWord : _notOwnedWord)
                .Replace("{price}", priceText)
                .Replace("{currency}", currency ?? "");
        }

        private sealed class Entry
        {
            public string itemId;
            public Dictionary<string, int> prices;
        }

        private static readonly Dictionary<string, Dictionary<string, Entry>> _catalog =
            new Dictionary<string, Dictionary<string, Entry>>();
        private static readonly Dictionary<string, List<System.Action>> _waiting =
            new Dictionary<string, List<System.Action>>();

        private static string Version => EzAdminClient.CatalogVersion ?? "";
        private string Key => Version;

        private void EnsurePrices()
        {
            string key = Key;
            if (_catalog.ContainsKey(key)) return;
            if (!PlayFabClientAPI.IsClientLoggedIn()) return;
            List<System.Action> list;
            if (_waiting.TryGetValue(key, out list))
            {
                list.Add(ApplyIfAlive);
                return;
            }
            _waiting[key] = new List<System.Action> { ApplyIfAlive };
            PlayFabClientAPI.GetCatalogItems(new GetCatalogItemsRequest
            {
                CatalogVersion = string.IsNullOrEmpty(Version) ? null : Version
            }, result =>
            {
                var map = new Dictionary<string, Entry>(System.StringComparer.OrdinalIgnoreCase);
                if (result.Catalog != null)
                    foreach (var ci in result.Catalog)
                    {
                        if (string.IsNullOrEmpty(ci.ItemId)) continue;
                        var prices = new Dictionary<string, int>();
                        if (ci.VirtualCurrencyPrices != null)
                            foreach (var p in ci.VirtualCurrencyPrices) prices[p.Key] = (int)p.Value;
                        var entry = new Entry { itemId = ci.ItemId, prices = prices };
                        map[ci.ItemId] = entry;
                        if (!string.IsNullOrEmpty(ci.DisplayName)) map[ci.DisplayName] = entry;
                    }
                _catalog[key] = map;
                Flush(key);
            }, error =>
            {
                _catalog[key] = new Dictionary<string, Entry>();
                Flush(key);
            });
        }

        private static void Flush(string key)
        {
            List<System.Action> list;
            if (!_waiting.TryGetValue(key, out list)) return;
            _waiting.Remove(key);
            foreach (var cb in list) cb();
        }

        private void ApplyIfAlive()
        {
            if (this != null) Apply();
        }

        private bool Resolve(out string itemId, out string currency, out int price)
        {
            itemId = _item;
            currency = _currency;
            price = 0;
            Dictionary<string, Entry> map;
            if (!_catalog.TryGetValue(Key, out map)) return false;
            Entry entry;
            if (string.IsNullOrEmpty(_item) || !map.TryGetValue(_item, out entry)) return false;
            itemId = entry.itemId;
            if (entry.prices == null || entry.prices.Count == 0) return false;
            int found;
            if (!string.IsNullOrEmpty(_currency) && entry.prices.TryGetValue(_currency, out found))
            {
                price = found;
                return true;
            }
            foreach (var pair in entry.prices)
            {
                if (pair.Key == "RM") continue;
                currency = pair.Key;
                price = pair.Value;
                return true;
            }
            return false;
        }

        private static void SetText(GameObject go, string value)
        {
            var legacy = go.GetComponent<UnityEngine.UI.Text>();
            if (legacy != null)
            {
                if (legacy.text != value) legacy.text = value;
                return;
            }
            foreach (var component in go.GetComponents<Component>())
            {
                if (component == null || component is Transform) continue;
                var prop = TextProperty(component.GetType());
                if (prop == null) continue;
                if ((prop.GetValue(component) as string) != value) prop.SetValue(component, value);
                return;
            }
        }

        private static PropertyInfo TextProperty(System.Type type)
        {
            PropertyInfo cached;
            if (_textProps.TryGetValue(type, out cached)) return cached;
            PropertyInfo found = null;
            var prop = type.GetProperty("text", BindingFlags.Public | BindingFlags.Instance,
                                        null, typeof(string), System.Type.EmptyTypes, null);
            if (prop != null && prop.CanRead && prop.CanWrite) found = prop;
            _textProps[type] = found;
            return found;
        }

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