using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting;

namespace BilibiliMiniGame
{
    internal sealed partial class BilibiliMiniGameBridge : MonoBehaviour
    {
        private const string BridgeObjectName = "__BilibiliMiniGameSdkBridge_6F77A34C";
        private const int UnsupportedEnvironmentError = -1000;
        private const int RequestTimeoutError = -1003;

        private static BilibiliMiniGameBridge instance;
        private readonly Dictionary<string, PendingRequest> pendingRequests =
            new Dictionary<string, PendingRequest>();
        private readonly List<string> expiredRequestIds = new List<string>();

        internal static bool BL_Cancel(string requestId)
        {
            return instance != null && instance.pendingRequests.Remove(requestId);
        }

        internal static void BL_CancelOwnerRequests(BilibiliMiniGameSdk owner)
        {
            if (instance == null || ReferenceEquals(owner, null))
            {
                return;
            }

            instance.expiredRequestIds.Clear();
            foreach (KeyValuePair<string, PendingRequest> pair in instance.pendingRequests)
            {
                if (ReferenceEquals(pair.Value.Owner, owner))
                {
                    instance.expiredRequestIds.Add(pair.Key);
                }
            }

            foreach (string requestId in instance.expiredRequestIds)
            {
                instance.pendingRequests.Remove(requestId);
            }

            instance.expiredRequestIds.Clear();
        }

        private static string BL_CreateRequestId()
        {
            return Guid.NewGuid().ToString("N");
        }

        private static BilibiliMiniGameBridge BL_GetOrCreate()
        {
            if (instance != null)
            {
                return instance;
            }

            GameObject bridgeObject = GameObject.Find(BridgeObjectName);
            if (bridgeObject == null)
            {
                bridgeObject = new GameObject(BridgeObjectName);
            }

            instance = bridgeObject.GetComponent<BilibiliMiniGameBridge>();
            if (instance == null)
            {
                instance = bridgeObject.AddComponent<BilibiliMiniGameBridge>();
            }

            DontDestroyOnLoad(bridgeObject);
            return instance;
        }

        private void Awake()
        {
            if (instance != null && instance != this)
            {
                Destroy(gameObject);
                return;
            }

            instance = this;
            gameObject.name = BridgeObjectName;
            DontDestroyOnLoad(gameObject);
        }

        private void Update()
        {
            if (pendingRequests.Count == 0)
            {
                return;
            }

            float now = Time.realtimeSinceStartup;
            expiredRequestIds.Clear();

            foreach (KeyValuePair<string, PendingRequest> pair in pendingRequests)
            {
                if (pair.Value.Deadline <= now)
                {
                    expiredRequestIds.Add(pair.Key);
                }
            }

            foreach (string requestId in expiredRequestIds)
            {
                BL_CompleteWithFailure(
                    requestId,
                    new BilibiliMiniGameError(RequestTimeoutError, "Bilibili Mini Game API request timed out."));
            }

            expiredRequestIds.Clear();
        }

        private void BL_RegisterAndInvoke(PendingRequest request, Action invocation)
        {
            pendingRequests.Add(request.RequestId, request);

#if UNITY_WEBGL && !UNITY_EDITOR
            try
            {
                invocation?.Invoke();
            }
            catch (Exception exception)
            {
                BL_CompleteWithFailure(
                    request.RequestId,
                    new BilibiliMiniGameError(-1002, exception.Message));
            }
#else
            BL_CompleteWithFailure(
                request.RequestId,
                new BilibiliMiniGameError(
                    UnsupportedEnvironmentError,
                    "Bilibili Mini Game APIs are only available in a non-Editor WebGL build."));
#endif
        }

        private void BL_RegisterAndInvoke(PendingRequest request, string scene, Action invocation)
        {
            if (string.IsNullOrEmpty(scene))
            {
                pendingRequests.Add(request.RequestId, request);
                BL_CompleteWithFailure(
                    request.RequestId,
                    new BilibiliMiniGameError(20001, "scene cannot be null or empty."));
                return;
            }

            BL_RegisterAndInvoke(request, invocation);
        }

        [Preserve]
        public void BL_OnBilibiliMiniGameCallback(string json)
        {
            JsCallback callback;
            try
            {
                callback = JsonUtility.FromJson<JsCallback>(json);
            }
            catch (Exception exception)
            {
                Debug.LogError($"[BilibiliMiniGameSdk] Invalid JS callback: {exception.Message}\n{json}");
                return;
            }

            if (callback == null || string.IsNullOrEmpty(callback.requestId))
            {
                Debug.LogError($"[BilibiliMiniGameSdk] JS callback has no requestId: {json}");
                return;
            }

            if (!pendingRequests.TryGetValue(callback.requestId, out PendingRequest request))
            {
                return;
            }

            pendingRequests.Remove(callback.requestId);

            if (callback.success)
            {
                BL_CompleteWithSuccess(request, callback);
            }
            else
            {
                BL_InvokeSafely(
                    () => request.OnFail?.Invoke(
                        new BilibiliMiniGameError(callback.errCode, callback.errMsg)));
            }

            BL_InvokeSafely(request.OnComplete);
        }

        private void BL_CompleteWithSuccess(PendingRequest request, JsCallback callback)
        {
            BL_InvokeSafely(
                () => request.OnSuccess?.Invoke(
                    new BilibiliMiniGameResponse(callback.isExist, callback.errCode, callback.errMsg)));
        }

        private void BL_CompleteWithFailure(string requestId, BilibiliMiniGameError error)
        {
            if (!pendingRequests.TryGetValue(requestId, out PendingRequest request))
            {
                return;
            }

            pendingRequests.Remove(requestId);
            BL_InvokeSafely(() => request.OnFail?.Invoke(error));
            BL_InvokeSafely(request.OnComplete);
        }

        private static void BL_InvokeSafely(Action callback)
        {
            if (callback == null)
            {
                return;
            }

            try
            {
                callback.Invoke();
            }
            catch (Exception exception)
            {
                Debug.LogException(exception);
            }
        }

        private sealed class PendingRequest
        {
            internal BilibiliMiniGameSdk Owner { get; private set; }
            internal string RequestId { get; private set; }
            internal float Deadline { get; private set; }
            internal Action<BilibiliMiniGameResponse> OnSuccess { get; private set; }
            internal Action<BilibiliMiniGameError> OnFail { get; private set; }
            internal Action OnComplete { get; private set; }

            internal static PendingRequest BL_Create(
                BilibiliMiniGameSdk owner,
                string requestId,
                float timeoutSeconds,
                Action<BilibiliMiniGameResponse> onSuccess,
                Action<BilibiliMiniGameError> onFail,
                Action onComplete)
            {
                return new PendingRequest
                {
                    Owner = owner,
                    RequestId = requestId,
                    Deadline = Time.realtimeSinceStartup + timeoutSeconds,
                    OnSuccess = onSuccess,
                    OnFail = onFail,
                    OnComplete = onComplete
                };
            }
        }

        [Serializable]
        private sealed class JsCallback
        {
            public string requestId;
            public bool success;
            public bool isExist;
            public int errCode;
            public string errMsg;
        }
    }
}
