using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

namespace Davanci.Nexus.Editor
{
    public class NexusSetupWindow : EditorWindow
    {
        const string ConfigAssetPath = "Assets/DavanciNexus/Resources/NexusConfig.asset";
        const string DefaultDbUrl = "https://davancigames-default-rtdb.firebaseio.com/";
        const string LogoPath = "Assets/DavanciNexus/Art/logo-icon-cube.png";

        // The fixed wizard steps. An optional "Update" step is prepended at runtime (index 0)
        // only when a newer package version is published, see _hasUpdateStep.
        static readonly string[] BaseStepTitles =
        {
            "Platform",
            "Install SDKs",
            "Configure",
            "Ratings",
            "Notifications",
            "Privacy (GDPR)",
            "Firebase",
            "Remote Config",
            "Cloud Save",
            "Monetization",
            "Verify",
            "Splash",
            "Finish",
        };

        int _step;
        NexusConfig _config;

        const string KeyAwaitingUpdate = "Davanci.Nexus.Setup.AwaitingUpdate";

        bool _hasUpdateStep;                 // true when the update step is shown
        bool _updateStepResolved;            // true after the client is on the latest version
        NexusUpdater.UpdateInfo _update;     // populated by the open-time update probe

        int TotalSteps() => BaseStepTitles.Length + (_hasUpdateStep ? 1 : 0);
        // Maps a visible step index to a BaseStepTitles index (accounting for the update step).
        int BaseIndex(int step) => _hasUpdateStep ? step - 1 : step;

        // Cached AppLovin mediation networks (loaded on demand; fetching hits the network).
        List<NexusAdsNetworks.Net> _adNetworks;
        string _adNetworksMsg;

        bool _remotePackagesLoaded;
        bool _remotePackagesLoading;

        VisualElement _stepsBar;
        VisualElement _content;
        Button _backBtn;
        Button _nextBtn;
        Label _reviewHint;

        static readonly Color Accent   = new Color(1f, 0.42f, 0.42f);
        static readonly Color Accent2  = new Color(1f, 0.56f, 0.33f);
        static readonly Color Panel    = new Color(0.13f, 0.14f, 0.18f);
        static readonly Color PanelHi  = new Color(0.17f, 0.19f, 0.24f);
        static readonly Color OkGreen  = new Color(0.40f, 0.80f, 0.45f);
        static readonly Color Todo     = new Color(0.90f, 0.65f, 0.30f);
        static readonly Color Muted    = new Color(0.62f, 0.64f, 0.70f);

        static NexusSetupWindow _open;

        [MenuItem("Davanci Nexus/Nexus Setup")]
        public static void Open()
        {
            var w = GetWindow<NexusSetupWindow>();
            w.titleContent = new GUIContent("Nexus Setup",
                AssetDatabase.LoadAssetAtPath<Texture2D>(LogoPath));
            w.minSize = new Vector2(520, 620);
            w.Show();
        }

        public static void RefreshIfOpen()
        {
            if (_open != null)
                _open.RenderStep();
        }

        void OnEnable()
        {
            _open = this;
            _config = LoadOrCreateConfig();
            BuildChrome();
            RenderStep();
            CheckForUpdate();
            NexusPackageUpdater.Changed += RenderStep;
        }

        void OnDisable()
        {
            NexusPackageUpdater.Changed -= RenderStep;
            if (_open == this) _open = null;
        }

        // Silent probe on open. If a newer package is published, surface an Update step at
        // index 0 and jump to it; otherwise the wizard is unchanged. Skipped in the source
        // project (the author publishes, never consumes).
        void CheckForUpdate()
        {
            if (NexusUpdater.IsAuthoringProject()) return;

            string awaiting = EditorPrefs.GetString(KeyAwaitingUpdate, "");

            NexusUpdater.GetUpdateInfo(info =>
            {
                if (!string.IsNullOrEmpty(awaiting))
                {
                    _hasUpdateStep = true;
                    _updateStepResolved = !info.Available
                        || NexusUpdater.IsAtLeastVersion(NexusVersion.Current, awaiting);
                    if (_updateStepResolved)
                        EditorPrefs.DeleteKey(KeyAwaitingUpdate);
                    else
                        _update = info;
                    _step = 0;
                    RenderStep();
                    return;
                }

                if (!info.Available) return;
                _update = info;
                _hasUpdateStep = true;
                _step = 0;
                RenderStep();
            });
        }

        void RefreshUpdateState()
        {
            NexusUpdater.GetUpdateInfo(info =>
            {
                if (info.Available) return;
                _updateStepResolved = true;
                EditorPrefs.DeleteKey(KeyAwaitingUpdate);
                RenderStep();
            });
        }

        void BuildChrome()
        {
            var root = rootVisualElement;
            root.Clear();
            root.style.backgroundColor = new Color(0.10f, 0.11f, 0.14f);

            var header = new VisualElement();
            header.style.flexDirection = FlexDirection.Row;
            header.style.flexShrink = 0;
            header.style.justifyContent = Justify.SpaceBetween;
            header.style.alignItems = Align.FlexStart;
            header.style.paddingLeft = 18;
            header.style.paddingRight = 18;
            header.style.paddingTop = 16;
            header.style.paddingBottom = 8;

            var left = new VisualElement();
            left.style.flexDirection = FlexDirection.Row;
            left.style.alignItems = Align.Center;
            left.style.flexShrink = 1;

            var logo = AssetDatabase.LoadAssetAtPath<Texture2D>(LogoPath);
            if (logo != null)
            {
                var logoImg = new Image { image = logo, scaleMode = ScaleMode.ScaleToFit };
                logoImg.style.width = 44;
                logoImg.style.height = 44;
                logoImg.style.flexShrink = 0;
                logoImg.style.marginRight = 10;
                left.Add(logoImg);
            }

            var titleCol = new VisualElement();
            titleCol.style.flexShrink = 1;
            var title = new Label("DavanciNexus");
            title.style.fontSize = 20;
            title.style.unityFontStyleAndWeight = FontStyle.Bold;
            title.style.color = Accent2;
            var subtitle = new Label("Analytics & attribution setup for iOS and Android");
            subtitle.style.fontSize = 11;
            subtitle.style.color = Muted;
            subtitle.style.marginTop = 2;
            subtitle.style.whiteSpace = WhiteSpace.Normal;
            titleCol.Add(title);
            titleCol.Add(subtitle);
            left.Add(titleCol);
            header.Add(left);

            var doctorBtn = new Button(NexusDoctorWindow.Open) { text = "Davanci Doctor" };
            StyleSecondaryButton(doctorBtn);
            doctorBtn.style.marginTop = 4;
            doctorBtn.style.marginLeft = 8;
            doctorBtn.style.flexShrink = 0;
            doctorBtn.style.paddingLeft = 14; doctorBtn.style.paddingRight = 14;
            header.Add(doctorBtn);

            root.Add(header);

            _stepsBar = new VisualElement();
            _stepsBar.style.flexDirection = FlexDirection.Row;
            _stepsBar.style.flexShrink = 0;
            _stepsBar.style.flexWrap = Wrap.Wrap;
            _stepsBar.style.paddingLeft = 18;
            _stepsBar.style.paddingRight = 18;
            _stepsBar.style.marginBottom = 8;
            root.Add(_stepsBar);

            var scroll = new ScrollView();
            scroll.style.flexGrow = 1;
            scroll.style.minHeight = 0;
            scroll.style.paddingLeft = 18;
            scroll.style.paddingRight = 18;
            _content = new VisualElement();
            scroll.Add(_content);
            root.Add(scroll);

            var footer = new VisualElement();
            footer.style.flexDirection = FlexDirection.Row;
            footer.style.flexShrink = 0;
            footer.style.justifyContent = Justify.SpaceBetween;
            footer.style.paddingLeft = 18;
            footer.style.paddingRight = 18;
            footer.style.paddingTop = 10;
            footer.style.paddingBottom = 14;
            footer.style.borderTopWidth = 1;
            footer.style.borderTopColor = new Color(1, 1, 1, 0.06f);

            _backBtn = new Button(() => { if (_step > 0) { _step--; RenderStep(); } }) { text = "← Back" };
            StyleSecondaryButton(_backBtn);
            _nextBtn = new Button(() =>
            {
                if (_step < TotalSteps() - 1) { _step++; RenderStep(); }
                else Close();
            }) { text = "Next →" };
            StylePrimaryButton(_nextBtn);

            footer.Add(_backBtn);
            footer.Add(_nextBtn);
            root.Add(footer);
        }

        void RenderStep()
        {
            BuildStepsBar();
            _content.Clear();

            if (_hasUpdateStep && _step == 0)
            {
                BuildUpdateStep();
            }
            else
            {
                switch (BaseIndex(_step))
                {
                    case 0: BuildPlatformStep(); break;
                    case 1: BuildInstallStep(); break;
                    case 2: BuildConfigStep(); break;
                    case 3: BuildRatingsStep(); break;
                    case 4: BuildNotificationsStep(); break;
                    case 5: BuildPrivacyStep(); break;
                    case 6: BuildFirebaseStep(); break;
                    case 7: BuildRemoteConfigStep(); break;
                    case 8: BuildCloudSaveStep(); break;
                    case 9: BuildMonetizationStep(); break;
                    case 10: BuildVerifyStep(); break;
                    case 11: BuildSplashStep(); break;
                    case 12: BuildFinishStep(); break;
                }
            }

            _backBtn.SetEnabled(_step > 0);
            _nextBtn.text = _step == TotalSteps() - 1 ? "Done ✓" : "Next →";

            AnimateContentIn();
        }

        void AnimateContentIn()
        {
            _content.style.opacity = 0f;
            _content.style.translate = new Translate(0, 10, 0);
            _content.style.transitionProperty =
                new List<StylePropertyName> { "opacity", "translate" };
            _content.style.transitionDuration =
                new List<TimeValue> { new TimeValue(160, TimeUnit.Millisecond), new TimeValue(160, TimeUnit.Millisecond) };
            _content.schedule.Execute(() =>
            {
                _content.style.opacity = 1f;
                _content.style.translate = new Translate(0, 0, 0);
            }).StartingIn(16);
        }

        void BuildStepsBar()
        {
            _stepsBar.Clear();
            var titles = CurrentStepTitles();
            for (int i = 0; i < titles.Count; i++)
            {
                int idx = i;
                var pill = new Button(() => { _step = idx; RenderStep(); });
                pill.text = $"{i + 1}. {titles[i]}";
                pill.style.flexGrow = 1;
                pill.style.flexShrink = 1;
                pill.style.flexBasis = 90;
                pill.style.marginLeft = i == 0 ? 0 : 4;
                pill.style.marginRight = 0;
                pill.style.marginBottom = 4;
                pill.style.height = 26;
                pill.style.fontSize = 11;
                pill.style.whiteSpace = WhiteSpace.NoWrap;
                bool active = i == _step;
                bool done = i < _step || (_hasUpdateStep && i == 0 && _updateStepResolved);
                pill.style.backgroundColor = active ? Accent : (done ? new Color(Accent.r, Accent.g, Accent.b, 0.35f) : Panel);
                pill.style.color = active ? Color.white : Muted;
                RoundCorners(pill, 6);
                NoBorder(pill);
                _stepsBar.Add(pill);
            }
        }

        List<string> CurrentStepTitles()
        {
            var list = new List<string>(BaseStepTitles);
            if (_hasUpdateStep) list.Insert(0, "Update");
            return list;
        }

        void BuildUpdateStep()
        {
            if (_updateStepResolved)
            {
                _content.Add(SectionTitle("Up to date"));
                _content.Add(Note("DavanciNexus is on the latest published version. Continue to the next step."));

                var card = Card();
                card.Add(CardHeader("Update complete"));

                var current = new Label($"Installed: v{NexusVersion.Current}");
                current.style.fontSize = 11;
                current.style.color = OkGreen;
                current.style.marginBottom = 6;
                card.Add(current);

                var updateBtn = new Button(() => { }) { text = $"Up to date (v{NexusVersion.Current})" };
                StylePrimaryButton(updateBtn);
                updateBtn.SetEnabled(false);
                card.Add(updateBtn);
                _content.Add(card);
                return;
            }

            _content.Add(SectionTitle("Update available"));
            _content.Add(Note("A newer DavanciNexus is published. Updating keeps your SDKs, fixes and Doctor checks current. You can skip with Next, but updating first is recommended."));

            var card2 = Card();
            card2.Add(CardHeader($"v{_update.Version} available"));

            var installed = new Label($"Installed: v{NexusVersion.Current}");
            installed.style.fontSize = 11;
            installed.style.color = Muted;
            installed.style.marginBottom = 6;
            card2.Add(installed);

            if (!string.IsNullOrEmpty(_update.Notes))
            {
                card2.Add(MiniLabel("Release notes"));
                var notes = new Label(_update.Notes);
                notes.style.fontSize = 11;
                notes.style.color = Color.white;
                notes.style.whiteSpace = WhiteSpace.Normal;
                notes.style.marginBottom = 6;
                card2.Add(notes);
            }

            var updateBtn2 = new Button(() =>
            {
                EditorPrefs.SetString(KeyAwaitingUpdate, _update.Version);
                NexusUpdater.DownloadUpdate(_update.Version, _update.Url, success =>
                {
                    if (success)
                    {
                        NexusUpdater.ClearRemotePackageCache();
                        NexusPackageCatalog.Reload();
                        RefreshUpdateState();
                    }
                });
            }) { text = $"Update to v{_update.Version}" };
            StylePrimaryButton(updateBtn2);
            card2.Add(updateBtn2);
            _content.Add(card2);

            _content.Add(Note("The package downloads and the Unity import dialog opens. After importing, this step shows Up to date and you can continue."));

            EnsureRemotePackagesLoaded();
            BuildPackageUpdatesSection();
        }

        void EnsureRemotePackagesLoaded()
        {
            if (_remotePackagesLoaded || _remotePackagesLoading) return;
            _remotePackagesLoading = true;
            NexusUpdater.GetRemotePackages(dict =>
            {
                NexusPackageCatalog.SetRemote(dict);
                _remotePackagesLoaded = true;
                _remotePackagesLoading = false;
                RenderStep();
            });
        }

        void BuildPackageUpdatesSection()
        {
            var updates = NexusPackageCatalog.All
                .Select(p => NexusPackageCatalog.GetStatus(p.id))
                .Where(s => s.UpdateAvailable || s.Installed)
                .ToList();
            if (updates.Count == 0) return;

            var anyUpdate = updates.Any(s => s.UpdateAvailable);
            if (!anyUpdate && !_remotePackagesLoaded) return;

            _content.Add(SectionTitle("SDK package updates"));
            _content.Add(Note("Version catalog ships with DavanciNexus. When a newer SDK is published, an Update button appears here and in Install SDKs."));

            foreach (var status in updates)
            {
                if (!status.Installed && !status.UpdateAvailable) continue;
                _content.Add(PackageVersionCard(status));
            }
        }

        VisualElement PackageVersionCard(NexusPackageCatalog.PackageStatus status)
        {
            var card = Card();
            string name = status.Entry?.displayName ?? status.Entry?.id ?? "Package";

            var top = Row();
            top.style.justifyContent = Justify.SpaceBetween;
            top.style.alignItems = Align.Center;

            var title = new Label(name);
            title.style.unityFontStyleAndWeight = FontStyle.Bold;
            title.style.fontSize = 12;
            top.Add(title);

            var badge = new Label(status.Installed ? (status.UpdateAvailable ? "Update available" : "Up to date") : "Not installed");
            badge.style.fontSize = 10;
            badge.style.color = status.UpdateAvailable ? Todo : OkGreen;
            badge.style.unityFontStyleAndWeight = FontStyle.Bold;
            top.Add(badge);
            card.Add(top);

            string installed = string.IsNullOrEmpty(status.InstalledVersion) ? "—" : "v" + status.InstalledVersion;
            string bundled = string.IsNullOrEmpty(status.BundledVersion) ? "—" : "v" + status.BundledVersion;
            string remote = string.IsNullOrEmpty(status.RemoteVersion) ? "—" : "v" + status.RemoteVersion;

            var ver = new Label($"Installed: {installed}   ·   Bundled: {bundled}   ·   Remote: {remote}");
            ver.style.fontSize = 10;
            ver.style.color = Muted;
            ver.style.whiteSpace = WhiteSpace.Normal;
            ver.style.marginTop = 4;
            card.Add(ver);

            var row = Row();
            row.style.marginTop = 8;

            if (status.UpdateAvailable)
            {
                string target = status.TargetVersion ?? status.BundledVersion;
                var update = new Button(() =>
                {
                    NexusPackageUpdater.UpdatePackage(status.Entry.id, ok => { if (ok) RenderStep(); });
                }) { text = NexusPackageUpdater.UpdatingId == status.Entry.id ? "Updating…" : $"Update to v{target}" };
                StylePrimaryButton(update);
                update.style.flexGrow = 1;
                update.SetEnabled(!NexusPackageUpdater.UpdateInProgress);
                row.Add(update);
            }
            else if (!status.Installed)
            {
                var install = new Button(() =>
                {
                    NexusPackageUpdater.InstallOrUpdate(status.Entry.id, _ => RenderStep());
                }) { text = "Install" };
                StyleSecondaryButton(install);
                install.style.flexGrow = 1;
                row.Add(install);
            }

            card.Add(row);
            return card;
        }

        void BuildPlatformStep()
        {
            _content.Add(SectionTitle("Choose your target platform(s)"));
            _content.Add(Note("DavanciNexus supports both iOS and Android. Pick the active build target you want to work on. Define symbols are applied to all platforms regardless."));

            var card = Card();
            var current = NexusInstaller.ActiveBuildTarget;
            var status = new Label($"Active build target: {current}");
            status.style.color = Muted;
            status.style.marginBottom = 8;
            card.Add(status);

            bool isIos = current == BuildTarget.iOS;
            bool isAndroid = current == BuildTarget.Android;

            var row = Row();
            var ios = PlatformButton("BuildSettings.iPhone.Small",
                isIos ? "iOS (active)" : "Switch to iOS",
                () => { NexusInstaller.SwitchPlatform(BuildTarget.iOS); RenderStep(); });
            var android = PlatformButton("BuildSettings.Android.Small",
                isAndroid ? "Android (active)" : "Switch to Android",
                () => { NexusInstaller.SwitchPlatform(BuildTarget.Android); RenderStep(); });
            android.style.marginLeft = 6;
            ios.SetEnabled(!isIos);
            android.SetEnabled(!isAndroid);
            row.Add(ios); row.Add(android);
            card.Add(row);
            _content.Add(card);

            _content.Add(Note("iOS extras handled automatically at build time: NSUserTrackingUsageDescription is added to Info.plist for ATT/IDFA. Android needs no equivalent."));
        }

        Button PlatformButton(string iconName, string label, Action onClick)
        {
            var b = new Button(onClick);
            StyleSecondaryButton(b);
            b.style.flexGrow = 1;
            b.style.height = 40;
            b.style.flexDirection = FlexDirection.Row;
            b.style.alignItems = Align.Center;
            b.style.justifyContent = Justify.Center;

            var icon = EditorGUIUtility.IconContent(iconName);
            if (icon != null && icon.image != null)
            {
                var img = new Image { image = icon.image, scaleMode = ScaleMode.ScaleToFit };
                img.style.width = 20;
                img.style.height = 20;
                img.style.marginRight = 8;
                b.Add(img);
            }

            var lbl = new Label(label);
            lbl.style.color = Color.white;
            lbl.style.fontSize = 12;
            b.Add(lbl);
            return b;
        }

        void BuildInstallStep()
        {
            _content.Add(SectionTitle("Install SDK packages"));
            _content.Add(Note("Install only what you need. Firebase already bundles the External Dependency Manager, so you usually don't need to install EDM4U separately."));

            int reconciled = NexusPackageCatalog.ReconcileLegacyInstalls();
            if (reconciled > 0)
                _content.Add(Note($"Recorded versions for {reconciled} already-installed SDK(s). Update buttons only appear when a newer version is published."));

            var catalogRow = Row();
            catalogRow.style.alignItems = Align.Center;
            catalogRow.style.marginBottom = 6;
            var catalogLabel = new Label("Version catalog: Assets/DavanciNexus/Editor/NexusPackageVersions.json");
            catalogLabel.style.fontSize = 10;
            catalogLabel.style.color = Muted;
            catalogLabel.style.flexGrow = 1;
            catalogRow.Add(catalogLabel);
            var openCatalog = new Button(() =>
            {
                var asset = AssetDatabase.LoadAssetAtPath<TextAsset>("Assets/DavanciNexus/Editor/NexusPackageVersions.json");
                if (asset != null) Selection.activeObject = asset;
                EditorGUIUtility.PingObject(asset);
            }) { text = "Open catalog" };
            StyleSecondaryButton(openCatalog);
            catalogRow.Add(openCatalog);
            _content.Add(catalogRow);

            EnsureRemotePackagesLoaded();

            foreach (var sdk in NexusInstaller.Sdks)
                _content.Add(SdkCard(sdk));

            _content.Add(MiniLabel("Ads, IAP & notifications"));
            foreach (var id in new[] { "applovin_max", "levelplay", "unity_iap", "mobile_notifications" })
            {
                var status = NexusPackageCatalog.GetStatus(id);
                if (status.Entry != null)
                    _content.Add(PackageVersionCard(status));
            }

            var actions = Row();
            actions.style.marginTop = 8;
            var all = new Button(NexusInstaller.ImportAll) { text = "Install All" };
            StylePrimaryButton(all); all.style.flexGrow = 1;
            var removeAll = new Button(() => { NexusInstaller.RemoveAll(); RenderStep(); }) { text = "Remove All" };
            StyleDangerButton(removeAll); removeAll.style.flexGrow = 1; removeAll.style.marginLeft = 6;
            actions.Add(all); actions.Add(removeAll);
            _content.Add(actions);

            var refresh = new Button(RenderStep) { text = "Refresh status" };
            StyleSecondaryButton(refresh); refresh.style.marginTop = 6;
            _content.Add(refresh);
        }

        VisualElement SdkCard(NexusSdk sdk)
        {
            var card = Card();
            bool installed = sdk.IsInstalled;

            var top = Row();
            top.style.alignItems = Align.Center;

            top.Add(LogoElement(sdk));

            var textCol = new VisualElement();
            textCol.style.flexGrow = 1;
            textCol.style.marginLeft = 10;

            var titleRow = Row();
            titleRow.style.justifyContent = Justify.SpaceBetween;
            titleRow.style.alignItems = Align.Center;

            var name = new Label(sdk.DisplayName);
            name.style.unityFontStyleAndWeight = FontStyle.Bold;
            name.style.fontSize = 12;
            titleRow.Add(name);

            var badge = new Label(installed ? "Installed" : "Not installed");
            badge.style.fontSize = 10;
            badge.style.color = installed ? OkGreen : Todo;
            badge.style.unityFontStyleAndWeight = FontStyle.Bold;
            titleRow.Add(badge);
            textCol.Add(titleRow);

            var pkgStatus = NexusPackageCatalog.GetStatus(sdk.Id);
            if (installed || pkgStatus.UpdateAvailable)
            {
                string installedVer = string.IsNullOrEmpty(pkgStatus.InstalledVersion)
                    ? "unknown (not recorded yet)"
                    : "v" + pkgStatus.InstalledVersion;
                string latest = pkgStatus.UpdateAvailable
                    ? $" → v{pkgStatus.TargetVersion} available"
                    : (string.IsNullOrEmpty(pkgStatus.InstalledVersion) ? "" : " ✓");
                var verLine = new Label(installed ? $"Version: {installedVer}{latest}" : "");
                verLine.style.fontSize = 10;
                verLine.style.color = pkgStatus.UpdateAvailable ? Todo : Muted;
                verLine.style.marginTop = 2;
                textCol.Add(verLine);
            }

            var desc = new Label(sdk.Description);
            desc.style.fontSize = 10;
            desc.style.color = Muted;
            desc.style.whiteSpace = WhiteSpace.Normal;
            desc.style.marginTop = 2;
            textCol.Add(desc);

            top.Add(textCol);
            card.Add(top);

            var buttons = Row();
            buttons.style.marginTop = 8;

            bool busy = NexusPackageUpdater.UpdateInProgress;
            bool importingThis = NexusPackageUpdater.UpdatingId == sdk.Id;

            if (pkgStatus.UpdateAvailable)
            {
                var update = new Button(() =>
                {
                    NexusPackageUpdater.UpdatePackage(sdk.Id, ok => { if (ok) RenderStep(); });
                }) { text = importingThis ? "Updating…" : $"Update to v{pkgStatus.TargetVersion}" };
                StylePrimaryButton(update);
                update.style.flexGrow = 1;
                update.SetEnabled(!busy);
                buttons.Add(update);
            }
            else
            {
                var install = new Button(() => { NexusInstaller.Import(sdk); })
                {
                    text = importingThis ? "Importing…" : installed ? "Reimport" : "Install"
                };
                StyleSecondaryButton(install); install.style.flexGrow = 1;
                install.SetEnabled(!busy);
                buttons.Add(install);
            }

            var remove = new Button(() => { NexusInstaller.Remove(sdk); RenderStep(); }) { text = "Remove" };
            StyleDangerButton(remove); remove.style.flexGrow = 1; remove.style.marginLeft = 6;
            remove.SetEnabled(installed && !busy);
            buttons.Add(remove);
            card.Add(buttons);

            return card;
        }

        // The SDK's shipped logo, or a colored initials badge as fallback.
        VisualElement LogoElement(NexusSdk sdk)
        {
            const int size = 46;
            const int pad = 6;

            Texture2D tex = string.IsNullOrEmpty(sdk.LogoAssetPath)
                ? null
                : AssetDatabase.LoadAssetAtPath<Texture2D>(sdk.LogoAssetPath);

            if (tex != null)
            {
                // White tile so dark logos stay visible on the dark editor background.
                var tile = new VisualElement();
                tile.style.width = size;
                tile.style.height = size;
                tile.style.flexShrink = 0;
                tile.style.backgroundColor = Color.white;
                tile.style.alignItems = Align.Center;
                tile.style.justifyContent = Justify.Center;
                tile.style.paddingLeft = pad; tile.style.paddingRight = pad;
                tile.style.paddingTop = pad; tile.style.paddingBottom = pad;
                RoundCorners(tile, 10);

                var img = new Image { image = tex, scaleMode = ScaleMode.ScaleToFit };
                img.style.width = size - pad * 2;
                img.style.height = size - pad * 2;
                tile.Add(img);
                return tile;
            }

            var badge = new VisualElement();
            badge.style.width = size;
            badge.style.height = size;
            badge.style.flexShrink = 0;
            badge.style.backgroundColor = sdk.BrandColor;
            badge.style.alignItems = Align.Center;
            badge.style.justifyContent = Justify.Center;
            RoundCorners(badge, 10);

            var initials = new Label(string.IsNullOrEmpty(sdk.Initials) ? "?" : sdk.Initials);
            initials.style.color = Color.white;
            initials.style.unityFontStyleAndWeight = FontStyle.Bold;
            initials.style.fontSize = 14;
            badge.Add(initials);
            return badge;
        }

        void BuildConfigStep()
        {
            _content.Add(SectionTitle("Keys, IDs & startup"));

            if (_config == null)
            {
                _content.Add(Note("No NexusConfig found."));
                var create = new Button(() => { _config = LoadOrCreateConfig(); RenderStep(); }) { text = "Create NexusConfig" };
                StylePrimaryButton(create);
                _content.Add(create);
                return;
            }

            var startup = Card();
            startup.Add(CardHeader("Permissions"));
            startup.Add(ConfigToggle("Request ATT (iOS only)", _config.requestATT, v => _config.requestATT = v));
            startup.Add(Note("Push notifications and D1/D3/D7 retention are configured in the Notifications step."));
            _content.Add(startup);

            var af = Card();
            af.Add(CardHeader("AppsFlyer"));
            af.Add(ConfigText("Dev Key", _config.appsFlyerDevKey, v => _config.appsFlyerDevKey = v));
            af.Add(ConfigText("Apple App ID", _config.appsFlyerAppID, v => _config.appsFlyerAppID = v));
            af.Add(ConfigToggle("Mirror custom events to AppsFlyer", _config.appsFlyerMirrorEvents, v => _config.appsFlyerMirrorEvents = v));
            af.Add(Note("IAP revenue (af_purchase) and ad revenue always reach AppsFlyer. This also forwards every Custom()/design event — leave off to stay within AppsFlyer's event limits."));
            _content.Add(af);

            var ga = Card();
            ga.Add(CardHeader("GameAnalytics"));
            ga.Add(Note("Paste the keys from your GameAnalytics dashboard. 'Apply' writes them straight into the GameAnalytics settings asset, no need to open Window → GameAnalytics."));

            ga.Add(MiniLabel("iOS"));
            ga.Add(ConfigText("Game Key", _config.gaIosGameKey, v => _config.gaIosGameKey = v));
            ga.Add(ConfigText("Secret Key", _config.gaIosSecretKey, v => _config.gaIosSecretKey = v));

            ga.Add(MiniLabel("Android"));
            ga.Add(ConfigText("Game Key", _config.gaAndroidGameKey, v => _config.gaAndroidGameKey = v));
            ga.Add(ConfigText("Secret Key", _config.gaAndroidSecretKey, v => _config.gaAndroidSecretKey = v));

            var gaStatus = new Label();
            gaStatus.style.fontSize = 10;
            gaStatus.style.color = Muted;
            gaStatus.style.whiteSpace = WhiteSpace.Normal;
            gaStatus.style.marginTop = 6;

            var applyBtn = new Button(() =>
            {
                if (!NexusGameAnalyticsSetup.IsAvailable)
                {
                    gaStatus.text = "GameAnalytics isn't installed yet, install it in step 2 first.";
                    gaStatus.style.color = Todo;
                    return;
                }
                string result = NexusGameAnalyticsSetup.Apply(
                    _config.gaIosGameKey, _config.gaIosSecretKey,
                    _config.gaAndroidGameKey, _config.gaAndroidSecretKey);
                bool ok = result.StartsWith("Applied");

                // Without the economy types GA drops every CurrencyEarned/Spent, so do both.
                if (ok)
                    result += " " + NexusGameAnalyticsSetup.RegisterEconomyDefaults();

                gaStatus.text = result;
                gaStatus.style.color = ok ? OkGreen : Todo;
            })
            { text = "Apply to GameAnalytics" };
            StylePrimaryButton(applyBtn);
            applyBtn.style.marginTop = 6;
            ga.Add(applyBtn);
            ga.Add(gaStatus);

            ga.Add(MiniLabel("Economy resource types"));
            ga.Add(Note("'Apply' already registers a generic starter set of currencies (coins/cash/gems) and item types so GameAnalytics accepts DavanciNexus.CurrencyEarned/Spent. Use this button to re-register after adding more, or add game-specific ones in Window → GameAnalytics."));
            var econStatus = new Label();
            econStatus.style.fontSize = 10;
            econStatus.style.color = Muted;
            econStatus.style.whiteSpace = WhiteSpace.Normal;
            econStatus.style.marginTop = 4;
            var econBtn = new Button(() =>
            {
                if (!NexusGameAnalyticsSetup.IsAvailable)
                {
                    econStatus.text = "GameAnalytics isn't installed yet, install it in step 2 first.";
                    econStatus.style.color = Todo;
                    return;
                }
                string result = NexusGameAnalyticsSetup.RegisterEconomyDefaults();
                econStatus.text = result;
                econStatus.style.color = result.StartsWith("Registered") || result.Contains("already")
                    ? OkGreen : Todo;
            })
            { text = "Register economy resource types" };
            StyleSecondaryButton(econBtn);
            econBtn.style.marginTop = 6;
            ga.Add(econBtn);
            ga.Add(econStatus);
            _content.Add(ga);

        }

        void BuildMonetizationStep()
        {
            _content.Add(SectionTitle("Monetization : Ads & In-App Purchases"));

            if (_config == null)
            {
                _content.Add(Note("No NexusConfig found, finish the Configure step first."));
                return;
            }

            _content.Add(Note("Everything that earns revenue lives here: ads (rewarded, interstitial, in-game banner/MREC) and Unity IAP purchases. Both are off until you enable them, so games that don't monetize are unaffected."));

            BuildAdsShared();
            BuildAdsUsageDocs();
            if (_config.adMediation == NexusMediation.LevelPlay)
                BuildLevelPlayCard();
            else
                BuildAdsCard();
            BuildIapCard();
        }

        void BuildAdsUsageDocs()
        {
            var usage = Card();
            usage.Add(CardHeader("Game hookup & ready events"));
            usage.Add(Note("Grant double XP, free skins, coins, etc. only when r.Rewarded is true (full watch). " +
                "Nexus sends ad_watched analytics on the same condition — early closes log ad_failed, not ad_watched. " +
                "ad_revenue is separate (impression LTV). Subscribe to ready events so buttons stay disabled until fill."));

            const string showSnippet =
                "DavanciMonetization.ShowRewardedAd(\"coins_x2\", r =>\n" +
                "{\n" +
                "    if (r.Rewarded) GrantDoubleXp();   // same gate as ad_watched analytics\n" +
                "});\n" +
                "DavanciMonetization.ShowInterstitialAd(\"level_end\");\n" +
                "DavanciMonetization.ShowBanner(\"hud_bottom\");   // HideBanner() to remove";
            usage.Add(SnippetHeader("Show ads & grant rewards", showSnippet));
            usage.Add(CodeLine("DavanciMonetization.ShowRewardedAd(\"coins_x2\", r =>"));
            usage.Add(CodeLine("    { if (r.Rewarded) GrantDoubleXp(); });"));
            usage.Add(CodeLine("DavanciMonetization.ShowInterstitialAd(\"level_end\");"));
            usage.Add(CodeLine("DavanciMonetization.ShowBanner(\"hud_bottom\");"));

            const string readySnippet =
                "void OnEnable()\n" +
                "{\n" +
                "    DavanciMonetization.AdsInitialized += ok =>\n" +
                "        Debug.Log(\"Ads init: \" + ok);\n" +
                "    DavanciMonetization.RewardedReadyChanged += ready =>\n" +
                "        rewardedButton.interactable = ready;\n" +
                "    DavanciMonetization.InterstitialReadyChanged += ready =>\n" +
                "        interstitialButton.interactable = ready;\n" +
                "}\n\n" +
                "void OnDisable()\n" +
                "{\n" +
                "    DavanciMonetization.AdsInitialized -= OnAdsInitialized;\n" +
                "    DavanciMonetization.RewardedReadyChanged -= OnRewardedReadyChanged;\n" +
                "    DavanciMonetization.InterstitialReadyChanged -= OnInterstitialReadyChanged;\n" +
                "}";
            usage.Add(SnippetHeader("Ready events (disable buttons until fill)", readySnippet));
            usage.Add(CodeLine("DavanciMonetization.AdsInitialized += ok => { ... };"));
            usage.Add(CodeLine("DavanciMonetization.RewardedReadyChanged += ready => { ... };"));
            usage.Add(CodeLine("DavanciMonetization.InterstitialReadyChanged += ready => { ... };"));
            usage.Add(Note("Late subscribers get the current ready state immediately. Poll IsRewardedReady only if you cannot use events."));
            _content.Add(usage);
        }

        void BuildAdsShared()
        {
            var card = Card();
            card.Add(CardHeader("Ads"));
            card.Add(Note("Rewarded, interstitial and in-game (banner/MREC) ads. Call DavanciMonetization.ShowRewardedAd / ShowInterstitialAd / ShowBanner from your game. When off, no ad calls are made."));

            card.Add(ConfigToggle("Enable Ads", _config.enableAds, v => _config.enableAds = v));
            card.Add(ConfigToggle("Auto-initialize at startup", _config.adsAutoInitialize, v => _config.adsAutoInitialize = v));
            card.Add(ConfigToggle("Test mode (show test ads : OFF for production)", _config.adsTestMode, v => _config.adsTestMode = v));

            var mediationChoices = new System.Collections.Generic.List<string> { "AppLovin MAX", "LevelPlay" };
            var mediation = new DropdownField("Active mediation", mediationChoices, (int)_config.adMediation);
            mediation.tooltip = "Which SDK serves ads at runtime. Both can be installed, switch when you're ready.";
            mediation.RegisterValueChangedCallback(e =>
            {
                _config.adMediation = e.newValue == "LevelPlay" ? NexusMediation.LevelPlay : NexusMediation.AppLovinMax;
                SaveConfig();
                RenderStep();   // swap the provider-specific card below
            });
            card.Add(mediation);

            _content.Add(card);
        }

        void BuildAdsCard()
        {
            var ads = Card();
            ads.Add(CardHeader("AppLovin MAX"));

            bool installed = NexusAdsInstaller.Installed;
            bool defined = NexusAdsInstaller.DefineSet;

            var pkgRow = Row();
            pkgRow.style.alignItems = Align.Center;
            pkgRow.style.marginTop = 4;
            var pkgStatus = new Label(installed
                ? (defined ? "AppLovin MAX installed ✓" : "AppLovin MAX installed,define not set")
                : "AppLovin MAX not installed");
            pkgStatus.style.fontSize = 11;
            pkgStatus.style.flexGrow = 1;
            pkgStatus.style.color = installed && defined ? OkGreen : Todo;
            pkgRow.Add(pkgStatus);

            if (!installed)
            {
                var import = new Button(NexusAdsInstaller.Import) { text = "Import AppLovin MAX" };
                StyleSecondaryButton(import);
                import.tooltip = "Imports the bundled AppLovin MAX .unitypackage from InstallerPackages/ and sets DAVANCI_ADS.";
                import.SetEnabled(NexusAdsInstaller.PackageBundled);
                pkgRow.Add(import);
            }
            else if (!defined)
            {
                var enable = new Button(() => { NexusAdsInstaller.SetDefine(true); RenderStep(); }) { text = "Enable" };
                StyleSecondaryButton(enable);
                pkgRow.Add(enable);
            }
            ads.Add(pkgRow);

            if (!installed && !NexusAdsInstaller.PackageBundled)
                ads.Add(Note("AppLovin MAX package not found in InstallerPackages/. Drop the AppLovin-MAX-Unity-Plugin .unitypackage there, then press Import."));

            var sdkStatus = new Label();
            sdkStatus.style.fontSize = 10;
            sdkStatus.style.whiteSpace = WhiteSpace.Normal;
            sdkStatus.style.marginTop = 4;
            sdkStatus.style.color = Muted;

            ads.Add(ConfigText("AppLovin SDK Key", _config.applovinSdkKey, v => _config.applovinSdkKey = v));
            var applyKey = new Button(() =>
            {
                bool ok = NexusAdsInstaller.ApplySdkKey(_config.applovinSdkKey, out string msg);
                sdkStatus.text = msg;
                sdkStatus.style.color = ok ? OkGreen : Todo;
            }) { text = "Apply SDK key to AppLovin" };
            StyleSecondaryButton(applyKey);
            applyKey.style.marginTop = 4;
            applyKey.tooltip = "Writes the key into AppLovinSettings so you never open AppLovin's Integration Manager.";
            applyKey.SetEnabled(installed);
            ads.Add(applyKey);
            ads.Add(sdkStatus);

            if (installed && defined)
                BuildNetworksSection(ads);

            ads.Add(MiniLabel("Ad unit ID: Rewarded"));
            ads.Add(ConfigText("iOS", _config.iosRewardedAdUnitId, v => _config.iosRewardedAdUnitId = v));
            ads.Add(ConfigText("Android", _config.androidRewardedAdUnitId, v => _config.androidRewardedAdUnitId = v));

            ads.Add(MiniLabel("Ad unit IDs : Interstitial"));
            ads.Add(ConfigText("iOS", _config.iosInterstitialAdUnitId, v => _config.iosInterstitialAdUnitId = v));
            ads.Add(ConfigText("Android", _config.androidInterstitialAdUnitId, v => _config.androidInterstitialAdUnitId = v));

            ads.Add(MiniLabel("Ad unit IDs : In-game (Banner / MREC)"));
            ads.Add(ConfigText("iOS", _config.iosBannerAdUnitId, v => _config.iosBannerAdUnitId = v));
            ads.Add(ConfigText("Android", _config.androidBannerAdUnitId, v => _config.androidBannerAdUnitId = v));

            ads.Add(Note("Ad unit IDs and the SDK key come from your AppLovin dashboard. Use the sample scene to test the show/reward flow with no SDK."));

            var docs = new Button(NexusAdsInstaller.OpenDocs) { text = "AppLovin integration docs" };
            StyleSecondaryButton(docs); docs.style.marginTop = 6;
            ads.Add(docs);

            _content.Add(ads);
        }

        void BuildLevelPlayCard()
        {
            var lp = Card();
            lp.Add(CardHeader("LevelPlay / ironSource"));

            bool installed = NexusLevelPlayInstaller.Installed;
            bool defined = NexusLevelPlayInstaller.DefineSet;

            var pkgRow = Row();
            pkgRow.style.alignItems = Align.Center;
            pkgRow.style.marginTop = 4;
            var pkgStatus = new Label(installed
                ? (defined ? "LevelPlay installed ✓" : "LevelPlay installed, define not set")
                : "LevelPlay not installed");
            pkgStatus.style.fontSize = 11;
            pkgStatus.style.flexGrow = 1;
            pkgStatus.style.color = installed && defined ? OkGreen : Todo;
            pkgRow.Add(pkgStatus);

            if (!installed)
            {
                var import = new Button(NexusLevelPlayInstaller.Import) { text = "Import LevelPlay" };
                StyleSecondaryButton(import);
                import.tooltip = "Imports the bundled LevelPlay .unitypackage from InstallerPackages/ and sets DAVANCI_LEVELPLAY.";
                import.SetEnabled(NexusLevelPlayInstaller.PackageBundled);
                pkgRow.Add(import);
            }
            else if (!defined)
            {
                var enable = new Button(() => { NexusLevelPlayInstaller.SetDefine(true); RenderStep(); }) { text = "Enable" };
                StyleSecondaryButton(enable);
                pkgRow.Add(enable);
            }

            if (installed)
            {
                var remove = new Button(() => { NexusLevelPlayInstaller.Remove(); RenderStep(); }) { text = "Remove" };
                StyleSecondaryButton(remove);
                remove.tooltip = "Deletes the LevelPlay / ironSource folders and clears DAVANCI_LEVELPLAY.";
                pkgRow.Add(remove);
            }
            lp.Add(pkgRow);

            if (!installed && !NexusLevelPlayInstaller.PackageBundled)
                lp.Add(Note("LevelPlay package not found in InstallerPackages/. Drop the LevelPlay (ironSource) .unitypackage there, then press Import."));

            lp.Add(MiniLabel("App Key (per platform)"));
            lp.Add(ConfigText("iOS", _config.iosLevelPlayAppKey, v => _config.iosLevelPlayAppKey = v));
            lp.Add(ConfigText("Android", _config.androidLevelPlayAppKey, v => _config.androidLevelPlayAppKey = v));

            lp.Add(MiniLabel("Ad unit IDs : Rewarded"));
            lp.Add(ConfigText("iOS", _config.iosLpRewardedAdUnitId, v => _config.iosLpRewardedAdUnitId = v));
            lp.Add(ConfigText("Android", _config.androidLpRewardedAdUnitId, v => _config.androidLpRewardedAdUnitId = v));

            var more = new Foldout { text = "Interstitial & Banner / MREC", value = false };
            more.style.marginTop = 4;
            more.Add(MiniLabel("Ad unit IDs : Interstitial"));
            more.Add(ConfigText("iOS", _config.iosLpInterstitialAdUnitId, v => _config.iosLpInterstitialAdUnitId = v));
            more.Add(ConfigText("Android", _config.androidLpInterstitialAdUnitId, v => _config.androidLpInterstitialAdUnitId = v));
            more.Add(MiniLabel("Ad unit IDs : In-game (Banner / MREC)"));
            more.Add(ConfigText("iOS", _config.iosLpBannerAdUnitId, v => _config.iosLpBannerAdUnitId = v));
            more.Add(ConfigText("Android", _config.androidLpBannerAdUnitId, v => _config.androidLpBannerAdUnitId = v));
            lp.Add(more);

            lp.Add(ConfigToggle("Test-Suite mode (real ads OFF — for the Mediation Test Suite only)",
                _config.adsTestSuiteMode, v => _config.adsTestSuiteMode = v));
            lp.Add(Note("Keep Test-Suite mode OFF to serve real ads. ironSource keeps debug and production " +
                "serving separate: while it's ON, every rewarded/interstitial load returns 509 \"No fill\". " +
                "Turn it ON only to open the Mediation Test Suite, then back OFF for real ads."));

            var nmBtn = new Button(NexusLevelPlayInstaller.OpenNetworkManager) { text = "Open LevelPlay Network Manager" };
            StyleSecondaryButton(nmBtn);
            nmBtn.style.marginTop = 4;
            nmBtn.tooltip = "Install or update Meta, Liftoff, Unity Ads, and other LevelPlay adapters.";
            lp.Add(nmBtn);

            lp.Add(Note("App Key + ad unit IDs come from your ironSource / LevelPlay dashboard."));

            var docs = new Button(NexusLevelPlayInstaller.OpenDocs) { text = "LevelPlay integration docs" };
            StyleSecondaryButton(docs); docs.style.marginTop = 6;
            lp.Add(docs);

            _content.Add(lp);
        }

        void BuildNetworksSection(VisualElement ads)
        {
            ads.Add(MiniLabel("Mediation networks"));
            ads.Add(Note("Add network adapters here,no need to open AppLovin's Integration Manager. Recommended to start: Meta, Unity, Mintegral, Vungle, Pangle, ironSource. AppLovin's own Exchange is built in."));

            var loadRow = Row();
            var loadBtn = new Button(() =>
            {
                _adNetworksMsg = "Loading networks…";
                bool started = NexusAdsNetworks.LoadAsync(list =>
                {
                    _adNetworks = list;
                    _adNetworksMsg = list == null
                        ? "Couldn't load networks (check your internet connection)."
                        : $"{list.Count} networks available.";
                    RenderStep();
                });
                if (!started)
                    _adNetworksMsg = "AppLovin Integration Manager not available, import AppLovin MAX first.";
                RenderStep();
            }) { text = _adNetworks == null ? "Load available networks" : "Refresh networks" };
            StyleSecondaryButton(loadBtn); loadBtn.style.flexGrow = 1;
            loadRow.Add(loadBtn);
            ads.Add(loadRow);

            if (!string.IsNullOrEmpty(_adNetworksMsg))
            {
                var msg = new Label(_adNetworksMsg);
                msg.style.fontSize = 10;
                msg.style.color = Muted;
                msg.style.whiteSpace = WhiteSpace.Normal;
                msg.style.marginTop = 4;
                ads.Add(msg);
            }

            if (_adNetworks == null) return;

            foreach (var net in _adNetworks)
                ads.Add(NetworkRow(net));
        }

        VisualElement NetworkRow(NexusAdsNetworks.Net net)
        {
            var row = Row();
            row.style.justifyContent = Justify.SpaceBetween;
            row.style.alignItems = Align.Center;
            row.style.marginBottom = 3;

            // Recommended networks get a medal-tier star: gold / silver / bronze for the top three,
            // a soft amber for the rest. Rank is the position in NexusAdsNetworks.Recommended.
            int rank = Array.IndexOf(NexusAdsNetworks.Recommended, net.Name);
            if (rank >= 0)
            {
                var star = new Label("★");
                star.style.fontSize = 13;
                star.style.color = MedalColor(rank);
                star.style.marginRight = 4;
                star.style.unityTextAlign = TextAnchor.MiddleCenter;
                row.Add(star);
            }

            var name = new Label(net.DisplayName);
            name.style.fontSize = 11;
            name.style.color = net.Installed ? Color.white : Muted;
            name.style.flexGrow = 1;
            row.Add(name);

            var state = new Label(net.Installed
                ? (net.RequiresUpdate ? $"v{net.CurrentVersion} → v{net.LatestVersion}" : $"v{net.CurrentVersion} ✓")
                : "");
            state.style.fontSize = 10;
            state.style.color = net.RequiresUpdate ? Todo : OkGreen;
            state.style.marginRight = 6;
            row.Add(state);

            string label = !net.Installed ? "Install" : (net.RequiresUpdate ? "Update" : "Reinstall");
            var btn = new Button(() =>
            {
                if (NexusAdsNetworks.Install(net, out string msg))
                    _adNetworksMsg = msg;
                else
                    _adNetworksMsg = msg;
                RenderStep();
            }) { text = label };
            StyleSecondaryButton(btn);
            btn.style.width = 90;
            row.Add(btn);
            return row;
        }

        // Gold / silver / bronze for the top three recommended networks, soft amber beyond.
        static Color MedalColor(int rank)
        {
            switch (rank)
            {
                case 0: return new Color(1.00f, 0.84f, 0.20f);  // gold
                case 1: return new Color(0.80f, 0.82f, 0.86f);  // silver
                case 2: return new Color(0.82f, 0.52f, 0.25f);  // bronze
                default: return new Color(0.70f, 0.62f, 0.40f); // soft amber
            }
        }

        void BuildIapCard()
        {
            var iap = Card();
            iap.Add(CardHeader("In-App Purchases"));
            iap.Add(Note("Enable IAP here — Nexus turns on Unity Services In-App Purchasing, installs Unity IAP, generates AppleTangle (iOS) automatically, and wires purchase analytics. For Android receipt validation, paste your Play Console licensing key below."));

            var enableIap = new Toggle("Enable IAP") { value = _config.enableIap };
            enableIap.style.fontSize = 11;
            enableIap.RegisterValueChangedCallback(e =>
            {
                _config.enableIap = e.newValue;
                SaveConfig();
                NexusIapPipeline.SyncFromConfig(_config);
                RenderStep();
            });
            iap.Add(enableIap);

            bool servicesOn = NexusUnityPurchasingServices.IsEnabled;
            var svcStatus = new Label(servicesOn
                ? "Unity Services In-App Purchasing: enabled ✓"
                : "Unity Services In-App Purchasing: off (enabled automatically when IAP is on)");
            svcStatus.style.fontSize = 11;
            svcStatus.style.color = servicesOn ? OkGreen : Muted;
            svcStatus.style.marginTop = 2;
            iap.Add(svcStatus);
            iap.Add(ConfigToggle("Auto-initialize at startup", _config.iapAutoInitialize, v => _config.iapAutoInitialize = v));
            iap.Add(ConfigToggle("Use real store in Editor (experimental)", _config.iapUseRealStoreInEditor,
                v => _config.iapUseRealStoreInEditor = v));

            bool installed = NexusIapInstaller.Installed;
            bool defined = NexusIapInstaller.DefineSet;

            var pkgRow = Row();
            pkgRow.style.alignItems = Align.Center;
            pkgRow.style.marginTop = 4;
            var pkgStatus = new Label(installed
                ? (defined ? "Unity IAP installed ✓" : "Unity IAP installed, define not set")
                : "Unity IAP not installed");
            pkgStatus.style.fontSize = 11;
            pkgStatus.style.flexGrow = 1;
            pkgStatus.style.color = installed && defined ? OkGreen : Todo;
            pkgRow.Add(pkgStatus);

            if (!installed)
            {
                var install = new Button(NexusIapInstaller.Install) { text = "Install Unity IAP" };
                StyleSecondaryButton(install);
                install.tooltip = "Adds com.unity.purchasing via Package Manager, then sets DAVANCI_IAP.";
                pkgRow.Add(install);
            }
            else if (!defined)
            {
                var enable = new Button(() => { NexusIapInstaller.SetDefine(true); RenderStep(); }) { text = "Enable" };
                StyleSecondaryButton(enable);
                pkgRow.Add(enable);
            }
            iap.Add(pkgRow);

            if (installed && defined)
            {
                bool tangles = NexusIapValidationInstaller.TanglesPresent;
                var validate = new Toggle("Local receipt validation") { value = NexusIapInstaller.ValidationEnabled };
                validate.style.fontSize = 11;
                validate.tooltip = "Uses AppleTangle.cs / GooglePlayTangle.cs — Nexus generates these when IAP is enabled.";
                validate.RegisterValueChangedCallback(e => NexusIapInstaller.SetValidation(e.newValue));
                iap.Add(validate);

                var tangleStatus = new Label(tangles
                    ? $"Receipt tangles: Apple{(NexusIapValidationInstaller.AppleTanglePresent ? " ✓" : "")} · Google{(NexusIapValidationInstaller.GoogleTanglePresent ? " ✓" : " (paste licensing key)")}"
                    : "Receipt tangles: will generate when Unity IAP is installed");
                tangleStatus.style.fontSize = 11;
                tangleStatus.style.color = NexusIapValidationInstaller.AppleTanglePresent ? OkGreen : Todo;
                tangleStatus.style.marginTop = 4;
                iap.Add(tangleStatus);

                iap.Add(MiniLabel("Google Play licensing key (Android receipt validation)"));
                var gpKey = new TextField { value = _config.googlePlayLicensingKey ?? "" };
                gpKey.style.fontSize = 11;
                gpKey.tooltip = "Play Console → Monetize with Play → Monetization setup → Licensing → Base64-encoded RSA public key.";
                gpKey.RegisterValueChangedCallback(e =>
                {
                    _config.googlePlayLicensingKey = e.newValue;
                    SaveConfig();
                    if (_config.enableIap && !string.IsNullOrWhiteSpace(e.newValue))
                    {
                        string err = NexusIapTangleSetup.GenerateGoogle(e.newValue);
                        if (!string.IsNullOrEmpty(err))
                            Debug.LogWarning($"[Nexus] GooglePlayTangle: {err}");
                        NexusIapValidationInstaller.Sync();
                        RenderStep();
                    }
                });
                iap.Add(gpKey);

                var regen = new Button(() =>
                {
                    NexusIapTangleSetup.GenerateAll(_config);
                    NexusIapValidationInstaller.Sync();
                    RenderStep();
                }) { text = "Regenerate receipt tangles" };
                StyleSecondaryButton(regen);
                regen.style.marginTop = 4;
                iap.Add(regen);
            }

            iap.Add(MiniLabel("Products (IDs must match the store consoles)"));
            var list = new VisualElement();
            iap.Add(list);
            RebuildProductRows(list);

            var add = new Button(() =>
            {
                _config.iapProducts.Add(new NexusProduct { id = "", kind = NexusProductKind.Consumable });
                SaveConfig();
                RebuildProductRows(list);
            }) { text = "+ Add product" };
            StyleSecondaryButton(add); add.style.marginTop = 4;
            iap.Add(add);

            const string buySnippet =
                "var price = DavanciNexus.GetPrice(\"coins_100\");\n" +
                "if (price.IsAvailable)\n" +
                "    priceLabel.text = price.Display;   // e.g. \"$0.99\"\n" +
                "    // price.Amount + price.Currency for custom formatting\n" +
                "DavanciNexus.Buy(\"coins_100\", r =>\n" +
                "{\n" +
                "    if (r.Success) GrantCoins(100);\n" +
                "});\n" +
                "DavanciNexus.RestorePurchases();   // iOS needs a Restore button\n" +
                "if (DavanciNexus.OwnsProduct(\"remove_ads\")) HideAds();";
            iap.Add(SnippetHeader("Game hookup", buySnippet));
            iap.Add(CodeLine("var price = DavanciNexus.GetPrice(\"coins_100\");"));
            iap.Add(CodeLine("priceLabel.text = price.Display;   // \"$0.99\" — after IAP ready"));
            iap.Add(CodeLine("DavanciNexus.Buy(\"coins_100\", r =>"));
            iap.Add(CodeLine("    { if (r.Success) GrantCoins(100); });"));
            iap.Add(CodeLine("DavanciNexus.RestorePurchases();   // iOS needs a Restore button"));
            iap.Add(CodeLine("if (DavanciNexus.OwnsProduct(\"remove_ads\")) HideAds();"));
            iap.Add(Note("The same product IDs must also exist in App Store Connect + Play Console."));
            _content.Add(iap);
        }

        void RebuildProductRows(VisualElement list)
        {
            list.Clear();
            var kinds = new List<string> { "Consumable", "NonConsumable" };
            for (int i = 0; i < _config.iapProducts.Count; i++)
            {
                int idx = i;
                var p = _config.iapProducts[idx];
                if (p == null) continue;

                var row = Row();
                row.style.alignItems = Align.Center;
                row.style.marginBottom = 3;

                var idField = new TextField { value = p.id };
                idField.style.flexGrow = 1;
                idField.style.fontSize = 11;
                idField.RegisterValueChangedCallback(e => { p.id = e.newValue; SaveConfig(); });
                row.Add(idField);

                var kind = new DropdownField(kinds, (int)p.kind);
                kind.style.width = 130;
                kind.style.marginLeft = 4;
                kind.RegisterValueChangedCallback(e =>
                {
                    p.kind = e.newValue == "NonConsumable" ? NexusProductKind.NonConsumable : NexusProductKind.Consumable;
                    SaveConfig();
                });
                row.Add(kind);

                var del = new Button(() => { _config.iapProducts.RemoveAt(idx); SaveConfig(); RebuildProductRows(list); }) { text = "✕" };
                StyleDangerButton(del); del.style.width = 26; del.style.marginLeft = 4;
                row.Add(del);

                list.Add(row);
            }
        }

        void BuildRatingsStep()
        {
            _content.Add(SectionTitle("Player ratings"));

            if (_config == null)
            {
                _content.Add(Note("No NexusConfig found, finish the Configure step first."));
                return;
            }

            var ids = Card();
            ids.Add(CardHeader("Store"));
            ids.Add(ConfigText("iOS App Store ID", _config.iosAppStoreId, v => _config.iosAppStoreId = v));
            ids.Add(ConfigText("Android package (optional)", _config.androidPackageName, v => _config.androidPackageName = v));
            ids.Add(DbUrlField());
            _content.Add(ids);

            const string snippet = "NexusReviewPopup.TryShow();";
            var usage = Card();
            usage.Add(CardHeader("Show the popup"));
            usage.Add(Note("Call this wherever you want to ask for a rating. It shows only until the player rates once, then stays hidden everywhere."));
            usage.Add(CodeLine(snippet));

            var copyHint = new Label();
            copyHint.style.fontSize = 10;
            copyHint.style.color = OkGreen;
            copyHint.style.marginTop = 2;

            var copy = new Button(() =>
            {
                EditorGUIUtility.systemCopyBuffer = snippet;
                copyHint.text = "Copied!";
            }) { text = "Copy code" };
            StyleSecondaryButton(copy);
            copy.style.marginTop = 4;
            usage.Add(copy);
            usage.Add(copyHint);
            _content.Add(usage);

            var test = Card();
            test.Add(CardHeader("Preview popup"));
            test.Add(Note("Asks \"Do you like the game?\". Yes triggers the OS native in-app review sheet; No just thanks & closes (nothing sent)."));

            var testBtn = new Button(() =>
            {
                if (!Application.isPlaying)
                {
                    _reviewHint.text = "Enter Play Mode to preview the popup.";
                    return;
                }
                NexusReviewPopup.ResetRating();
                if (NexusReviewPopup.TryShow())
                    _reviewHint.text = "Spawned the popup in the current scene.";
                else
                    _reviewHint.text = "Popup prefab not found in Resources.";
            })
            { text = "Test rating popup" };
            StylePrimaryButton(testBtn);
            test.Add(testBtn);

            _reviewHint = new Label();
            _reviewHint.style.fontSize = 10;
            _reviewHint.style.color = Muted;
            _reviewHint.style.whiteSpace = WhiteSpace.Normal;
            _reviewHint.style.marginTop = 4;
            test.Add(_reviewHint);
            _content.Add(test);

            _content.Add(Note("Integration code is in Davanci Doctor → Integration guide."));
        }

        void BuildNotificationsStep()
        {
            _content.Add(SectionTitle("Push notifications & retention"));

            if (_config == null)
            {
                _content.Add(Note("No NexusConfig found, finish the Configure step first."));
                return;
            }

            var pkg = Card();
            pkg.Add(CardHeader("Mobile Notifications package"));
            pkg.Add(Note("Install Unity Mobile Notifications once per project. Nexus prompts for permission at startup when enabled below."));
            AddNotificationPackageRow(pkg);
            _content.Add(pkg);

            var perm = Card();
            perm.Add(CardHeader("Permission"));
            perm.Add(ConfigToggle("Request notification permission at startup", _config.requestNotifications,
                v => _config.requestNotifications = v));
            _content.Add(perm);

            var retention = Card();
            retention.Add(CardHeader("D1 / D3 / D7 retention"));
            retention.Add(Note("Local come-back reminders after the player's first session. Scheduled when the app is in the background; cancelled while they are playing. Requires notification permission."));
            retention.Add(ConfigToggle("Enable retention notifications", _config.enableRetentionNotifications,
                v => _config.enableRetentionNotifications = v));

            var hourChoices = new List<string>();
            for (int h = 6; h <= 22; h++)
                hourChoices.Add(h.ToString("00") + ":00");
            int hourIdx = Mathf.Clamp(_config.retentionSendHourLocal - 6, 0, hourChoices.Count - 1);
            var hourDd = new DropdownField("Send at (local time)", hourChoices, hourIdx);
            hourDd.style.fontSize = 11;
            hourDd.RegisterValueChangedCallback(e =>
            {
                if (int.TryParse(e.newValue.Substring(0, 2), out int h))
                {
                    _config.retentionSendHourLocal = h;
                    SaveConfig();
                }
            });
            retention.Add(hourDd);

            AddRetentionDayFields(retention, "Day 1 (next day)", 1,
                () => _config.retentionD1Enabled, v => _config.retentionD1Enabled = v,
                () => _config.retentionD1Title, v => _config.retentionD1Title = v,
                () => _config.retentionD1Body, v => _config.retentionD1Body = v);
            AddRetentionDayFields(retention, "Day 3", 3,
                () => _config.retentionD3Enabled, v => _config.retentionD3Enabled = v,
                () => _config.retentionD3Title, v => _config.retentionD3Title = v,
                () => _config.retentionD3Body, v => _config.retentionD3Body = v);
            AddRetentionDayFields(retention, "Day 7", 7,
                () => _config.retentionD7Enabled, v => _config.retentionD7Enabled = v,
                () => _config.retentionD7Title, v => _config.retentionD7Title = v,
                () => _config.retentionD7Body, v => _config.retentionD7Body = v);

            _content.Add(retention);

            const string scheduleSnippet =
                "// Pick a unique id per notification (e.g. 9201, 9202)\n" +
                "DavanciNexus.ScheduleNotificationIn(9201, \"Reward ready!\", \"Your bonus is waiting.\", 30 * 60);\n\n" +
                "DavanciNexus.ScheduleNotification(9202, \"Daily reset\", \"Claim your login reward.\",\n" +
                "    System.DateTime.Today.AddDays(1).AddHours(9));\n\n" +
                "DavanciNexus.CancelNotification(9201);\n" +
                "DavanciNexus.RefreshRetentionNotifications();   // optional — auto on background";
            var code = Card();
            code.Add(CardHeader("Schedule from your game"));
            code.Add(Note("Local notifications only fire when the app is in the background, nothing is shown while the player is in the game. D1/D3/D7 retention above is automatic; use this for rewards, daily resets, etc."));
            code.Add(SnippetHeader("Schedule & cancel", scheduleSnippet));
            code.Add(CodeLine("DavanciNexus.ScheduleNotificationIn(9201, \"Reward ready!\", \"...\", 30 * 60);"));
            code.Add(CodeLine("DavanciNexus.ScheduleNotification(9202, \"Daily reset\", \"...\", fireTime);"));
            code.Add(CodeLine("DavanciNexus.CancelNotification(9201);"));
            _content.Add(code);
        }

        void BuildPrivacyStep()
        {
            _content.Add(SectionTitle("Privacy & consent (GDPR / US)"));

            if (_config == null)
            {
                _content.Add(Note("No NexusConfig found, finish the Configure step first."));
                return;
            }

            var intro = Card();
            intro.Add(CardHeader("InMobi Choice — certified CMP"));
            intro.Add(Note("Publishing worldwide (EEA + US) requires a certified Consent Management Platform. " +
                "InMobi Choice is free and IAB-certified. On first launch it shows the consent UI and writes the " +
                "IAB TCF / GPP strings; your mediation (LevelPlay / ironSource / AppLovin) reads those automatically " +
                "so ads run in the right personalized/limited state. Nexus runs it before ads init."));
            _content.Add(intro);

            var install = Card();
            install.Add(CardHeader("InMobi Choice CMP SDK"));
            install.Add(Note("Install adds the native iOS xcframework + Android AAR under Assets/Plugins and sets DAVANCI_INMOBI_CMP. Remove deletes them and clears the define."));
            AddInMobiCmpRow(install);
            _content.Add(install);

            var settings = Card();
            settings.Add(CardHeader("Consent settings"));
            settings.Add(ConfigToggle("Show CMP consent at startup", _config.enableCmp,
                v => _config.enableCmp = v));

            bool installed = NexusInMobiCmpInstaller.Installed;
            if (_config.enableCmp && !installed)
                settings.Add(Note("⚠ CMP is enabled but the InMobi SDK isn't installed — install it above or startup will just proceed with limited ads."));
            if (_config.enableCmp && string.IsNullOrEmpty(_config.inmobiCmpPcode))
                settings.Add(Note("⚠ Set your InMobi property code (pcode) below, or the consent UI won't show."));

            settings.Add(ConfigText("Property code (pcode)", _config.inmobiCmpPcode,
                v => _config.inmobiCmpPcode = v));

            var portal = new Button(() => Application.OpenURL("https://choice.inmobi.com/")) { text = "Open InMobi Choice portal" };
            StyleSecondaryButton(portal);
            portal.style.marginTop = 6;
            portal.tooltip = "Find your pcode under your published property in the InMobi Choice dashboard.";
            settings.Add(portal);
            _content.Add(settings);
        }

        void AddInMobiCmpRow(VisualElement parent)
        {
            bool installed = NexusInMobiCmpInstaller.Installed;
            bool defined = NexusInMobiCmpInstaller.DefineSet;
            bool payload = NexusInMobiCmpInstaller.PayloadAvailable;

            var row = Row();
            row.style.alignItems = Align.Center;
            row.style.marginTop = 4;

            var status = new Label(installed
                ? (defined ? "InMobi Choice CMP installed ✓" : "Installed, define not set")
                : (payload ? "InMobi Choice CMP not installed" : "Installer payload missing"));
            status.style.fontSize = 11;
            status.style.flexGrow = 1;
            status.style.color = installed && defined ? OkGreen : Todo;
            row.Add(status);

            if (!installed)
            {
                var btn = new Button(() => { NexusInMobiCmpInstaller.Install(); RenderStep(); }) { text = "Install" };
                StyleSecondaryButton(btn);
                btn.SetEnabled(payload);
                btn.tooltip = "Copies the InMobi Choice native SDK into Assets/Plugins and sets DAVANCI_INMOBI_CMP.";
                row.Add(btn);
            }
            else
            {
                if (!defined)
                {
                    var enable = new Button(() => { NexusInMobiCmpInstaller.SetDefine(true); RenderStep(); }) { text = "Enable" };
                    StyleSecondaryButton(enable);
                    enable.style.marginRight = 6;
                    row.Add(enable);
                }
                var remove = new Button(() =>
                {
                    if (EditorUtility.DisplayDialog("DavanciNexus",
                            "Remove the InMobi Choice CMP SDK and clear DAVANCI_INMOBI_CMP?", "Remove", "Cancel"))
                    {
                        NexusInMobiCmpInstaller.Remove();
                        RenderStep();
                    }
                }) { text = "Remove" };
                StyleSecondaryButton(remove);
                row.Add(remove);
            }
            parent.Add(row);
        }

        void AddNotificationPackageRow(VisualElement parent)
        {
            bool notifInstalled = NexusNotificationsInstaller.Installed;
            bool notifDefined = NexusNotificationsInstaller.DefineSet;
            bool notifInstalling = NexusNotificationsInstaller.InstallInProgress;
            var notifRow = Row();
            notifRow.style.alignItems = Align.Center;
            notifRow.style.marginTop = 4;
            var notifStatus = new Label(notifInstalling
                ? "Installing Mobile Notifications…"
                : notifInstalled
                    ? (notifDefined ? "Mobile Notifications installed ✓" : "Mobile Notifications installed, define not set")
                    : "Mobile Notifications not installed");
            notifStatus.style.fontSize = 11;
            notifStatus.style.flexGrow = 1;
            notifStatus.style.color = notifInstalling ? Muted
                : notifInstalled && notifDefined ? OkGreen : Todo;
            notifRow.Add(notifStatus);

            if (notifInstalling)
            {
                var installing = new Button(() => { }) { text = "Installing…" };
                StyleSecondaryButton(installing);
                installing.SetEnabled(false);
                notifRow.Add(installing);
            }
            else if (!notifInstalled)
            {
                var install = new Button(() =>
                {
                    NexusNotificationsInstaller.Install();
                    RenderStep();
                }) { text = "Install Mobile Notifications" };
                StyleSecondaryButton(install);
                install.tooltip = "Adds com.unity.mobile.notifications via Package Manager, then sets DAVANCI_NOTIFICATIONS.";
                notifRow.Add(install);
            }
            else if (!notifDefined)
            {
                var enable = new Button(() => { NexusNotificationsInstaller.SetDefine(true); RenderStep(); }) { text = "Enable" };
                StyleSecondaryButton(enable);
                notifRow.Add(enable);
            }
            parent.Add(notifRow);
        }

        void AddRetentionDayFields(VisualElement parent, string label, int day,
            Func<bool> getEnabled, Action<bool> setEnabled,
            Func<string> getTitle, Action<string> setTitle,
            Func<string> getBody, Action<string> setBody)
        {
            var fold = new Foldout { text = label, value = day == 1 };
            fold.style.marginTop = 6;
            fold.Add(ConfigToggle("Enabled", getEnabled(), setEnabled));
            fold.Add(ConfigText("Title", getTitle(), setTitle));
            var body = new TextField("Message") { value = getBody(), multiline = true };
            body.style.fontSize = 11;
            body.style.minHeight = 48;
            body.RegisterValueChangedCallback(e => { setBody(e.newValue); SaveConfig(); });
            fold.Add(body);
            parent.Add(fold);
        }

        void BuildFirebaseStep()
        {
            NexusGitignore.EnsureFirebaseDesktopIgnored();

            _content.Add(SectionTitle("Firebase configuration files"));
            _content.Add(Note("Firebase Analytics works on the FREE Spark plan, no billing/Blaze required. " +
                              "Unlike GameAnalytics, Firebase reads a per-platform config file you download from the Firebase console."));

            var ios = Card();
            ios.Add(CardHeader("iOS: GoogleService-Info.plist"));
            ios.Add(Note("Firebase console → Project settings → your iOS app → download GoogleService-Info.plist, then drop it into the Assets/ root."));
            ios.Add(FileStatusRow(NexusInstaller.FirebaseIosFile, NexusInstaller.FirebaseIosConfigPresent));
            _content.Add(ios);

            var android = Card();
            android.Add(CardHeader("Android: google-services.json"));
            android.Add(Note("Firebase console → Project settings → your Android app → download google-services.json, then drop it into the Assets/ root."));
            android.Add(FileStatusRow(NexusInstaller.FirebaseAndroidFile, NexusInstaller.FirebaseAndroidConfigPresent));
            _content.Add(android);

#if UNITY_EDITOR_OSX
            var mac = Card();
            mac.Add(CardHeader("macOS: plugin quarantine"));
            mac.Add(Note("macOS blocks Firebase's native .bundle on Play (\"Apple could not verify…\"). Nexus strips the quarantine flag automatically on import, but you can re-run it here if a plugin still fails to load."));
            var fixQuarantine = new Button(() => NexusMacQuarantine.CleanMenu()) { text = "Fix macOS plugin quarantine" };
            StyleSecondaryButton(fixQuarantine);
            mac.Add(fixQuarantine);
            _content.Add(mac);
#endif

            var actions = Row();
            var reveal = new Button(() =>
            {
                EditorUtility.RevealInFinder(Application.dataPath);
            }) { text = "Open Assets folder" };
            StyleSecondaryButton(reveal); reveal.style.flexGrow = 1;
            var refresh = new Button(RenderStep) { text = "Refresh" };
            StyleSecondaryButton(refresh); refresh.style.flexGrow = 1; refresh.style.marginLeft = 6;
            actions.Add(reveal); actions.Add(refresh);
            _content.Add(actions);

            if (NexusGitignore.IsFirebaseDesktopIgnored())
                _content.Add(Note("Git: Assets/Firebase/Plugins/x86_64/ is in .gitignore. Editor-only binaries stay local and won't hit GitHub's 100 MB file limit."));

            _content.Add(Note("Firebase init is automatic at runtime: FirebaseApp.CheckAndFixDependenciesAsync() runs, then analytics collection is enabled. You don't call GameAnalytics.Initialize() for it."));
        }

        void BuildRemoteConfigStep()
        {
            _content.Add(SectionTitle("Remote Config & A/B Testing"));
            _content.Add(Note("Change values in live games without shipping a build. Values are published from the DavanciNexus dashboard and read over Realtime Database REST — no Firebase SDK needed, so this works even if you skipped the Firebase step."));

            var main = Card();
            main.Add(CardHeader("Connection"));
            main.Add(ConfigToggle("Enable Remote Config", _config.enableRemoteConfig,
                v => _config.enableRemoteConfig = v));
            main.Add(ConfigText("Game ID", _config.gameId, v => _config.gameId = v));
            main.Add(Note("Copy the Game ID from your game's page on the DavanciNexus dashboard. It selects which config this build receives."));

            var timeout = new IntegerField("Timeout (s)") { value = _config.remoteConfigTimeoutSeconds };
            timeout.style.fontSize = 11;
            timeout.RegisterValueChangedCallback(e =>
            {
                _config.remoteConfigTimeoutSeconds = Mathf.Clamp(e.newValue, 1, 60);
                SaveConfig();
            });
            main.Add(timeout);
            main.Add(Note("The fetch never blocks the game. On timeout the last cached values are used, and if there are none, the defaults you pass to Get* apply."));
            _content.Add(main);

            if (!_config.enableRemoteConfig)
            {
                _content.Add(Note("Remote Config is off — Get* calls return your defaults and no network request is made."));
                return;
            }

            var test = Card();
            test.Add(CardHeader("Test the connection"));

            string url = NexusRemoteConfig.BuildUrl(_config);
            if (url == null)
            {
                var missing = Note("Set a Game ID above to enable fetching.");
                missing.style.color = Todo;
                test.Add(missing);
            }
            else
            {
                var urlLabel = Note(url);
                urlLabel.style.marginBottom = 6;
                test.Add(urlLabel);

                var result = Note(_rcTestResult ?? "Not fetched yet.");
                if (_rcTestOk.HasValue)
                    result.style.color = _rcTestOk.Value ? OkGreen : Accent;

                var actions = Row();
                var fetch = new Button(() => FetchRemoteConfigPreview(url)) { text = "Fetch now" };
                StylePrimaryButton(fetch); fetch.style.flexGrow = 1;
                var clear = new Button(() =>
                {
                    NexusRemoteConfig.ClearCache();
                    _rcTestResult = "Cache cleared.";
                    _rcTestOk = null;
                    RenderStep();
                }) { text = "Clear cache" };
                StyleSecondaryButton(clear); clear.style.flexGrow = 1; clear.style.marginLeft = 6;
                actions.Add(fetch); actions.Add(clear);
                test.Add(actions);
                test.Add(result);
            }
            _content.Add(test);

            if (!string.IsNullOrEmpty(_rcTestPayload))
                BuildPayloadCard(_rcTestPayload);

            var usage = Card();
            usage.Add(CardHeader("Reading values in your game"));
            usage.Add(SnippetHeader("Always pass the value you would ship offline.", RemoteConfigSnippet));
            foreach (var line in RemoteConfigSnippet.Split('\n'))
                usage.Add(CodeLine(line));
            _content.Add(usage);

            BuildRemoteKeysCard();

            var ab = Card();
            ab.Add(CardHeader("A/B testing"));
            ab.Add(Note("Variants are assigned per install from a stable hash, so a player never flips between launches and the dashboard stores no assignments. Every assignment is logged as a remote_config_variant event, so you can slice results in GameAnalytics or Firebase."));
            ab.Add(CodeLine("if (DavanciNexus.GetVariant(\"paywall_price\") == \"cheap\") ShowDiscount();"));
            var reroll = new Button(() =>
            {
                NexusRemoteConfig.ResetInstallId();
                NexusRemoteConfig.ClearCache();
                _rcTestResult = "Install ID reset — this device will re-bucket on the next fetch.";
                _rcTestOk = null;
                RenderStep();
            }) { text = "Re-roll this device's variant (QA)" };
            StyleSecondaryButton(reroll);
            ab.Add(reroll);
            _content.Add(ab);
        }

        // What the fetched payload actually resolves to on this device: the merged values,
        // which variant this install landed in, and which keys that variant overrode. The
        // raw JSON stays available but collapsed — it is rarely the thing you want to read.
        void BuildPayloadCard(string json)
        {
            var payload = JsonUtility.FromJson<NexusRemoteConfig.Payload>(json);
            if (payload == null) return;

            var card = Card();
            card.Add(CardHeader($"Live config — v{payload.version}"));

            var assignments = NexusRemoteConfig.Assignments;
            var overridden = new HashSet<string>();
            foreach (var exp in payload.experiments ?? new NexusRemoteConfig.Experiment[0])
            {
                if (exp?.variants == null || !assignments.TryGetValue(exp.key, out var chosen)) continue;
                foreach (var v in exp.variants)
                    if (v != null && v.name == chosen && v.values != null)
                        foreach (var e in v.values)
                            if (e != null && !string.IsNullOrEmpty(e.k)) overridden.Add(e.k);
            }

            foreach (var kv in NexusRemoteConfig.All)
                card.Add(PayloadValueRow(kv.Key, kv.Value, overridden.Contains(kv.Key)));

            if (payload.experiments != null && payload.experiments.Length > 0)
            {
                var head = Note("Experiments — this device only. Every install rolls its own bucket.");
                head.style.marginTop = 8;
                card.Add(head);

                foreach (var exp in payload.experiments)
                {
                    if (exp == null || string.IsNullOrEmpty(exp.key)) continue;
                    assignments.TryGetValue(exp.key, out var chosen);
                    card.Add(ExperimentRow(exp, chosen));
                }
            }

            var raw = new Foldout { text = "Raw JSON", value = false };
            raw.style.fontSize = 11;
            raw.style.marginTop = 8;
            var box = new TextField { value = json, multiline = true, isReadOnly = true };
            box.style.fontSize = 10;
            box.style.maxHeight = 220;
            raw.Add(box);
            card.Add(raw);

            _content.Add(card);
        }

        VisualElement PayloadValueRow(string key, string value, bool fromVariant)
        {
            var row = Row();
            row.style.justifyContent = Justify.SpaceBetween;
            row.style.alignItems = Align.Center;
            row.style.marginBottom = 2;

            var name = new Label(fromVariant ? $"{key}  ●" : key);
            name.style.fontSize = 11;
            name.style.color = fromVariant ? Accent2 : Color.white;
            if (fromVariant) name.tooltip = "Overridden by this device's experiment variant.";
            row.Add(name);

            var val = new Label(string.IsNullOrEmpty(value) ? "(empty)" : value);
            val.style.fontSize = 11;
            val.style.color = string.IsNullOrEmpty(value) ? Muted : OkGreen;
            val.style.unityFontStyleAndWeight = FontStyle.Bold;
            row.Add(val);

            return row;
        }

        VisualElement ExperimentRow(NexusRemoteConfig.Experiment exp, string chosen)
        {
            var box = new VisualElement();
            box.style.marginBottom = 4;

            var title = new Label(string.IsNullOrEmpty(exp.property)
                ? exp.key
                : $"{exp.key}   →   {exp.property}");
            title.style.fontSize = 11;
            title.style.unityFontStyleAndWeight = FontStyle.Bold;
            title.style.color = Color.white;
            title.tooltip = string.IsNullOrEmpty(exp.property)
                ? "No GA4 dimension allocated — exposures are counted, but revenue and retention can't be split by arm."
                : "GA4 user-scoped custom dimension this experiment reports into.";
            box.Add(title);

            int total = 0;
            foreach (var v in exp.variants)
                if (v != null && v.weight > 0) total += v.weight;

            foreach (var v in exp.variants)
            {
                if (v == null || string.IsNullOrEmpty(v.name)) continue;

                bool active = v.name == chosen;
                int pct = total > 0 && v.weight > 0 ? Mathf.RoundToInt(v.weight * 100f / total) : 0;

                var row = Row();
                row.style.justifyContent = Justify.SpaceBetween;
                row.style.marginLeft = 10;

                var left = new Label(active ? $"▸ {v.name}" : $"   {v.name}");
                left.style.fontSize = 11;
                left.style.color = active ? Accent2 : Muted;
                row.Add(left);

                var right = new Label(active ? $"{pct}%  ← this device" : $"{pct}%");
                right.style.fontSize = 10;
                right.style.color = active ? Accent2 : Muted;
                row.Add(right);

                box.Add(row);
            }

            return box;
        }

        // Static scan of the project's scripts: which keys this build actually reads, with the
        // default it ships. Copied out as JSON, this is what the dashboard offers for the game.
        void BuildRemoteKeysCard()
        {
            var card = Card();
            card.Add(CardHeader("Keys your game reads"));
            card.Add(Note("Scanned from your scripts. These are the keys — and the defaults you ship — that the dashboard should offer for this game."));

            var usages = NexusRemoteConfigUsageScanner.Scan();
            if (usages.Count == 0)
            {
                var none = Note("No DavanciNexus.Get* calls found yet. Read a key in your game code, then re-scan.");
                none.style.color = Todo;
                card.Add(none);
            }
            else
            {
                foreach (var u in usages)
                    card.Add(RemoteKeyRow(u));

                var clashes = usages.Where(u => u.Conflicted).Select(u => u.Key).ToList();
                if (clashes.Count > 0)
                {
                    var warn = Note($"Read as more than one type: {string.Join(", ", clashes)}. A key carries one string on the wire, so one of those call sites will always fall back — pick a single type per key.");
                    warn.style.color = Todo;
                    card.Add(warn);
                }

                var copy = new Button(() =>
                {
                    EditorGUIUtility.systemCopyBuffer = NexusRemoteConfigUsageScanner.ToDashboardJson(usages);
                    _rcKeysNote = $"Copied {usages.Count} key(s) to the clipboard — paste into the game's page on the dashboard.";
                    RenderStep();
                }) { text = "Copy keys as JSON for the dashboard" };
                StyleSecondaryButton(copy); copy.style.marginTop = 6;
                card.Add(copy);
            }

            var rescan = new Button(() => { _rcKeysNote = null; RenderStep(); }) { text = "Re-scan code" };
            StyleSecondaryButton(rescan); rescan.style.marginTop = 6;
            card.Add(rescan);

            if (!string.IsNullOrEmpty(_rcKeysNote))
            {
                var done = Note(_rcKeysNote);
                done.style.color = OkGreen;
                card.Add(done);
            }

            _content.Add(card);
        }

        VisualElement RemoteKeyRow(RemoteKeyUsage usage)
        {
            var row = Row();
            row.style.justifyContent = Justify.SpaceBetween;
            row.style.alignItems = Align.Center;
            row.style.marginBottom = 3;

            string type = NexusRemoteConfigUsageScanner.TypeName(usage.Kind);
            string label = usage.IsExperiment || string.IsNullOrEmpty(usage.Default)
                ? $"{usage.Key}  ({type})"
                : $"{usage.Key}  ({type} = {usage.Default})";

            var name = new Label(label);
            name.style.fontSize = 11;
            name.style.color = usage.Conflicted ? Todo : Color.white;
            row.Add(name);

            var first = usage.Sites[0];
            var link = new Button(() =>
            {
                var obj = AssetDatabase.LoadMainAssetAtPath(first.AssetPath);
                EditorGUIUtility.PingObject(obj);
                Selection.activeObject = obj;
            })
            { text = $"{System.IO.Path.GetFileName(first.AssetPath)}:{first.Line}" +
                     (usage.Sites.Count > 1 ? $" (+{usage.Sites.Count - 1})" : "") };
            link.style.height = 20;
            link.style.fontSize = 10;
            link.style.color = Muted;
            link.style.backgroundColor = new Color(0, 0, 0, 0);
            NoBorder(link);
            row.Add(link);

            return row;
        }

        const string RemoteConfigSnippet =
            "if (!DavanciNexus.GetBool(\"ads_enabled\", true)) return;\n" +
            "int cooldown = DavanciNexus.GetInt(\"interstitial_cooldown\", 45);\n" +
            "float mult = DavanciNexus.GetFloat(\"rewarded_multiplier\", 1.5f);";

        string _rcTestResult;
        string _rcTestPayload;
        bool? _rcTestOk;
        string _rcKeysNote;

        void FetchRemoteConfigPreview(string url)
        {
            var req = UnityEngine.Networking.UnityWebRequest.Get(url);
            req.timeout = Mathf.Max(1, _config.remoteConfigTimeoutSeconds);
            req.SetRequestHeader("Cache-Control", "no-cache");
            var op = req.SendWebRequest();

            void Poll()
            {
                if (!op.isDone) return;
                EditorApplication.update -= Poll;

                bool ok = req.result == UnityEngine.Networking.UnityWebRequest.Result.Success;
                string text = ok ? req.downloadHandler.text : null;
                string error = req.error;
                req.Dispose();

                if (!ok)
                {
                    _rcTestOk = false;
                    _rcTestResult = $"Fetch failed: {error}";
                    _rcTestPayload = null;
                }
                else if (string.IsNullOrEmpty(text) || text.Trim() == "null")
                {
                    _rcTestOk = false;
                    _rcTestResult = "Reached the database, but there is no config for this Game ID yet.";
                    _rcTestPayload = null;
                }
                else if (!NexusRemoteConfig.Validate(text, out string schemaError, out string summary))
                {
                    _rcTestOk = false;
                    _rcTestPayload = text;
                    _rcTestResult = $"Reached the database, but the payload doesn't match the schema: {schemaError}. A build would ignore it and keep its previous values.";
                }
                else
                {
                    NexusRemoteConfig.Adopt(text);
                    _rcTestPayload = text;
                    _rcTestOk = true;
                    _rcTestResult = $"OK — {summary}.";
                }

                RenderStep();
            }

            EditorApplication.update += Poll;
        }

        void BuildCloudSaveStep()
        {
            NexusGpgsSetup.SyncFromGpgsSettings(_config);

            _content.Add(SectionTitle("Cloud Save"));
            _content.Add(Note("Syncs each player's save to their own cloud, iCloud on iOS, Google Play Games on Android. When off, saves stay on-device only and no iCloud entitlement is added."));

            var enable = Card();
            enable.Add(CardHeader("Enable"));
            enable.Add(ConfigToggle("Enable Cloud Save", _config.enableCloudSave, v => _config.enableCloudSave = v));
            enable.Add(ConfigToggle("Auto-sync (load on launch, save on pause/quit)", _config.cloudSaveAutoSync, v => _config.cloudSaveAutoSync = v));
            _content.Add(enable);

            // iOS — iCloud
            var ios = Card();
            ios.Add(CardHeader("iOS : iCloud (Key-Value Store)"));
            ios.Add(Note("No console setup and no sign-in prompt. Enabling Cloud Save adds the iCloud Key-Value Store entitlement to the iOS build automatically. The save is keyed by your bundle ID."));
            var iosKey = new TextField("iCloud key (iOS bundle ID)") { value = IosBundleId() };
            iosKey.SetEnabled(false);
            iosKey.style.fontSize = 11;
            iosKey.tooltip = "Fixed to the iOS bundle identifier, stable and unique.";
            ios.Add(iosKey);
            _content.Add(ios);

            // Android — Google Play Games
            var android = Card();
            android.Add(CardHeader("Android : Google Play Games (Saved Games)"));

            var gpgsSdk = NexusInstaller.Sdks.FirstOrDefault(s => s.Id == "gpgs");
            if (gpgsSdk == null || !gpgsSdk.IsInstalled)
            {
                android.Add(Note("Install the Google Play Games plugin in step 2 first, then return here."));
                if (gpgsSdk != null)
                {
                    var btn = new Button(() => NexusInstaller.Import(gpgsSdk)) { text = "Install Play Games plugin" };
                    StylePrimaryButton(btn);
                    android.Add(btn);
                }
            }
            else
            {
                bool configured = NexusGpgsSetup.IsConfigured;
                var statusLine = new Label(configured
                    ? "Configured. Play Games Android setup has run with your Play Console resources."
                    : "Not configured yet. Paste your resources below and click Apply setup.");
                statusLine.style.fontSize = 11;
                statusLine.style.color = configured ? OkGreen : Todo;
                statusLine.style.whiteSpace = WhiteSpace.Normal;
                statusLine.style.marginBottom = 6;
                android.Add(statusLine);

                android.Add(Note("Play Console → Grow → Play Games Services → Setup and management → Configuration → Get resources (Android). Copy the full XML block."));
                var openConsole = new Button(() => Application.OpenURL("https://play.google.com/console/developers")) { text = "Open Play Console" };
                StyleSecondaryButton(openConsole);
                android.Add(openConsole);

                var webClient = ConfigText("Web Client ID (optional)", _config.gpgsWebClientId, v => _config.gpgsWebClientId = v);
                webClient.tooltip = "OAuth 2.0 Web Client ID from Play Console / Google Cloud. Required for server-side auth; leave blank if you only use Saved Games locally.";
                android.Add(webClient);

                var snapshot = ConfigText("Saved Games snapshot name", _config.gpgsSnapshotName, v => _config.gpgsSnapshotName = v);
                snapshot.tooltip = "Stable slot name for Android cloud saves. One per game. Ignored on iOS.";
                android.Add(snapshot);

                var xmlLabel = new Label("Android Resources XML");
                xmlLabel.style.fontSize = 11;
                xmlLabel.style.marginTop = 6;
                android.Add(xmlLabel);

                var xml = new TextField { value = _config.gpgsResourcesXml ?? "" };
                xml.multiline = true;
                xml.style.fontSize = 11;
                xml.style.minHeight = 120;
                xml.style.whiteSpace = WhiteSpace.Normal;
                xml.RegisterValueChangedCallback(e =>
                {
                    _config.gpgsResourcesXml = e.newValue;
                    SaveConfig();
                });
                android.Add(xml);

                var result = new Label("");
                result.style.fontSize = 10;
                result.style.whiteSpace = WhiteSpace.Normal;
                result.style.marginTop = 6;
                result.style.display = DisplayStyle.None;

                var apply = new Button(() =>
                {
                    string msg = NexusGpgsSetup.Apply(_config);
                    bool ok = msg.StartsWith("Play Games Android setup complete", StringComparison.Ordinal);
                    result.text = msg;
                    result.style.color = ok ? OkGreen : Todo;
                    result.style.display = DisplayStyle.Flex;
                    RenderStep();
                }) { text = "Apply Play Games setup" };
                StylePrimaryButton(apply);
                apply.style.marginTop = 8;
                android.Add(apply);
                android.Add(result);
            }
            _content.Add(android);

            // Code snippet
            var code = Card();
            code.Add(CardHeader("Game hookup"));
            const string bindSnippet =
                "DavanciNexus.BindCloudSave(\n" +
                "    save: () => MySave.ToJson(),\n" +
                "    load: json => { if (json != null) MySave.FromJson(json); });";
            code.Add(SnippetHeader("With Auto-sync on", bindSnippet));
            code.Add(CodeLine("DavanciNexus.BindCloudSave("));
            code.Add(CodeLine("    save: () => MySave.ToJson(),"));
            code.Add(CodeLine("    load: json => { if (json != null) MySave.FromJson(json); });"));

            const string manualSnippet =
                "DavanciNexus.Save(MySave.ToJson());\n" +
                "DavanciNexus.Load(json => { ... });";
            code.Add(SnippetHeader("Or manually, anywhere", manualSnippet));
            code.Add(CodeLine("DavanciNexus.Save(MySave.ToJson());"));
            code.Add(CodeLine("DavanciNexus.Load(json => { ... });"));
            code.Add(Note("Last-write-wins resolves cloud vs. local. Runtime activates Play Games on Android automatically."));
            _content.Add(code);
        }

        VisualElement FileStatusRow(string path, bool present)
        {
            var row = Row();
            row.style.marginTop = 4;
            var label = new Label(present ? $"Found: {path}" : $"Missing: {path}");
            label.style.fontSize = 10;
            label.style.color = present ? OkGreen : Todo;
            row.Add(label);
            return row;
        }

        void BuildVerifyStep()
        {
            _content.Add(SectionTitle("Verify analytics events"));
            _content.Add(Note("Call DavanciNexus.* from your gameplay code (e.g. DavanciNexus.TutorialComplete(), DavanciNexus.Progress.Start(1)) to log events. Every call is forwarded to all installed providers and captured in an in-memory buffer you can watch live."));

            var funnel = Card();
            funnel.Add(CardHeader("Tutorial and progress (every game)"));
            funnel.Add(Note("The standard funnel: same names and parameters in every game. Tutorial steps are sent once per install; a completed level is sent once, and its replays are skipped."));
            funnel.Add(CodeLine("DavanciNexus.TutorialBegin();  DavanciNexus.TutorialComplete();"));
            funnel.Add(CodeLine("DavanciNexus.TutorialStep(0, \"tap_to_start\");  DavanciNexus.TutorialStepDone(0);"));
            funnel.Add(CodeLine("DavanciNexus.FtueStall(2, \"buy_ride\", idleSeconds);"));
            funnel.Add(CodeLine("DavanciNexus.Progress.Start(5);  DavanciNexus.Progress.Complete(5);"));
            funnel.Add(CodeLine("DavanciNexus.Progress.Fail(5, \"out_of_moves\");  DavanciNexus.Progress.Start(2, \"world\", \"jungle\");"));
            _content.Add(funnel);

            var examples = Card();
            examples.Add(CardHeader("Generic tycoon / idle calls"));
            examples.Add(CodeLine("DavanciNexus.CurrencyEarned(\"coins\", 1500, \"gameplay\");"));
            examples.Add(CodeLine("DavanciNexus.CurrencySpent(\"coins\", 800, \"upgrade\", \"farm\");"));
            examples.Add(CodeLine("DavanciNexus.Acquired(\"business\", \"farm\");  DavanciNexus.Upgraded(\"business\", \"farm\", 5);"));
            examples.Add(CodeLine("DavanciNexus.Unlocked(\"area\", \"city\");  DavanciNexus.Reset(\"prestige\", 2, 50);"));
            examples.Add(CodeLine("DavanciNexus.OfflineEarningsCollected(12000, 3600);"));
            examples.Add(CodeLine("DavanciNexus.Checkpoint(\"world2_after_first_prestige\");"));
            _content.Add(examples);

            var monitor = Card();
            monitor.Add(CardHeader("Live event monitor"));
            monitor.Add(Note("Opens a dockable window that lists DavanciNexus.* events as they fire in Play Mode, with the providers each event reached. It also has test-fire buttons so you can verify wiring without touching your game."));
            var open = new Button(NexusEventMonitor.Open) { text = "Open Nexus Event Monitor" };
            StylePrimaryButton(open);
            monitor.Add(open);
            _content.Add(monitor);

            BuildEventUsageCard();

            var snap = Card();
            snap.Add(CardHeader($"Captured so far · {DavanciNexus.Recent.Count} event(s)"));
            if (DavanciNexus.Recent.Count == 0)
            {
                snap.Add(Note("Nothing captured yet. Enter Play Mode and fire an event (or use the monitor's test buttons)."));
            }
            else
            {
                int shown = 0;
                for (int i = DavanciNexus.Recent.Count - 1; i >= 0 && shown < 6; i--, shown++)
                {
                    var ev = DavanciNexus.Recent[i];
                    var line = new Label($"{ev.Stamp:HH:mm:ss}  {ev.Name} {ev.Parameters} → {ev.Providers}");
                    line.style.fontSize = 10;
                    line.style.color = Muted;
                    line.style.whiteSpace = WhiteSpace.Normal;
                    snap.Add(line);
                }
            }
            var refresh = new Button(RenderStep) { text = "Refresh" };
            StyleSecondaryButton(refresh); refresh.style.marginTop = 6;
            snap.Add(refresh);
            _content.Add(snap);
        }

        Label CodeLine(string text)
        {
            var l = new Label(text);
            l.style.fontSize = 11;
            l.style.color = OkGreen;
            l.style.whiteSpace = WhiteSpace.Normal;
            l.style.marginBottom = 2;
            return l;
        }

        // Static scan of the project's scripts: which DN events are called (and where)
        // vs. never referenced, catches events you forgot to wire up.
        void BuildEventUsageCard()
        {
            var card = Card();
            card.Add(CardHeader("Wired in your code"));
            card.Add(Note("Scans your scripts for DavanciNexus.* calls so you can confirm each event is actually hooked up, not just whatever fired at runtime."));

            var usages = NexusEventUsageScanner.Scan();
            int wired = usages.Count(u => u.IsCalled);

            var summary = new Label($"{wired} of {usages.Count} tracked event(s) called in your code");
            summary.style.fontSize = 11;
            summary.style.unityFontStyleAndWeight = FontStyle.Bold;
            summary.style.color = wired > 0 ? OkGreen : Todo;
            summary.style.marginBottom = 6;
            card.Add(summary);

            foreach (var usage in usages)
                card.Add(EventUsageRow(usage));

            foreach (var warning in NexusEventUsageScanner.ScanWarnings())
                card.Add(EventWarningRow(warning));

            var rescan = new Button(RenderStep) { text = "Re-scan code" };
            StyleSecondaryButton(rescan); rescan.style.marginTop = 6;
            card.Add(rescan);

            _content.Add(card);
        }

        VisualElement EventWarningRow(EventWarning warning)
        {
            var link = new Button(() =>
            {
                var obj = AssetDatabase.LoadMainAssetAtPath(warning.AssetPath);
                EditorGUIUtility.PingObject(obj);
                Selection.activeObject = obj;
            })
            { text = $"⚠ {System.IO.Path.GetFileName(warning.AssetPath)}:{warning.Line}  {warning.Message}" };
            link.style.fontSize = 10;
            link.style.color = Todo;
            link.style.whiteSpace = WhiteSpace.Normal;
            link.style.unityTextAlign = TextAnchor.MiddleLeft;
            link.style.backgroundColor = new Color(0, 0, 0, 0);
            link.style.marginTop = 2;
            NoBorder(link);
            return link;
        }

        VisualElement EventUsageRow(EventUsage usage)
        {
            var row = Row();
            row.style.justifyContent = Justify.SpaceBetween;
            row.style.alignItems = Align.Center;
            row.style.marginBottom = 3;

            var name = new Label($"DavanciNexus.{usage.Method}");
            name.style.fontSize = 11;
            name.style.color = usage.IsCalled ? Color.white : Muted;
            row.Add(name);

            if (usage.IsCalled)
            {
                var first = usage.Sites[0];
                var link = new Button(() =>
                {
                    var obj = AssetDatabase.LoadMainAssetAtPath(first.AssetPath);
                    EditorGUIUtility.PingObject(obj);
                    Selection.activeObject = obj;
                })
                { text = $"✓ {System.IO.Path.GetFileName(first.AssetPath)}:{first.Line}" +
                         (usage.Sites.Count > 1 ? $" (+{usage.Sites.Count - 1})" : "") };
                link.style.height = 20;
                link.style.fontSize = 10;
                link.style.color = OkGreen;
                link.style.backgroundColor = new Color(0, 0, 0, 0);
                NoBorder(link);
                row.Add(link);
            }
            else
            {
                var miss = new Label("not called");
                miss.style.fontSize = 10;
                miss.style.color = Todo;
                row.Add(miss);
            }
            return row;
        }

        void BuildSplashStep()
        {
            _content.Add(SectionTitle("Splash screen"));
            _content.Add(Note(
                "The Davanci Nexus splash is required in every game. Nexus keeps NexusSplash at build index 0 " +
                "(in front of your scenes); whatever was first before becomes index 1 and the splash loads it. " +
                "A build whose first scene is something else fails."));

            var card = Card();

            var status = new Label();
            status.style.fontSize = 11;
            status.style.color = Muted;
            status.style.whiteSpace = WhiteSpace.Normal;
            if (!NexusSplashSceneInstaller.SplashSceneExists)
                status.text = "✗ Scene missing — re-import DavanciNexus package.";
            else
            {
                NexusSplashSceneInstaller.EnsureFirstInBuild();
                status.text = "✓ " + NexusSplashSceneInstaller.SplashScenePath + " is build index 0.";
            }
            card.Add(status);

            _content.Add(card);

            // Unity's own "Made with Unity" splash plays before any scene, so it runs in front of
            // the Nexus splash. Bound straight to Player Settings so it always shows the real value.
            var unityCard = Card();
            unityCard.Add(CardHeader("Unity splash screen"));
            unityCard.Add(Note("Turn off to open straight on the Nexus splash (Player Settings → Splash Image → Show Splash Screen)."));

            var removeUnitySplash = new Toggle("Remove Unity splash screen") { value = !PlayerSettings.SplashScreen.show };
            removeUnitySplash.style.fontSize = 11;
            removeUnitySplash.RegisterValueChangedCallback(e =>
            {
                PlayerSettings.SplashScreen.show = !e.newValue;
                AssetDatabase.SaveAssets();
                RenderStep();
            });
            unityCard.Add(removeUnitySplash);

#if !UNITY_6000_0_OR_NEWER
            // Before Unity 6, Personal licenses must show the Unity splash; the setting is ignored.
            if (!Application.HasProLicense())
                unityCard.Add(Note("Unity Personal before Unity 6 always shows the Unity splash; this has no effect."));
#endif

            _content.Add(unityCard);
        }

        void BuildFinishStep()
        {
            _content.Add(SectionTitle("You're set"));
            _content.Add(Note("Nothing else to wire up — AuthorizationManager spawns on app start and the Nexus splash opens every launch."));

            var card = Card();
            var doctorBtn = new Button(NexusDoctorWindow.Open) { text = "Open Davanci Doctor" };
            StyleSecondaryButton(doctorBtn); doctorBtn.style.marginBottom = 6;
            card.Add(doctorBtn);

            var pingCfg = new Button(() =>
            {
                EditorGUIUtility.PingObject(_config);
                Selection.activeObject = _config;
            }) { text = "Select NexusConfig asset" };
            StyleSecondaryButton(pingCfg); pingCfg.style.marginBottom = 6;
            card.Add(pingCfg);
            _content.Add(card);

            _content.Add(Note("Checklist: SDKs installed → keys filled → Firebase config files in Assets/ → build."));
        }

        VisualElement Card()
        {
            var c = new VisualElement();
            c.style.backgroundColor = Panel;
            c.style.paddingLeft = 12; c.style.paddingRight = 12;
            c.style.paddingTop = 10; c.style.paddingBottom = 10;
            c.style.marginBottom = 8;
            RoundCorners(c, 8);
            return c;
        }

        VisualElement Row()
        {
            var r = new VisualElement();
            r.style.flexDirection = FlexDirection.Row;
            return r;
        }

        Label SectionTitle(string text)
        {
            var l = new Label(text);
            l.style.fontSize = 14;
            l.style.unityFontStyleAndWeight = FontStyle.Bold;
            l.style.marginTop = 6;
            l.style.marginBottom = 8;
            return l;
        }

        Label CardHeader(string text)
        {
            var l = new Label(text);
            l.style.unityFontStyleAndWeight = FontStyle.Bold;
            l.style.fontSize = 12;
            l.style.marginBottom = 6;
            l.style.color = Accent2;
            return l;
        }

        Label MiniLabel(string text)
        {
            var l = new Label(text);
            l.style.fontSize = 11;
            l.style.unityFontStyleAndWeight = FontStyle.Bold;
            l.style.color = Muted;
            l.style.marginTop = 6;
            l.style.marginBottom = 2;
            return l;
        }

        VisualElement SnippetHeader(string text, string snippet)
        {
            var row = Row();
            row.style.alignItems = Align.Center;
            row.style.justifyContent = Justify.SpaceBetween;
            var label = MiniLabel(text);
            label.style.flexGrow = 1;
            row.Add(label);
            var copy = new Button(() =>
            {
                EditorGUIUtility.systemCopyBuffer = snippet;
                ShowNotification(new GUIContent("Copied"));
            }) { text = "Copy" };
            StyleSecondaryButton(copy);
            copy.style.fontSize = 10;
            copy.style.paddingLeft = 8;
            copy.style.paddingRight = 8;
            copy.tooltip = "Copy this snippet to the clipboard.";
            row.Add(copy);
            return row;
        }

        Label Note(string text)
        {
            var l = new Label(text);
            l.style.fontSize = 10;
            l.style.color = Muted;
            l.style.whiteSpace = WhiteSpace.Normal;
            l.style.marginBottom = 8;
            return l;
        }

        Toggle ConfigToggle(string label, bool value, Action<bool> setter)
        {
            var t = new Toggle(label) { value = value };
            t.style.fontSize = 11;
            t.RegisterValueChangedCallback(e => { setter(e.newValue); SaveConfig(); });
            return t;
        }

        VisualElement DbUrlField()
        {
            var wrap = new VisualElement();
            bool isCustom = !string.IsNullOrEmpty(_config.realtimeDbUrl) && _config.realtimeDbUrl != DefaultDbUrl;

            var custom = new TextField("Custom URL") { value = _config.realtimeDbUrl };
            custom.style.fontSize = 11;
            custom.style.display = isCustom ? DisplayStyle.Flex : DisplayStyle.None;
            custom.RegisterValueChangedCallback(e => { _config.realtimeDbUrl = e.newValue; SaveConfig(); });

            var choices = new List<string> { "Davanci (default)", "Custom" };
            var dd = new DropdownField("Feedback DB", choices, isCustom ? 1 : 0);
            dd.style.fontSize = 11;
            dd.RegisterValueChangedCallback(e =>
            {
                if (e.newValue == "Custom")
                {
                    custom.style.display = DisplayStyle.Flex;
                    _config.realtimeDbUrl = custom.value;
                }
                else
                {
                    custom.style.display = DisplayStyle.None;
                    _config.realtimeDbUrl = DefaultDbUrl;
                    custom.SetValueWithoutNotify(DefaultDbUrl);
                }
                SaveConfig();
            });

            wrap.Add(dd);
            wrap.Add(custom);
            return wrap;
        }

        TextField ConfigText(string label, string value, Action<string> setter)
        {
            var f = new TextField(label) { value = value };
            f.style.fontSize = 11;
            f.RegisterValueChangedCallback(e => { setter(e.newValue); SaveConfig(); });
            return f;
        }

        static void RoundCorners(VisualElement e, int r)
        {
            e.style.borderTopLeftRadius = r; e.style.borderTopRightRadius = r;
            e.style.borderBottomLeftRadius = r; e.style.borderBottomRightRadius = r;
        }

        static void NoBorder(VisualElement e)
        {
            e.style.borderTopWidth = 0; e.style.borderBottomWidth = 0;
            e.style.borderLeftWidth = 0; e.style.borderRightWidth = 0;
        }

        void StylePrimaryButton(Button b)
        {
            b.style.height = 30; b.style.color = Color.white;
            b.style.unityFontStyleAndWeight = FontStyle.Bold;
            RoundCorners(b, 6); NoBorder(b);
            AddButtonFeedback(b, Accent);
        }

        void StyleSecondaryButton(Button b)
        {
            b.style.height = 28; b.style.color = Color.white;
            RoundCorners(b, 6); NoBorder(b);
            AddButtonFeedback(b, PanelHi);
        }

        void StyleDangerButton(Button b)
        {
            b.style.height = 28; b.style.color = Color.white;
            RoundCorners(b, 6); NoBorder(b);
            AddButtonFeedback(b, new Color(0.55f, 0.20f, 0.22f));
        }

        // Manual hover/press/disabled colors (inline styles override UI Toolkit's :hover/:active).
        static void AddButtonFeedback(Button b, Color baseColor)
        {
            Color hover  = Lighten(baseColor, 0.10f);
            Color active = Lighten(baseColor, -0.14f);

            b.style.backgroundColor = baseColor;
            b.style.transitionProperty = new List<StylePropertyName> { "background-color" };
            b.style.transitionDuration = new List<TimeValue> { new TimeValue(80, TimeUnit.Millisecond) };

            bool hovering = false;
            bool pressed = false;

            void Apply()
            {
                if (!b.enabledSelf) { b.style.backgroundColor = Dim(baseColor); return; }
                b.style.backgroundColor = pressed ? active : (hovering ? hover : baseColor);
            }

            b.RegisterCallback<PointerEnterEvent>(_ => { hovering = true; Apply(); });
            b.RegisterCallback<PointerLeaveEvent>(_ => { hovering = false; pressed = false; Apply(); });
            b.RegisterCallback<PointerDownEvent>(_ => { pressed = true; Apply(); });
            b.RegisterCallback<PointerUpEvent>(_ => { pressed = false; Apply(); });
            b.RegisterCallback<AttachToPanelEvent>(_ => Apply());
        }

        static Color Lighten(Color c, float amount)
        {
            return new Color(
                Mathf.Clamp01(c.r + amount),
                Mathf.Clamp01(c.g + amount),
                Mathf.Clamp01(c.b + amount),
                c.a);
        }

        static Color Dim(Color c) => new Color(c.r, c.g, c.b, 0.45f);

        void SaveConfig()
        {
            if (_config == null) return;
            EditorUtility.SetDirty(_config);
            AssetDatabase.SaveAssetIfDirty(_config);
        }

        // iOS bundle ID specifically, Application.identifier returns the active platform's,
        // which would be wrong when Android is the build target. iCloud KVS is iOS-only.
        static string IosBundleId()
        {
            string id = PlayerSettings.GetApplicationIdentifier(NamedBuildTarget.iOS);
            return string.IsNullOrEmpty(id) ? "(set your iOS bundle ID in Player Settings)" : id;
        }

        static NexusConfig LoadOrCreateConfig()
        {
            var cfg = AssetDatabase.LoadAssetAtPath<NexusConfig>(ConfigAssetPath);
            if (cfg != null) return cfg;

            Directory.CreateDirectory("Assets/DavanciNexus/Resources");
            cfg = ScriptableObject.CreateInstance<NexusConfig>();
            AssetDatabase.CreateAsset(cfg, ConfigAssetPath);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            return cfg;
        }
    }
}
