using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Services.Scenes;
using Darkmatter.Core.Enums.Services.Scenes;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;

namespace Darkmatter.Services.Scenes
{
    public class SceneService : ISceneService
    {
        private readonly HashSet<GameScene> _loadedScenes = new();
        private readonly Dictionary<string, AsyncOperationHandle<SceneInstance>> _loadedAddressableScenes =
            new(StringComparer.Ordinal);

        public async UniTask LoadSceneAsync(GameScene scene, IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            if (SceneManager.GetSceneByName(scene.ToString()).isLoaded) _loadedScenes.Add(scene);

            if (_loadedScenes.Contains(scene))
            {
                progress?.Report(1f);
                return;
            }

            await SceneManager.LoadSceneAsync(scene.ToString(), LoadSceneMode.Additive)
                .ToUniTask(progress: progress, cancellationToken: cancellationToken);

            SceneManager.SetActiveScene(SceneManager.GetSceneByName(scene.ToString()));

            _loadedScenes.Add(scene);
        }

        public async UniTask UnloadSceneAsync(GameScene scene, IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            if (SceneManager.GetSceneByName(scene.ToString()).isLoaded) _loadedScenes.Add(scene);

            if (!_loadedScenes.Contains(scene))
            {
                progress?.Report(1f);
                return;
            }

            await SceneManager.UnloadSceneAsync(scene.ToString())
                .ToUniTask(progress: progress, cancellationToken: cancellationToken);

            _loadedScenes.Remove(scene);
        }

        public async UniTask LoadSceneAsync(
            string sceneKey,
            IProgress<float> progress,
            CancellationToken cancellationToken,
            bool setActiveScene = true)
        {
            if (string.IsNullOrWhiteSpace(sceneKey))
                throw new ArgumentException("Scene key is required.", nameof(sceneKey));

            if (_loadedAddressableScenes.TryGetValue(sceneKey, out AsyncOperationHandle<SceneInstance> loadedHandle))
            {
                if (loadedHandle.IsValid() && loadedHandle.Result.Scene.isLoaded)
                {
                    if (setActiveScene)
                        SceneManager.SetActiveScene(loadedHandle.Result.Scene);

                    progress?.Report(1f);
                    return;
                }

                _loadedAddressableScenes.Remove(sceneKey);
            }

            AsyncOperationHandle<SceneInstance> handle =
                Addressables.LoadSceneAsync(sceneKey, LoadSceneMode.Additive, activateOnLoad: true);

            try
            {
                await handle.ToUniTask(progress: progress, cancellationToken: cancellationToken);

                if (setActiveScene && handle.Result.Scene.IsValid())
                    SceneManager.SetActiveScene(handle.Result.Scene);

                _loadedAddressableScenes[sceneKey] = handle;
                progress?.Report(1f);
            }
            catch
            {
                if (handle.IsValid())
                    Addressables.Release(handle);

                throw;
            }
        }

        public async UniTask UnloadSceneAsync(string sceneKey, IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(sceneKey))
                throw new ArgumentException("Scene key is required.", nameof(sceneKey));

            if (!_loadedAddressableScenes.TryGetValue(sceneKey, out AsyncOperationHandle<SceneInstance> loadedHandle))
            {
                Scene sceneByName = SceneManager.GetSceneByName(sceneKey);
                if (!sceneByName.isLoaded)
                {
                    progress?.Report(1f);
                    return;
                }

                await SceneManager.UnloadSceneAsync(sceneKey)
                    .ToUniTask(progress: progress, cancellationToken: cancellationToken);
                return;
            }

            try
            {
                AsyncOperationHandle<SceneInstance> unloadHandle =
                    Addressables.UnloadSceneAsync(loadedHandle, autoReleaseHandle: true);
                await unloadHandle.ToUniTask(progress: progress, cancellationToken: cancellationToken);
                progress?.Report(1f);
            }
            finally
            {
                _loadedAddressableScenes.Remove(sceneKey);
            }
        }

        public bool TryGetLoadedScene(string sceneKey, out Scene scene)
        {
            scene = default;
            if (string.IsNullOrWhiteSpace(sceneKey))
            {
                return false;
            }

            if (_loadedAddressableScenes.TryGetValue(sceneKey, out AsyncOperationHandle<SceneInstance> loadedHandle) &&
                loadedHandle.IsValid())
            {
                Scene candidate = loadedHandle.Result.Scene;
                if (candidate.IsValid() && candidate.isLoaded)
                {
                    scene = candidate;
                    return true;
                }
            }

            Scene byName = SceneManager.GetSceneByName(sceneKey);
            if (!byName.IsValid() || !byName.isLoaded)
            {
                return false;
            }

            scene = byName;
            return true;
        }
    }
}
