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

namespace Davanci.Nexus.PackageGuard
{
    // Keeps optional-package scripting defines honest.
    //
    // Some Nexus features depend on a UPM package (e.g. In-App Purchases needs
    // com.unity.purchasing). We gate that code behind a define (DAVANCI_IAP). If the package
    // later disappears — you discard a manifest change, switch branches, revert — the define
    // would still be set in PlayerSettings, so the gated code tries to compile against a
    // namespace that no longer exists and the whole project breaks.
    //
    // This guard drops the matching define the moment its package is gone, so the gated code
    // simply compiles out instead. It lives in its OWN dependency-free editor assembly on
    // purpose: it must keep working even when Assembly-CSharp is broken by exactly the dangling
    // define it exists to clear (Unity still loads independent editor assemblies, including in
    // Safe Mode, which is how this self-heals a project that opens already broken).
    [InitializeOnLoad]
    public static class NexusPackageGuard
    {
        // package id -> defines that must not outlive it.
        static readonly Dictionary<string, string[]> Watched = new Dictionary<string, string[]>
        {
            { "com.unity.purchasing", new[] { "DAVANCI_IAP", "NEXUS_IAP_VALIDATE" } },
            { "com.unity.mobile.notifications", new[] { "DAVANCI_NOTIFICATIONS" } },
        };

        static readonly NamedBuildTarget[] Targets =
        {
            NamedBuildTarget.Standalone,
            NamedBuildTarget.iOS,
            NamedBuildTarget.Android,
            NamedBuildTarget.tvOS,
        };

        // Non-UPM SDKs (e.g. AppLovin MAX) can't be watched via registeredPackages — they import
        // as a .unitypackage into Assets/. We instead reconcile by type presence: if the SDK's
        // entry-point type is gone but its define lingers, clear it so the gated code compiles out.
        // Keyed by a detection type short-name -> defines that must not outlive it.
        static readonly Dictionary<string, string[]> WatchedTypes = new Dictionary<string, string[]>
        {
            { "MaxSdk", new[] { "DAVANCI_ADS" } },
            { "LevelPlay", new[] { "DAVANCI_LEVELPLAY" } },
        };

        static UnityEditor.PackageManager.Requests.ListRequest _list;

        static NexusPackageGuard()
        {
            // Live removals: fires while the previous (good) domain is still alive, before the
            // recompile that would otherwise break.
            Events.registeredPackages += OnRegisteredPackages;

            // Cold start: reconcile against the resolved package set in case a package was removed
            // while the editor was closed. Offline list reads the lockfile, no network.
            _list = Client.List(offlineMode: true, includeIndirectDependencies: false);
            EditorApplication.update += PollList;

            // Type-presence reconcile for non-UPM SDKs, runs immediately (no async list needed).
            ReconcileTypeDefines();
        }

        static void ReconcileTypeDefines()
        {
            foreach (var kv in WatchedTypes)
                if (!TypeExists(kv.Key))
                    ClearDefines(kv.Value, kv.Key + " (type missing)");
        }

        static bool TypeExists(string shortName) => AppDomain.CurrentDomain.GetAssemblies()
            .Any(a =>
            {
                try
                {
                    if (a.GetType(shortName) != null) return true;
                    return a.GetTypes().Any(t => t != null && t.Name == shortName);
                }
                catch { return false; }
            });

        static void PollList()
        {
            if (_list == null || !_list.IsCompleted) return;
            EditorApplication.update -= PollList;

            if (_list.Status == StatusCode.Success)
            {
                var present = new HashSet<string>(_list.Result.Select(p => p.name));
                foreach (var kv in Watched)
                    if (!present.Contains(kv.Key))
                        ClearDefines(kv.Value, kv.Key);
            }
            _list = null;
        }

        static void OnRegisteredPackages(PackageRegistrationEventArgs args)
        {
            foreach (var removed in args.removed)
                if (Watched.TryGetValue(removed.name, out var defines))
                    ClearDefines(defines, removed.name);
        }

        static void ClearDefines(string[] defines, string packageId)
        {
            bool any = false;
            foreach (var target in Targets)
            {
                var list = PlayerSettings.GetScriptingDefineSymbols(target)
                    .Split(';').Select(d => d.Trim()).Where(d => d.Length > 0).ToList();

                int before = list.Count;
                list.RemoveAll(d => defines.Contains(d));
                if (list.Count == before) continue;

                PlayerSettings.SetScriptingDefineSymbols(target, string.Join(";", list));
                any = true;
            }

            if (any)
                Debug.Log($"[Nexus] '{packageId}' is no longer in the project — cleared its define(s) " +
                          $"({string.Join(", ", defines)}) so the dependent code compiles out.");
        }
    }
}
