using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;

/// <summary>
/// Shared utility for loading Addressable assets with automatic retry on failure.
/// All Addressable loads go through remote CDN and can fail due to transient network issues.
/// Without retries a single failed download permanently breaks the affected feature for the
/// session.
/// </summary>
public static class AddressableLoader {
    private const int DefaultMaxRetries = 3;
    private static readonly float[] RetryDelays = { 2f, 5f, 10f };

    /// <summary>
    /// Loads all assets matching a label, retrying on transient failures.
    /// Calls onSuccess with the loaded assets on success, or onFailure (if provided) on final
    /// failure.
    /// </summary>
    public static IEnumerator LoadWithRetry<T>(
            string label,
            Action<IList<T>> onSuccess,
            Action onFailure = null,
            int maxRetries = DefaultMaxRetries) {
        for (int attempt = 0; attempt <= maxRetries; attempt++) {
            var handle = Addressables.LoadAssetsAsync<T>(label, null);
            yield return handle;

            if (handle.Status == AsyncOperationStatus.Succeeded) {
                onSuccess(handle.Result);
                yield break;
            }

            Addressables.Release(handle);
            var error = handle.OperationException?.Message ?? "Unknown error";

            if (attempt < maxRetries) {
                float delay = attempt < RetryDelays.Length ? RetryDelays[attempt] : RetryDelays[^1];
                Debug.LogWarning(
                        $"Failed to load '{label}' (attempt {attempt + 1}/{maxRetries + 1}): " +
                        $"{error}. Retrying in {delay}s...");
                yield return new WaitForSeconds(delay);
            } else {
                Debug.LogError(
                        $"Failed to load '{label}' after {maxRetries + 1} attempts: {error}");
                onFailure?.Invoke();
            }
        }
    }

    /// <summary>
    /// Loads one asset by address, retrying on transient failures.
    /// Calls onSuccess with the loaded asset on success, or onFailure (if provided) on final
    /// failure.
    /// </summary>
    public static IEnumerator LoadAssetWithRetry<T>(
            string address,
            Action<T> onSuccess,
            Action onFailure = null,
            int maxRetries = DefaultMaxRetries) {
        for (int attempt = 0; attempt <= maxRetries; attempt++) {
            var handle = Addressables.LoadAssetAsync<T>(address);
            yield return handle;

            if (handle.Status == AsyncOperationStatus.Succeeded) {
                onSuccess(handle.Result);
                yield break;
            }

            Addressables.Release(handle);
            var error = handle.OperationException?.Message ?? "Unknown error";

            if (attempt < maxRetries) {
                float delay = attempt < RetryDelays.Length ? RetryDelays[attempt] : RetryDelays[^1];
                Debug.LogWarning(
                        $"Failed to load '{address}' (attempt {attempt + 1}/{maxRetries + 1}): " +
                        $"{error}. Retrying in {delay}s...");
                yield return new WaitForSeconds(delay);
            } else {
                Debug.LogError(
                        $"Failed to load '{address}' after {maxRetries + 1} attempts: {error}");
                onFailure?.Invoke();
            }
        }
    }

    /// <summary>
    /// Loads one asset from a label by matching the address or filename without extension.
    /// This keeps call sites from bulk-loading an entire label just to find one named asset.
    /// </summary>
    public static IEnumerator LoadNamedAssetWithRetry<T>(
            string label,
            string assetName,
            Action<T> onSuccess,
            Action onFailure = null,
            int maxRetries = DefaultMaxRetries) {
        for (int attempt = 0; attempt <= maxRetries; attempt++) {
            var locationsHandle = Addressables.LoadResourceLocationsAsync(label, typeof(T));
            yield return locationsHandle;

            if (locationsHandle.Status != AsyncOperationStatus.Succeeded) {
                var locationsError = locationsHandle.OperationException?.Message ?? "Unknown error";
                Addressables.Release(locationsHandle);
                yield return RetryOrFail(
                        label,
                        assetName,
                        attempt,
                        maxRetries,
                        locationsError,
                        onFailure);
                continue;
            }

            var location = locationsHandle.Result.FirstOrDefault(
                    candidate => LocationMatchesName(candidate, assetName));
            if (location == null) {
                Addressables.Release(locationsHandle);
                yield return RetryOrFail(
                        label,
                        assetName,
                        attempt,
                        maxRetries,
                        $"No asset named '{assetName}' was found",
                        onFailure);
                continue;
            }

            var handle = Addressables.LoadAssetAsync<T>(location);
            yield return handle;
            Addressables.Release(locationsHandle);

            if (handle.Status == AsyncOperationStatus.Succeeded) {
                onSuccess(handle.Result);
                yield break;
            }

            Addressables.Release(handle);
            var error = handle.OperationException?.Message ?? "Unknown error";
            yield return RetryOrFail(label, assetName, attempt, maxRetries, error, onFailure);
        }
    }

    /// <summary>
    /// Loads named assets from a label by matching each address or filename without extension.
    /// The label location list is resolved once per retry attempt.
    /// </summary>
    public static IEnumerator LoadNamedAssetsWithRetry<T>(
            string label,
            IEnumerable<string> assetNames,
            Action<IReadOnlyDictionary<string, T>> onSuccess,
            Action onFailure = null,
            int maxRetries = DefaultMaxRetries) {
        var requestedNames = assetNames.Where(name => !string.IsNullOrEmpty(name))
                                     .Distinct(StringComparer.Ordinal)
                                     .ToList();

        for (int attempt = 0; attempt <= maxRetries; attempt++) {
            var locationsHandle = Addressables.LoadResourceLocationsAsync(label, typeof(T));
            yield return locationsHandle;

            if (locationsHandle.Status != AsyncOperationStatus.Succeeded) {
                var locationsError = locationsHandle.OperationException?.Message ?? "Unknown error";
                Addressables.Release(locationsHandle);
                yield return RetryOrFail(
                        label,
                        string.Join(", ", requestedNames),
                        attempt,
                        maxRetries,
                        locationsError,
                        onFailure);
                continue;
            }

            var locationsByName = new Dictionary<string, IResourceLocation>(StringComparer.Ordinal);
            foreach (var assetName in requestedNames) {
                var location = locationsHandle.Result.FirstOrDefault(
                        candidate => LocationMatchesName(candidate, assetName));
                if (location != null) { locationsByName[assetName] = location; }
            }

            var missingNames =
                    requestedNames.Where(name => !locationsByName.ContainsKey(name)).ToList();
            if (missingNames.Count > 0) {
                Addressables.Release(locationsHandle);
                yield return RetryOrFail(
                        label,
                        string.Join(", ", missingNames),
                        attempt,
                        maxRetries,
                        $"No matching assets were found for: {string.Join(", ", missingNames)}",
                        onFailure);
                continue;
            }

            var loadedAssets = new Dictionary<string, T>(StringComparer.Ordinal);
            string loadError = null;
            foreach (var assetName in requestedNames) {
                var handle = Addressables.LoadAssetAsync<T>(locationsByName[assetName]);
                yield return handle;

                if (handle.Status == AsyncOperationStatus.Succeeded) {
                    loadedAssets[assetName] = handle.Result;
                } else {
                    loadError = handle.OperationException?.Message ??
                                $"Unknown error loading '{assetName}'";
                    Addressables.Release(handle);
                    break;
                }
            }
            Addressables.Release(locationsHandle);

            if (loadError == null) {
                onSuccess(loadedAssets);
                yield break;
            }

            yield return RetryOrFail(
                    label,
                    string.Join(", ", requestedNames),
                    attempt,
                    maxRetries,
                    loadError,
                    onFailure);
        }
    }

    private static bool LocationMatchesName(IResourceLocation location, string assetName) {
        return LocationKeyMatchesName(location.PrimaryKey, assetName) ||
               LocationKeyMatchesName(location.InternalId, assetName);
    }

    private static bool LocationKeyMatchesName(string key, string assetName) {
        if (string.IsNullOrEmpty(key)) { return false; }
        return key == assetName || Path.GetFileNameWithoutExtension(key) == assetName;
    }

    private static IEnumerator RetryOrFail(
            string label,
            string assetName,
            int attempt,
            int maxRetries,
            string error,
            Action onFailure) {
        if (attempt < maxRetries) {
            float delay = attempt < RetryDelays.Length ? RetryDelays[attempt] : RetryDelays[^1];
            Debug.LogWarning(
                    $"Failed to load '{assetName}' from '{label}' " +
                    $"(attempt {attempt + 1}/{maxRetries + 1}): {error}. " +
                    $"Retrying in {delay}s...");
            yield return new WaitForSeconds(delay);
        } else {
            Debug.LogError(
                    $"Failed to load '{assetName}' from '{label}' after {maxRetries + 1} attempts: {error}");
            onFailure?.Invoke();
        }
    }
}
