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

namespace Getly.Publisher
{
    /// <summary>
    /// Getly's publishing window: check the package, sign in, submit it.
    ///
    /// That order is the design. The check needs no account and no network, and
    /// it is the reason a publisher installs this at all — so it is the front
    /// door. Signing in is something you do before SUBMITTING, not before
    /// looking.
    ///
    /// One screen, not a wizard: an asset is checked many times and submitted
    /// once, and a wizard would make the repeated act the expensive one.
    /// </summary>
    public class GetlyPublisherWindow : EditorWindow
    {
        /// <summary>Only sign-in has stages. Nothing else is gated on anything.</summary>
        private enum Stage { SignedOut, Waiting, SignedIn }

        /// <summary>Room for the vertical scrollbar, so its arrival never reflows the text.</summary>
        private const float ScrollbarAllowance = 20f;

        private Stage _stage = Stage.SignedOut;
        private Vector2 _scroll;
        private bool _proSkinAtLastRepaint;

        // sign-in
        private GetlyApi.DeviceStart _device;
        private string _signInError;
        private bool _showAdvanced;
        private double _nextPollAt;

        // package
        private DefaultAsset _folder;
        private List<GetlyFinding> _findings;
        private bool _hasChecked;
        private readonly HashSet<string> _expanded = new HashSet<string>();

        // Submission. [SerializeField] because Unity recompiles scripts on
        // every save and rebuilds this window from its serialised fields —
        // without it a seller who edits one line of their own code loses the
        // name and the 150+ characters of description they just typed.
        [SerializeField] private string _name = string.Empty;
        [SerializeField] private string _description = string.Empty;
        [SerializeField] private float _price = 10f;
        [SerializeField] private string _status;
        [SerializeField] private bool _statusFailed;
        private bool _busy;

        [MenuItem("Window/Getly/Publish Asset")]
        public static void Open()
        {
            var window = GetWindow<GetlyPublisherWindow>("Getly");
            window.minSize = new Vector2(440, 560);
            window.Show();
        }

        private void OnEnable()
        {
            _stage = GetlySession.IsSignedIn ? Stage.SignedIn : Stage.SignedOut;
            _proSkinAtLastRepaint = EditorGUIUtility.isProSkin;
        }

        /// <summary>
        /// Without this the "Use selected" button stays greyed until something
        /// else happens to repaint the window — which reads as a dead button.
        /// </summary>
        private void OnSelectionChange() => Repaint();

        private void OnGUI()
        {
            // Styles cache their colours, and the editor theme can change under
            // an open window — otherwise black text on black.
            if (_proSkinAtLastRepaint != EditorGUIUtility.isProSkin)
            {
                _proSkinAtLastRepaint = EditorGUIUtility.isProSkin;
                GetlyUI.InvalidateStyles();
            }

            DrawHeaderBand();

            _scroll = EditorGUILayout.BeginScrollView(_scroll);
            GUILayout.Space(GetlyUI.Gutter);

            // An EXPLICIT content width, not "whatever fits".
            //
            // A text area holding a long description reports a large minimum
            // width, the enclosing group takes the maximum of its children's
            // minimums, and the whole body ends up wider than the window: a
            // horizontal scrollbar appears, labels stop wrapping, and the
            // framed mascot is pushed off the right edge. Pinning the width
            // makes every child wrap instead of pushing.
            // currentViewWidth, not position.width: position includes the OS
            // window frame, and being a few pixels optimistic is the difference
            // between a clean layout and a horizontal scrollbar.
            var viewWidth = EditorGUIUtility.currentViewWidth;
            var contentWidth = Mathf.Max(240f, viewWidth - GetlyUI.Gutter * 2f - ScrollbarAllowance);
            GetlyUI.SetContentWidth(contentWidth);

            using (new GUILayout.HorizontalScope())
            {
                GUILayout.Space(GetlyUI.Gutter);
                using (new GUILayout.VerticalScope(GUILayout.Width(contentWidth)))
                {
                    // Approving a device in a browser is the one genuinely modal
                    // moment — the seller is matching a code, and nothing else
                    // should compete for the screen.
                    if (_stage == Stage.Waiting) DrawWaiting();
                    else DrawBody();
                }
                GUILayout.Space(GetlyUI.Gutter);
            }

            GUILayout.Space(GetlyUI.Gutter * 2f);
            EditorGUILayout.EndScrollView();
        }

        // ---- header -------------------------------------------------------------

        /// <summary>
        /// Mark on the left, account state on the right. Above the scroll, so the
        /// tool always says whose store it is pointed at.
        /// </summary>
        private void DrawHeaderBand()
        {
            var band = EditorGUILayout.BeginVertical();
            GetlyUI.Fill(band, GetlyBrand.Surface);
            GUILayout.Space(10f);

            using (new GUILayout.HorizontalScope())
            {
                GUILayout.Space(GetlyUI.Gutter);
                GetlyUI.Mark(_stage == Stage.SignedIn ? "Publisher" : null);

                if (_stage == Stage.SignedIn)
                {
                    // Bounded: a store name is whatever the seller typed, and an
                    // unbounded one runs past the window edge.
                    const float accountWidth = 190f;
                    using (new GUILayout.VerticalScope(GUILayout.Width(accountWidth)))
                    {
                        GUILayout.Space(2f);
                        var store = string.IsNullOrEmpty(GetlySession.StoreName) ? "Signed in" : GetlySession.StoreName;
                        var label = new GUIStyle(GetlyUI.Label) { alignment = TextAnchor.MiddleRight, wordWrap = false };
                        GUILayout.Label(new GUIContent(GetlyUI.Elide(store, label, accountWidth), store), label);
                        using (new GUILayout.HorizontalScope())
                        {
                            GUILayout.FlexibleSpace();
                            // Not during a submission: signing out revokes the
                            // key the in-flight upload is authenticating with,
                            // and the seller would watch it fail for no stated
                            // reason.
                            using (new EditorGUI.DisabledScope(_busy))
                            {
                                if (GUILayout.Button("Sign out", EditorStyles.miniButton, GUILayout.Width(64f)))
                                {
                                    GetlySession.SignOut();
                                    _stage = Stage.SignedOut;
                                }
                            }
                        }
                    }
                }
                else if (_stage == Stage.SignedOut)
                {
                    using (new GUILayout.VerticalScope())
                    {
                        GUILayout.Space(6f);
                        if (GetlyUI.SecondaryButton("Sign in", 78f, !_busy)) BeginSignIn();
                    }
                }

                GUILayout.Space(GetlyUI.Gutter);
            }

            GUILayout.Space(10f);
            EditorGUILayout.EndVertical();
            GetlyUI.Fill(new Rect(band.x, band.yMax - 1f, band.width, 1f), GetlyBrand.Hairline);
        }

        // ---- body ---------------------------------------------------------------

        private void DrawBody()
        {
            if (!string.IsNullOrEmpty(_signInError)) GetlyUI.Problem(_signInError);

            DrawPackageSection();
            GetlyUI.Rule(GetlyUI.Gutter * 1.5f, GetlyUI.Gutter * 1.5f);
            DrawListingSection();

            if (!string.IsNullOrEmpty(_status))
            {
                GUILayout.Space(GetlyUI.Gutter);
                var card = GetlyUI.BeginCard();
                using (GetlyUI.CardRow())
                {
                    // A failure has to look like one. Seven distinct failures
                    // and the success used to render in the same neutral card,
                    // so the seller had to read carefully to learn whether
                    // their asset had been uploaded at all.
                    if (_statusFailed)
                    {
                        GetlyUI.SeverityDot(GetlyBrand.Alert);
                        GUILayout.Space(8f);
                    }
                    GetlyUI.Paragraph(_status, GetlyUI.Body,
                        GetlyUI.ContentWidth - (_statusFailed ? 40f : 24f));
                    GUILayout.Space(12f);
                }
                GetlyUI.EndCard(card, _statusFailed ? GetlyBrand.Alert : (Color?)null);
            }

            // Shown whatever the sign-in state: an override set while signed
            // out used to become invisible the moment it worked, so the one
            // setting that explains "why is this pointed somewhere odd" was
            // hidden exactly when someone would go looking for it.
            GUILayout.Space(GetlyUI.Gutter * 1.5f);
            DrawServerOverride();
        }

        // ---- the check ----------------------------------------------------------

        private void DrawPackageSection()
        {
            GUILayout.Label("STEP 1", GetlyUI.Eyebrow);

            // Nothing chosen yet: this is the screen a publisher meets first, so
            // it says what the tool is for — copy on the left, the mascot framed
            // on the right. Never text with emptiness beside it.
            if (_folder == null && !_hasChecked)
            {
                var leftWidth = Mathf.Max(120f, GetlyUI.ContentWidth - GetlyUI.MascotSize - GetlyUI.Gutter);
                GetlyUI.SplitWithMascot(() =>
                {
                    GUILayout.Label("Check it before a buyer does", GetlyUI.Display);
                    GUILayout.Space(6f);
                    GetlyUI.Paragraph(
                        "We read your package the way the buyer's project will — broken script "
                        + "references, missing .meta files, code that will not compile in a build.",
                        GetlyUI.Muted, leftWidth);
                    GUILayout.Space(8f);
                    GUILayout.Label("Runs offline. No account needed.", GetlyUI.Muted);
                });
                GUILayout.Space(GetlyUI.Gutter);
            }
            else
            {
                GUILayout.Label("Your asset folder", GetlyUI.Heading);
                GUILayout.Space(6f);
            }

            DrawFolderPicker();

            if (_hasChecked) DrawFindings();
        }

        private void DrawFolderPicker()
        {
            using (new GUILayout.HorizontalScope())
            {
                var picked = (DefaultAsset)EditorGUILayout.ObjectField(_folder, typeof(DefaultAsset), false);
                if (picked != _folder) SetFolder(picked);

                // Dragging into an object field is not the first thing anyone
                // reaches for. Selecting a folder in the Project window is.
                var selected = Selection.activeObject as DefaultAsset;
                if (GetlyUI.SecondaryButton("Use selected", 96f, selected != null && selected != _folder))
                {
                    SetFolder(selected);
                }
            }

            GUILayout.Space(8f);

            var valid = _folder != null && AssetDatabase.IsValidFolder(AssetDatabase.GetAssetPath(_folder));

            if (GetlyUI.PrimaryButton(_hasChecked ? "Check again" : "Check my package", valid && !_busy))
            {
                RunCheck();
            }

            // Every disabled control in this tool says why. A dead button with
            // no explanation is the most common way software reads as broken.
            if (_folder == null)
            {
                GetlyUI.Hint("Pick the single folder that holds everything you are selling — drag it in, "
                             + "or select it in the Project window and press Use selected.");
            }
            else if (!valid)
            {
                GetlyUI.Hint("That is a file, not a folder. Pick the folder that holds everything you are selling.");
            }
        }

        private void RunCheck()
        {
            var path = AssetDatabase.GetAssetPath(_folder);
            try
            {
                EditorUtility.DisplayProgressBar("Getly", "Reading your package…", 0.3f);
                _findings = GetlyValidator.Validate(GetlyCollector.Collect(path));
                _hasChecked = true;
                _expanded.Clear();
            }
            finally
            {
                EditorUtility.ClearProgressBar();
            }
        }

        /// <summary>
        /// Changing the folder invalidates the findings AND the sign-in error.
        ///
        /// The error was true when it happened, but it sits above the check and
        /// reads as "this whole tool is broken" long after the seller has moved
        /// on to the part that never needed a server.
        /// </summary>
        private void SetFolder(DefaultAsset folder)
        {
            _folder = folder;
            _findings = null;
            _hasChecked = false;
            _signInError = null;
            _status = null;
            _expanded.Clear();
        }

        // ---- findings -----------------------------------------------------------

        private void DrawFindings()
        {
            GUILayout.Space(GetlyUI.Gutter);

            var blocking = _findings.Where(f => f.IsBlocking).ToList();
            var advice = _findings.Where(f => !f.IsBlocking).ToList();

            if (blocking.Count == 0)
            {
                // Nothing blocks — say so plainly, whether or not there is
                // advice underneath. Without this a package with one cosmetic
                // suggestion looked identical to one that cannot be sold.
                var leftWidth = Mathf.Max(120f, GetlyUI.ContentWidth - GetlyUI.MascotSize - GetlyUI.Gutter);
                GetlyUI.SplitWithMascot(() =>
                {
                    GUILayout.Label(advice.Count == 0 ? "Nothing to fix" : "Ready to publish", GetlyUI.Display);
                    GUILayout.Space(6f);
                    GetlyUI.Paragraph(
                        advice.Count == 0
                            ? "This package imports clean. Every reference resolves, nothing points at "
                              + "your machine, and no runtime code needs the editor."
                            : "Nothing here stops a sale. The suggestions below would make the asset "
                              + "better, and you can publish without any of them.",
                        GetlyUI.Muted, leftWidth);
                });
                if (advice.Count > 0) GUILayout.Space(GetlyUI.Gutter);
            }

            if (blocking.Count > 0)
            {
                GUILayout.Label(blocking.Count == 1 ? "1 PROBLEM" : blocking.Count + " PROBLEMS", GetlyUI.Eyebrow);
                GUILayout.Label(
                    blocking.Count == 1
                        ? "A buyer would receive this broken. Fix it, then check again."
                        : "A buyer would receive these broken. Fix them, then check again.",
                    GetlyUI.Muted);
                GUILayout.Space(8f);
                DrawFindingGroups(blocking, true);
            }

            if (advice.Count > 0)
            {
                if (blocking.Count > 0) GUILayout.Space(GetlyUI.Gutter);
                GUILayout.Label(advice.Count == 1 ? "1 SUGGESTION" : advice.Count + " SUGGESTIONS", GetlyUI.Eyebrow);
                // Stated as suggestions because they are — a seller who knows
                // their asset better than our rule must still be able to ship.
                GUILayout.Label("None of these stop you publishing.", GetlyUI.Muted);
                GUILayout.Space(8f);
                DrawFindingGroups(advice, false);
            }
        }

        /// <summary>
        /// One card per KIND of problem, not per file.
        ///
        /// A pack of two hundred scripts with no namespace produced two hundred
        /// identical cards, which is not a report — it is a wall that hides the
        /// three findings that actually matter. The paths live inside the card.
        /// </summary>
        private void DrawFindingGroups(List<GetlyFinding> findings, bool blocking)
        {
            var byId = new List<string>();
            var groups = new Dictionary<string, List<GetlyFinding>>();
            foreach (var finding in findings)
            {
                if (!groups.TryGetValue(finding.Id, out var bucket))
                {
                    bucket = new List<GetlyFinding>();
                    groups[finding.Id] = bucket;
                    byId.Add(finding.Id);
                }
                bucket.Add(finding);
            }

            for (var i = 0; i < byId.Count; i++) DrawFinding(groups[byId[i]], i, blocking);
        }

        /// <summary>How many affected files to list before summarising the rest.</summary>
        private const int PathsShownPerFinding = 5;

        private void DrawFinding(List<GetlyFinding> group, int index, bool blocking)
        {
            var finding = group[0];
            var check = finding.Check;
            var key = finding.Id + "#" + index;
            var open = _expanded.Contains(key);

            var card = GetlyUI.BeginCard();

            // The card's own padding, stated once: 12 left + 8 dot + 8 gap +
            // 12 right. Without an explicit inner width a long asset path
            // pushes the card wider than the window.
            var inner = Mathf.Max(120f, GetlyUI.ContentWidth - 40f);

            using (GetlyUI.CardRow())
            {
                GetlyUI.SeverityDot(blocking ? GetlyBrand.Alert : GetlyBrand.Muted);
                GUILayout.Space(8f);

                using (new GUILayout.VerticalScope(GUILayout.Width(inner)))
                {
                    var title = check != null ? check.Title : finding.Id;
                    if (group.Count > 1) title += "  ×" + group.Count;
                    GetlyUI.Paragraph(title, GetlyUI.Label, inner);

                    var shown = 0;
                    foreach (var item in group)
                    {
                        if (string.IsNullOrEmpty(item.Path)) continue;
                        if (shown >= PathsShownPerFinding) break;
                        shown++;
                        using (new GUILayout.HorizontalScope())
                        {
                            GetlyUI.Paragraph(item.Path, GetlyUI.Mono, inner - 52f);
                            if (GUILayout.Button("Show", EditorStyles.miniButton, GUILayout.Width(44f)))
                            {
                                var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(item.Path);
                                if (asset != null) EditorGUIUtility.PingObject(asset);
                            }
                        }
                    }

                    var withPaths = group.Count(g => !string.IsNullOrEmpty(g.Path));
                    if (withPaths > shown)
                    {
                        // Said as a number rather than as fifty more rows: the
                        // count is the useful part once the pattern is clear.
                        GUILayout.Label("and " + (withPaths - shown) + " more file"
                                        + (withPaths - shown == 1 ? "" : "s"), GetlyUI.Mono);
                    }

                    if (!string.IsNullOrEmpty(finding.Detail)) GetlyUI.Paragraph(finding.Detail, GetlyUI.Mono, inner);

                    if (check != null)
                    {
                        GUILayout.Space(4f);
                        // Collapsed so nine findings stay readable at a glance;
                        // the reason is one click away, never hidden.
                        if (GUILayout.Button(open ? "Why this matters  ▾" : "Why this matters  ▸",
                                EditorStyles.miniLabel, GUILayout.Width(130f)))
                        {
                            if (open) _expanded.Remove(key); else _expanded.Add(key);
                        }

                        if (open)
                        {
                            GUILayout.Space(2f);
                            // Why before how: a rule without a reason gets worked
                            // around, and this one costs the seller a refund.
                            GetlyUI.Paragraph(check.Why, GetlyUI.Muted, inner);
                            GUILayout.Space(6f);
                            GUILayout.Label("FIX", GetlyUI.Eyebrow);
                            GetlyUI.Paragraph(check.Fix, GetlyUI.Muted, inner);
                        }
                    }
                }

                GUILayout.Space(12f);
            }

            GetlyUI.EndCard(card);
            GUILayout.Space(6f);
        }

        // ---- listing ------------------------------------------------------------

        private void DrawListingSection()
        {
            GUILayout.Label("STEP 2", GetlyUI.Eyebrow);
            GUILayout.Label("Your listing", GetlyUI.Heading);
            GUILayout.Space(8f);

            _name = GetlyUI.TextField("Name", _name);

            GUILayout.Space(8f);
            _description = GetlyUI.TextArea("Description", _description, 76f);
            GUILayout.Label(
                _description.Length >= 150
                    ? _description.Length + " characters"
                    : _description.Length + " / 150 characters — this is what buyers and search engines read",
                GetlyUI.Mono);

            GUILayout.Space(8f);
            // Clamped where it is read, not where it is sent: an unclamped
            // field would let a negative price through Mathf.RoundToInt into
            // the API as -100 cents.
            _price = Mathf.Max(0f, GetlyUI.FloatField("Price (USD)", _price));

            GUILayout.Space(GetlyUI.Gutter);

            var blocked = _hasChecked && _findings != null && GetlyValidator.HasBlocking(_findings);
            var signedIn = _stage == Stage.SignedIn;
            var ready = signedIn && _hasChecked && !blocked && !_busy
                        && !string.IsNullOrWhiteSpace(SafeFileName(_name))
                        && _description.Length >= 150
                        && _price >= 0f;

            if (GetlyUI.PrimaryButton("Submit to Getly as a draft", ready)) Submit();

            // One reason at a time, in the order the seller has to resolve them.
            if (!signedIn) GetlyUI.Hint("Sign in to submit. Everything above works without it.");
            else if (!_hasChecked) GetlyUI.Hint("Run the check first.");
            else if (blocked) GetlyUI.Hint("Fix the problems above, then check again.");
            else if (string.IsNullOrWhiteSpace(_name)) GetlyUI.Hint("Give the listing a name.");
            else if (_description.Length < 150) GetlyUI.Hint("The description needs at least 150 characters.");
            else GetlyUI.Hint("This creates a draft. You add images and publish it on getly.store.");
        }

        // ---- sign in ------------------------------------------------------------

        private void BeginSignIn()
        {
            _busy = true;
            _signInError = null;
            GetlyApi.StartDeviceAuth((result, error) =>
            {
                _busy = false;
                if (error != null || result == null)
                {
                    _signInError = error ?? "Could not start sign-in.";
                    Repaint();
                    return;
                }
                _device = result;
                _stage = Stage.Waiting;
                _nextPollAt = EditorApplication.timeSinceStartup + result.interval;
                // The browser opens on the code so the seller does not retype
                // what the machine already knows.
                Application.OpenURL(result.verificationUrl + "?code=" + Uri.EscapeDataString(result.userCode));
                EditorApplication.update += PollForApproval;
                Repaint();
            });
        }

        private void DrawWaiting()
        {
            GUILayout.Label("WAITING FOR YOU", GetlyUI.Eyebrow);

            var leftWidth = Mathf.Max(120f, GetlyUI.ContentWidth - GetlyUI.MascotSize - GetlyUI.Gutter);
            GetlyUI.SplitWithMascot(() =>
            {
                GUILayout.Label("Approve this editor", GetlyUI.Display);
                GUILayout.Space(6f);
                GetlyUI.Paragraph(
                    "Your browser should have opened. The code there must match the one below.",
                    GetlyUI.Muted, leftWidth);
            });

            GUILayout.Space(GetlyUI.Gutter);

            var card = GetlyUI.BeginCard(GetlyBrand.Surface);
            var codeStyle = new GUIStyle(GetlyUI.Display)
            {
                fontSize = 30,
                alignment = TextAnchor.MiddleCenter,
            };
            GUILayout.Label(_device != null ? _device.userCode : "…", codeStyle, GUILayout.Height(42f));
            GetlyUI.EndCard(card);

            GUILayout.Space(GetlyUI.Gutter);

            if (GetlyUI.SecondaryButton("Open the page again") && _device != null)
            {
                Application.OpenURL(_device.verificationUrl + "?code=" + Uri.EscapeDataString(_device.userCode));
            }
            GUILayout.Space(4f);
            if (GetlyUI.SecondaryButton("Cancel"))
            {
                EditorApplication.update -= PollForApproval;
                _stage = Stage.SignedOut;
            }

            if (!string.IsNullOrEmpty(_signInError))
            {
                GUILayout.Space(GetlyUI.Gutter);
                GetlyUI.Problem(_signInError);
            }
        }

        private void PollForApproval()
        {
            if (_device == null) { EditorApplication.update -= PollForApproval; return; }
            if (EditorApplication.timeSinceStartup < _nextPollAt) return;
            _nextPollAt = EditorApplication.timeSinceStartup + Math.Max(_device.interval, 3);

            GetlyApi.PollDeviceAuth(_device.deviceCode, (poll, error) =>
            {
                if (error != null || poll == null) return; // transient; keep waiting

                switch (poll.status)
                {
                    case "approved":
                        EditorApplication.update -= PollForApproval;
                        GetlySession.SignIn(poll.apiKey, poll.storeId, poll.storeName);
                        _stage = Stage.SignedIn;
                        _device = null;
                        Repaint();
                        break;
                    case "denied":
                        EditorApplication.update -= PollForApproval;
                        _signInError = "The request was refused in the browser.";
                        _stage = Stage.SignedOut;
                        Repaint();
                        break;
                    case "expired":
                    case "unknown":
                        EditorApplication.update -= PollForApproval;
                        _signInError = "That code expired. Start again.";
                        _stage = Stage.SignedOut;
                        Repaint();
                        break;
                }
            });
        }

        /// <summary>
        /// Which Getly this editor talks to.
        ///
        /// Folded away and defaulted to production, because a seller has no
        /// reason to touch it and a wrong value here looks exactly like a broken
        /// tool. It exists because there is otherwise no way to exercise sign-in
        /// against anything but the live site.
        /// </summary>
        private void DrawServerOverride()
        {
            _showAdvanced = EditorGUILayout.Foldout(_showAdvanced, "Advanced", true);
            if (!_showAdvanced) return;

            GUILayout.Space(6f);
            var card = GetlyUI.BeginCard();
            using (GetlyUI.CardRow())
            {
                using (new GUILayout.VerticalScope())
                {
                    GUILayout.Label("SERVER", GetlyUI.Eyebrow);
                    var edited = EditorGUILayout.TextField(GetlySession.BaseUrl);
                    if (edited != GetlySession.BaseUrl) GetlySession.BaseUrl = edited;
                    GUILayout.Label("Leave this alone unless you were asked to change it.", GetlyUI.Muted);

                    if (GetlySession.BaseUrl != GetlySession.DefaultBaseUrl)
                    {
                        // A forgotten override is the most confusing state this
                        // tool can be in, so it says so rather than hinting.
                        GUILayout.Space(6f);
                        GUILayout.Label("Pointed away from getly.store.", GetlyUI.Label);
                        if (GetlyUI.SecondaryButton("Reset to getly.store", 160f))
                        {
                            GetlySession.BaseUrl = GetlySession.DefaultBaseUrl;
                        }
                    }
                }
                GUILayout.Space(12f);
            }
            GetlyUI.EndCard(card);
        }

        // ---- submit -------------------------------------------------------------

        /// <summary>
        /// Begin a submission.
        ///
        /// Nothing heavy happens here. Exporting a package the size this tool
        /// exists for takes tens of seconds, and doing it inside OnGUI blocks
        /// the editor for all of it — with the window still showing the frame
        /// from before the click, so "Exporting your package…" is written to a
        /// field that never gets painted. The work moves to the next editor
        /// tick, by which time the seller can see what is happening.
        /// </summary>
        private void Submit()
        {
            _busy = true;
            _status = "Exporting your package…";
            Repaint();
            GetlyEditorLoop.RunOnce(ExportAndUpload);
        }

        private void ExportAndUpload()
        {
            var folderPath = AssetDatabase.GetAssetPath(_folder);
            var packagePath = Path.Combine(Path.GetTempPath(), SafeFileName(_name) + ".unitypackage");
            byte[] bytes;

            try
            {
                AssetDatabase.ExportPackage(folderPath, packagePath, ExportPackageOptions.Recurse);
                // Inside the SAME try as the export: ExportPackage does not
                // throw when it fails to write, so the missing file surfaces
                // here instead — and an escape from this point would leave
                // _busy stuck true, i.e. a dead Submit button whose own hint
                // says it works.
                bytes = File.ReadAllBytes(packagePath);
            }
            catch (Exception error)
            {
                Finish("Could not export the package: " + error.Message, failed: true);
                return;
            }

            var submission = new GetlySubmission
            {
                Name = _name.Trim(),
                Description = _description.Trim(),
                PriceCents = Mathf.RoundToInt(_price * 100f),
                FolderPath = folderPath,
            };

            _status = "Creating the draft…";
            Repaint();

            GetlyApi.CreateDraft(submission, (product, error) =>
            {
                if (error != null || product == null)
                {
                    Finish("Could not create the draft: " + (error ?? "unknown error"), failed: true);
                    return;
                }

                var fileName = Path.GetFileName(packagePath);
                GetlyApi.PresignUpload(product.id, fileName, bytes.LongLength, (presign, presignError) =>
                {
                    if (presignError != null || presign == null)
                    {
                        Finish("Could not prepare the upload: " + (presignError ?? "unknown error"), failed: true);
                        return;
                    }

                    _status = "Uploading… 0%";
                    Repaint();

                    GetlyApi.UploadFile(presign.uploadUrl, bytes,
                        progress =>
                        {
                            _status = "Uploading… " + Mathf.RoundToInt(progress * 100f) + "%";
                            Repaint();
                        },
                        uploadError =>
                        {
                            if (uploadError != null) { Finish("The upload failed: " + uploadError, failed: true); return; }

                            _status = "Attaching the file…";
                            Repaint();

                            GetlyApi.AttachFile(product.id, presign.fileUrl, fileName, bytes.LongLength, attachError =>
                            {
                                if (attachError != null) { Finish("Could not attach the file: " + attachError, failed: true); return; }

                                _status = "Getly is checking your package…";
                                Repaint();

                                GetlyApi.Verify(product.id, _findings ?? new List<GetlyFinding>(), (verify, verifyError) =>
                                {
                                    TryDelete(packagePath);

                                    if (verifyError != null || verify == null)
                                    {
                                        Finish("Uploaded, but the check did not run: " + (verifyError ?? "unknown error")
                                               + "\nYour draft is on getly.store — open it there.", failed: true);
                                        return;
                                    }

                                    if (!verify.canPublish)
                                    {
                                        var titles = verify.findings == null
                                            ? string.Empty
                                            : string.Join("\n• ", verify.findings
                                                .Where(f => f.severity == "error")
                                                .Select(f => f.title));
                                        Finish("Uploaded as a draft, but it cannot go live yet:\n• " + titles, failed: true);
                                        return;
                                    }

                                    Finish("Submitted. Your draft is on getly.store — add images and publish it there.");
                                });
                            });
                        });
                });
            });
        }

        /// <summary>
        /// End a submission and say how it went.
        ///
        /// `failed` exists because seven distinct failures and the success all
        /// used to render in one identical neutral card — so a seller had to
        /// read carefully to find out whether their asset was uploaded.
        /// </summary>
        private void Finish(string message, bool failed = false)
        {
            _busy = false;
            _status = message;
            _statusFailed = failed;
            Repaint();
        }

        private static void TryDelete(string path)
        {
            try { if (File.Exists(path)) File.Delete(path); }
            catch (Exception) { /* a temp file we could not remove is not worth reporting */ }
        }

        /// <summary>
        /// A filename from a listing name.
        ///
        /// Trimmed BEFORE the emptiness test, not after: a name of only
        /// punctuation reduces to "---", which is not whitespace, passes the
        /// guard, and then trims to nothing — producing a file literally called
        /// ".unitypackage".
        /// </summary>
        private static string SafeFileName(string value)
        {
            var cleaned = new string((value ?? string.Empty)
                .Select(c => char.IsLetterOrDigit(c) || c == '-' || c == '_' ? c : '-')
                .ToArray()).Trim('-');
            return string.IsNullOrWhiteSpace(cleaned) ? "package" : cleaned;
        }
    }
}
