using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

namespace Davanci.Nexus.Editor
{
    public enum AssetCategory { Texture, Audio, Model }

    public sealed class AssetIssue
    {
        public string Path;
        public AssetCategory Category;
        public DoctorStatus Severity;
        public string Detail;
        public Action Fix;
        public bool Optional;   // lossy build-size win, opt-in (not part of "Fix all")
    }

    // Scans textures, audio and models for import settings that inflate mobile build
    // size or runtime memory, and offers one-click fixes. Only assets the game actually
    // ships are audited: dependencies of the enabled Build Settings scenes plus anything
    // under Resources/. Editor-only assets are skipped.
    public static class NexusAssetAuditor
    {
        const int   MaxTextureSize   = 2048;
        const float LongClipSeconds  = 8f;   // music / VO: stream instead of holding in RAM
        const float ShortClipSeconds = 1f;   // SFX: decompress on load for low latency

        // Paths the current audit considers "used by the game"; null means audit everything
        // (fallback when no scenes are enabled in Build Settings).
        static HashSet<string> _usedPaths;

        public static List<AssetIssue> Audit()
        {
            _usedPaths = CollectUsedAssetPaths();
            var issues = new List<AssetIssue>();
            AuditTextures(issues);
            AuditAudio(issues);
            AuditModels(issues);
            _usedPaths = null;
            return issues;
        }

        // True when the last audit could resolve usage from Build Settings scenes.
        public static bool CanScopeToGame =>
            EditorBuildSettings.scenes.Any(s => s.enabled && !string.IsNullOrEmpty(s.path));

        // Everything the build will contain: recursive dependencies of enabled scenes,
        // plus all assets in Resources/ folders (Unity always ships those).
        static HashSet<string> CollectUsedAssetPaths()
        {
            var scenePaths = EditorBuildSettings.scenes
                .Where(s => s.enabled && !string.IsNullOrEmpty(s.path))
                .Select(s => s.path)
                .ToArray();

            if (scenePaths.Length == 0)
                return null; // no build scenes configured; audit the whole project

            var used = new HashSet<string>(AssetDatabase.GetDependencies(scenePaths, true));

            foreach (var guid in AssetDatabase.FindAssets("", new[] { "Assets" }))
            {
                string path = AssetDatabase.GUIDToAssetPath(guid);
                if (path.Contains("/Resources/"))
                    used.Add(path);
            }

            return used;
        }

        public static void FixAll(IEnumerable<AssetIssue> issues)
        {
            try
            {
                AssetDatabase.StartAssetEditing();
                foreach (var issue in issues)
                    issue.Fix?.Invoke();
            }
            finally
            {
                AssetDatabase.StopAssetEditing();
                AssetDatabase.Refresh();
            }
        }

        static void AuditTextures(List<AssetIssue> issues)
        {
            foreach (var path in FindShippedAssets("t:Texture2D"))
            {
                if (AssetImporter.GetAtPath(path) is not TextureImporter importer)
                    continue;

                if (importer.maxTextureSize > MaxTextureSize)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Texture,
                        Severity = DoctorStatus.Warning,
                        Detail = $"Max size {importer.maxTextureSize}px, caps to {MaxTextureSize}px to cut build size and memory.",
                        Fix = () => SetTexture(path, i => i.maxTextureSize = MaxTextureSize),
                    });
                }

                if (importer.textureCompression == TextureImporterCompression.Uncompressed)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Texture,
                        Severity = DoctorStatus.Warning,
                        Detail = "Uncompressed, enables compression for a much smaller footprint.",
                        Fix = () => SetTexture(path, i => i.textureCompression = TextureImporterCompression.Compressed),
                    });
                }

                if (importer.isReadable)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Texture,
                        Severity = DoctorStatus.Warning,
                        Detail = "Read/Write enabled, keeps a second CPU copy in memory. Disable unless you sample pixels at runtime.",
                        Fix = () => SetTexture(path, i => i.isReadable = false),
                    });
                }

                bool isUi = importer.textureType is TextureImporterType.Sprite or TextureImporterType.GUI;
                if (isUi && importer.mipmapEnabled)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Texture,
                        Severity = DoctorStatus.Warning,
                        Detail = "Mipmaps on a Sprite/UI texture, wastes ~33% memory and softens 2D art. Disable mipmaps.",
                        Fix = () => SetTexture(path, i => i.mipmapEnabled = false),
                    });
                }

                if (isUi
                    && importer.textureCompression != TextureImporterCompression.Uncompressed
                    && !importer.crunchedCompression)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Texture,
                        Severity = DoctorStatus.Warning,
                        Optional = true,
                        Detail = "Not crunch-compressed, crunch shrinks the download a lot (lossy, slower import; runtime memory is unchanged).",
                        Fix = () => SetTexture(path, i => { i.crunchedCompression = true; i.compressionQuality = 50; }),
                    });
                }
            }
        }

        static void SetTexture(string path, Action<TextureImporter> mutate)
        {
            if (AssetImporter.GetAtPath(path) is not TextureImporter importer) return;
            mutate(importer);
            importer.SaveAndReimport();
        }

        static void AuditAudio(List<AssetIssue> issues)
        {
            foreach (var path in FindShippedAssets("t:AudioClip"))
            {
                if (AssetImporter.GetAtPath(path) is not AudioImporter importer)
                    continue;

                var clip = AssetDatabase.LoadAssetAtPath<AudioClip>(path);
                float length = clip != null ? clip.length : 0f;
                var settings = importer.defaultSampleSettings;

                if (length >= LongClipSeconds &&
                    (settings.loadType != AudioClipLoadType.Streaming || !importer.loadInBackground))
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Audio,
                        Severity = DoctorStatus.Warning,
                        Detail = $"{length:0.0}s clip not set up for streaming, stream long music/VO and load it in the background so it isn't decompressed into RAM or stalling load.",
                        Fix = () =>
                        {
                            if (AssetImporter.GetAtPath(path) is not AudioImporter imp) return;
                            imp.loadInBackground = true;
                            var s = imp.defaultSampleSettings;
                            s.loadType = AudioClipLoadType.Streaming;
                            s.compressionFormat = AudioCompressionFormat.Vorbis;
                            imp.defaultSampleSettings = s;
                            imp.SaveAndReimport();
                        },
                    });
                }
                else if (length > 0f && length <= ShortClipSeconds && settings.loadType == AudioClipLoadType.Streaming)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Audio,
                        Severity = DoctorStatus.Warning,
                        Detail = $"{length:0.0}s SFX set to Streaming, short sounds should Decompress On Load for low latency.",
                        Fix = () => SetAudio(path, s =>
                        {
                            s.loadType = AudioClipLoadType.DecompressOnLoad;
                            return s;
                        }),
                    });
                }

                if (length > ShortClipSeconds && settings.compressionFormat == AudioCompressionFormat.PCM)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Audio,
                        Severity = DoctorStatus.Warning,
                        Detail = "Uncompressed PCM, switch to Vorbis to shrink the clip dramatically.",
                        Fix = () => SetAudio(path, s =>
                        {
                            s.compressionFormat = AudioCompressionFormat.Vorbis;
                            return s;
                        }),
                    });
                }

                if (clip != null && clip.channels > 1 && !importer.forceToMono)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Audio,
                        Severity = DoctorStatus.Warning,
                        Optional = true,
                        Detail = "Stereo clip, forcing to mono roughly halves its size. Great for SFX/UI; skip for true stereo music.",
                        Fix = () =>
                        {
                            if (AssetImporter.GetAtPath(path) is not AudioImporter imp) return;
                            imp.forceToMono = true;
                            imp.SaveAndReimport();
                        },
                    });
                }
            }
        }

        static void SetAudio(string path, Func<AudioImporterSampleSettings, AudioImporterSampleSettings> mutate)
        {
            if (AssetImporter.GetAtPath(path) is not AudioImporter importer) return;
            importer.defaultSampleSettings = mutate(importer.defaultSampleSettings);
            importer.SaveAndReimport();
        }

        static void AuditModels(List<AssetIssue> issues)
        {
            foreach (var path in FindShippedAssets("t:Model"))
            {
                if (AssetImporter.GetAtPath(path) is not ModelImporter importer)
                    continue;

                if (importer.isReadable)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Model,
                        Severity = DoctorStatus.Warning,
                        Detail = "Read/Write enabled, keeps a CPU mesh copy in memory. Disable unless you read mesh data at runtime.",
                        Fix = () => SetModel(path, i => i.isReadable = false),
                    });
                }

                if (importer.meshCompression == ModelImporterMeshCompression.Off)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Model,
                        Severity = DoctorStatus.Warning,
                        Detail = "Mesh compression Off, Medium trims the mesh in the build with little visible cost.",
                        Fix = () => SetModel(path, i => i.meshCompression = ModelImporterMeshCompression.Medium),
                    });
                }

                if (importer.meshOptimizationFlags != MeshOptimizationFlags.Everything)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Model,
                        Severity = DoctorStatus.Warning,
                        Detail = "Mesh optimization not fully enabled, reorders vertices/indices for better GPU cache use.",
                        Fix = () => SetModel(path, i => i.meshOptimizationFlags = MeshOptimizationFlags.Everything),
                    });
                }

                if (importer.meshCompression == ModelImporterMeshCompression.Low
                    || importer.meshCompression == ModelImporterMeshCompression.Medium)
                {
                    issues.Add(new AssetIssue
                    {
                        Path = path,
                        Category = AssetCategory.Model,
                        Severity = DoctorStatus.Warning,
                        Optional = true,
                        Detail = "Mesh compression below High, High packs the mesh tighter in the build (more vertex quantization; decompressed at runtime).",
                        Fix = () => SetModel(path, i => i.meshCompression = ModelImporterMeshCompression.High),
                    });
                }
            }
        }

        static void SetModel(string path, Action<ModelImporter> mutate)
        {
            if (AssetImporter.GetAtPath(path) is not ModelImporter importer) return;
            mutate(importer);
            importer.SaveAndReimport();
        }

        static IEnumerable<string> FindShippedAssets(string filter)
        {
            foreach (var guid in AssetDatabase.FindAssets(filter))
            {
                string path = AssetDatabase.GUIDToAssetPath(guid);
                if (!ShipsInBuild(path))
                    continue;
                if (_usedPaths != null && !_usedPaths.Contains(path))
                    continue;
                yield return path;
            }
        }

        static bool ShipsInBuild(string path)
        {
            if (string.IsNullOrEmpty(path) || !path.StartsWith("Assets/"))
                return false;

            return !(path.Contains("/Editor/")
                  || path.Contains("/Gizmos/")
                  || path.Contains("Editor Default Resources"));
        }
    }
}
