using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Services.Assets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine;
using UnityEngine.AddressableAssets;
using Object = UnityEngine.Object;

namespace Darkmatter.Services.Assets
{
    public class AddressableAssetProviderService : IAssetProviderService
    {
        private readonly AddressableLoadHandleTracker _loadHandleTracker = new();
        private readonly Dictionary<GameObject, AsyncOperationHandle<GameObject>> _spawnedInstances = new();

        public void UnloadAsset(string assetId)
        {
            if (string.IsNullOrWhiteSpace(assetId))
            {
                Debug.LogError("[AddressablesAssetProvider] Asset key is null or empty.");
                return;
            }

            try
            {
                if (!_loadHandleTracker.TryRelease(assetId))
                {
                    Debug.LogWarning(
                        $"[AddressablesAssetProvider] Tried to unload asset '{assetId}', but no tracked load handle was found.");
                }
            }
            catch (Exception e)
            {
                Debug.LogError(
                    $"[AddressablesAssetProvider] Failed to unload asset with key: {assetId}. Exception: {e}");
            }
        }

        public async UniTask LoadExternalCatalogAsync(string catalogUrl, IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(catalogUrl))
            {
                Debug.LogError("[AddressablesAssetProvider] Catalog url is null or empty.");
                return;
            }

            AsyncOperationHandle handle = default;

            try
            {
                handle = Addressables.LoadContentCatalogAsync(catalogUrl);

                while (!handle.IsDone)
                {
                    progress?.Report(handle.PercentComplete);
                    await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
                }

                await handle.ToUniTask(cancellationToken: cancellationToken);

                progress?.Report(1f);

                Debug.Log($"[AddressablesAssetProvider] Successfully mounted external mod catalog from {catalogUrl}");
            }
            catch (OperationCanceledException)
            {
                throw;
            }
            catch (Exception e)
            {
                Debug.LogError(
                    $"[AddressablesAssetProvider] Failed to mount external catalog from {catalogUrl}. Exception: {e}");
            }
            finally
            {
                if (handle.IsValid())
                    Addressables.Release(handle);
            }
        }

        public async UniTask<GameObject> InstantiateAsync(
            string assetId,
            Vector3 position,
            Quaternion rotation,
            IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(assetId))
            {
                Debug.LogError("[AddressablesAssetProvider] Asset key is null or empty.");
                return null;
            }

            AsyncOperationHandle<IList<IResourceLocation>> locationsHandle = default;
            AsyncOperationHandle<GameObject> handle = default;

            try
            {
                locationsHandle = Addressables.LoadResourceLocationsAsync(assetId);
                await locationsHandle.ToUniTask(cancellationToken: cancellationToken);

                if (locationsHandle.Result == null || locationsHandle.Result.Count == 0)
                {
                    Debug.LogWarning(
                        $"[AddressablesAssetProvider] No location found for key: {assetId}. Instantiate skipped.");
                    return null;
                }

                handle = Addressables.InstantiateAsync(assetId, position, rotation);
                while (!handle.IsDone)
                {
                    progress?.Report(handle.PercentComplete);
                    await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
                }

                await handle.ToUniTask(cancellationToken: cancellationToken);

                progress?.Report(1f);

                var instance = handle.Result;

                if (instance != null)
                {
                    _spawnedInstances[instance] = handle;
                    return instance;
                }

                Debug.LogError($"[AddressablesAssetProvider] Failed to load asset with key: {assetId}");
                return null;
            }
            catch (OperationCanceledException)
            {
                if (handle.IsValid())
                    Addressables.Release(handle);

                throw;
            }
            catch (Exception e)
            {
                if (handle.IsValid())
                    Addressables.Release(handle);

                Debug.LogError($"[AddressablesAssetProvider] Failed to instantiate asset with key: {assetId}. Exception: {e}");
                return null;
            }
            finally
            {
                if (locationsHandle.IsValid())
                    Addressables.Release(locationsHandle);
            }
        }


        public async UniTask<GameObject> InstantiateAsync(string assetId, IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            return await InstantiateAsync(assetId, Vector3.zero, Quaternion.identity, progress, cancellationToken);
        }

        public async UniTask<T> LoadAssetAsync<T>(string assetId, IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(assetId))
            {
                Debug.LogError("[AddressablesAssetProvider] Asset key is null or empty.");
                return default;
            }

            AsyncOperationHandle<IList<IResourceLocation>> locationsHandle = default;
            AsyncOperationHandle<T> handle = default;

            try
            {
                locationsHandle = Addressables.LoadResourceLocationsAsync(assetId);
                await locationsHandle.ToUniTask(cancellationToken: cancellationToken);

                if (locationsHandle.Result == null || locationsHandle.Result.Count == 0)
                {
                    Debug.LogWarning(
                        $"[AddressablesAssetProvider] No location found for key: {assetId}. Returning default value.");
                    return default;
                }

                handle = Addressables.LoadAssetAsync<T>(assetId);
                while (!handle.IsDone)
                {
                    progress?.Report(handle.PercentComplete);
                    await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
                }

                await handle.ToUniTask(cancellationToken: cancellationToken);
                progress?.Report(1f);
                if (ReferenceEquals(handle.Result, null))
                {
                    Addressables.Release(handle);
                    return default;
                }

                _loadHandleTracker.Track(handle, new[] { NormalizeReleaseKey(assetId) });
                return handle.Result;
            }
            catch (OperationCanceledException)
            {
                if (handle.IsValid())
                    Addressables.Release(handle);

                throw;
            }
            catch (Exception e)
            {
                if (handle.IsValid())
                    Addressables.Release(handle);

                Debug.LogError($"[AddressablesAssetProvider] Failed to load asset with key: {assetId}. Exception: {e}");
            }
            finally
            {
                if (locationsHandle.IsValid())
                    Addressables.Release(locationsHandle);
            }

            return default;
        }

        public async UniTask<IReadOnlyList<T>> LoadAssetsAsync<T>(
            IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            var locations = CollectLocationsForType<T>();
            if (locations.Count == 0)
            {
                Debug.LogWarning(
                    $"[AddressablesAssetProvider] No addressable locations found for type {typeof(T).Name}. Returning empty result.");
                return Array.Empty<T>();
            }

            AsyncOperationHandle<IList<T>> handle = default;

            try
            {
                handle = Addressables.LoadAssetsAsync<T>(locations, null);
                while (!handle.IsDone)
                {
                    progress?.Report(handle.PercentComplete);
                    await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
                }

                await handle.ToUniTask(cancellationToken: cancellationToken);
                progress?.Report(1f);

                if (handle.Result == null || handle.Result.Count == 0)
                {
                    if (handle.IsValid())
                    {
                        Addressables.Release(handle);
                    }

                    return Array.Empty<T>();
                }

                var assets = new List<T>(handle.Result.Count);
                for (var i = 0; i < handle.Result.Count; i++)
                {
                    if (handle.Result[i] != null)
                    {
                        assets.Add(handle.Result[i]);
                    }
                }

                if (assets.Count == 0)
                {
                    if (handle.IsValid())
                    {
                        Addressables.Release(handle);
                    }

                    return Array.Empty<T>();
                }

                _loadHandleTracker.Track(handle, CollectLocationReleaseKeys(locations));
                return assets;
            }
            catch (OperationCanceledException)
            {
                if (handle.IsValid())
                    Addressables.Release(handle);

                throw;
            }
            catch (Exception e)
            {
                if (handle.IsValid())
                    Addressables.Release(handle);

                Debug.LogError(
                    $"[AddressablesAssetProvider] Failed to load assets for type {typeof(T).Name}. Exception: {e}");
            }

            return Array.Empty<T>();
        }

        public async UniTask<IReadOnlyList<T>> LoadAssetsAsync<T>(
            object key,
            IProgress<float> progress,
            CancellationToken cancellationToken)
        {
            if (key == null)
            {
                Debug.LogError("[AddressablesAssetProvider] Asset key is null.");
                return Array.Empty<T>();
            }

            AsyncOperationHandle<IList<IResourceLocation>> locationsHandle = default;
            AsyncOperationHandle<IList<T>> handle = default;

            try
            {
                locationsHandle = Addressables.LoadResourceLocationsAsync(key, typeof(T));
                await locationsHandle.ToUniTask(cancellationToken: cancellationToken);

                if (locationsHandle.Result == null || locationsHandle.Result.Count == 0)
                {
                    Debug.LogWarning(
                        $"[AddressablesAssetProvider] No addressable locations found for key '{key}' and type {typeof(T).Name}. Returning empty result.");
                    return Array.Empty<T>();
                }

                var locations = locationsHandle.Result;
                handle = Addressables.LoadAssetsAsync<T>(locations, null);
                while (!handle.IsDone)
                {
                    progress?.Report(handle.PercentComplete);
                    await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
                }

                await handle.ToUniTask(cancellationToken: cancellationToken);
                progress?.Report(1f);

                if (handle.Result == null || handle.Result.Count == 0)
                {
                    if (handle.IsValid())
                    {
                        Addressables.Release(handle);
                    }

                    return Array.Empty<T>();
                }

                var assets = new List<T>(handle.Result.Count);
                foreach (var item in handle.Result)
                {
                    if (item != null)
                    {
                        assets.Add(item);
                    }
                }

                if (assets.Count == 0)
                {
                    if (handle.IsValid())
                    {
                        Addressables.Release(handle);
                    }

                    return Array.Empty<T>();
                }

                _loadHandleTracker.Track(handle, new[] { NormalizeReleaseKey(key) });
                return assets;

            }
            catch (OperationCanceledException)
            {
                if (handle.IsValid())
                    Addressables.Release(handle);
                throw;
            }
            catch (Exception e)
            {
                if (handle.IsValid())
                    Addressables.Release(handle);

                Debug.LogError(
                    $"[AddressablesAssetProvider] Failed to load assets for key '{key}' and type {typeof(T).Name}. Exception: {e}");
            }
            finally
            {
                if (locationsHandle.IsValid())
                    Addressables.Release(locationsHandle);
            }

            return Array.Empty<T>();
        }

        public void ReleaseInstance(GameObject instance)
        {
            if (instance == null) return;

            try
            {
                if (_spawnedInstances.TryGetValue(instance, out var handle))
                {
                    Addressables.ReleaseInstance(handle);
                    _spawnedInstances.Remove(instance);
                }
                else
                {
                    Debug.LogWarning(
                        $"[AddressablesAssetProvider] Tried to release {instance.name}, but it was not tracked by this service.");
                    Object.Destroy(instance);
                }
            }
            catch (Exception e)
            {
                Debug.LogError(
                    $"[AddressablesAssetProvider] Failed to release instance {instance.name}. Exception: {e}");
                Object.Destroy(instance);
            }
        }

        private static List<IResourceLocation> CollectLocationsForType<T>()
        {
            var locationsByKey = new Dictionary<string, IResourceLocation>(StringComparer.Ordinal);

            foreach (var locator in Addressables.ResourceLocators)
            {
                if (locator == null)
                {
                    continue;
                }

                foreach (var key in locator.Keys)
                {
                    if (key == null || !locator.Locate(key, typeof(T), out var locations) || locations == null)
                    {
                        continue;
                    }

                    for (var i = 0; i < locations.Count; i++)
                    {
                        var location = locations[i];
                        if (location == null)
                        {
                            continue;
                        }

                        var locationKey = GetLocationKey(location);
                        locationsByKey.TryAdd(locationKey, location);
                    }
                }
            }

            return new List<IResourceLocation>(locationsByKey.Values);
        }

        private static List<string> CollectLocationReleaseKeys(IReadOnlyList<IResourceLocation> locations)
        {
            var releaseKeys = new List<string>(locations.Count);
            var uniqueReleaseKeys = new HashSet<string>(StringComparer.Ordinal);

            for (var i = 0; i < locations.Count; i++)
            {
                var releaseKey = ResolveLocationReleaseKey(locations[i]);
                if (string.IsNullOrWhiteSpace(releaseKey) || !uniqueReleaseKeys.Add(releaseKey))
                {
                    continue;
                }

                releaseKeys.Add(releaseKey);
            }

            return releaseKeys;
        }

        private static string NormalizeReleaseKey(object key)
        {
            return key switch
            {
                string stringKey => stringKey.Trim(),
                null => string.Empty,
                _ => key.ToString()
            };
        }

        private static string ResolveLocationReleaseKey(IResourceLocation location)
        {
            if (location == null)
            {
                return string.Empty;
            }

            if (!string.IsNullOrWhiteSpace(location.PrimaryKey))
            {
                return location.PrimaryKey;
            }

            return GetLocationKey(location);
        }

        private static string GetLocationKey(IResourceLocation location)
        {
            if (!string.IsNullOrWhiteSpace(location.PrimaryKey) && !string.IsNullOrWhiteSpace(location.InternalId))
            {
                return $"{location.PrimaryKey}|{location.InternalId}";
            }

            if (!string.IsNullOrWhiteSpace(location.PrimaryKey))
            {
                return location.PrimaryKey;
            }

            if (!string.IsNullOrWhiteSpace(location.InternalId))
            {
                return location.InternalId;
            }

            return location.ToString();
        }
    }
}
