using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
using AvatarRefinery.Editor.Core;
using AvatarRefinery.Editor.Core.Rules;
using L10n = AvatarRefinery.Editor.Core.L10n;
using Object = UnityEngine.Object;

namespace AvatarRefinery.Editor.UI
{
    /// <summary>
    /// Avatar Refinery のメインウィンドウ(Lite)。
    /// 3クリック原則: 開く → アバターを選ぶ(選択中なら自動)→ 診断する。
    /// </summary>
    public sealed class RefineryWindow : EditorWindow
    {
        private const string MenuRoot = "Tools/Avatar Refinery/";

        [MenuItem(MenuRoot + "Open Avatar Refinery", priority = 0)]
        public static void Open()
        {
            var w = GetWindow<RefineryWindow>();
            w.titleContent = new GUIContent("Avatar Refinery");
            w.minSize = new Vector2(420, 480);
            w.Show();
        }

        [MenuItem(MenuRoot + "Check Selected Avatar", priority = 1)]
        public static void DiagnoseSelection()
        {
            var w = GetWindow<RefineryWindow>();
            w.titleContent = new GUIContent("Avatar Refinery");
            w.Show();
            var root = FindAvatarRoot(Selection.activeGameObject);
            if (root != null) { w._avatar = root; w.RunDiagnosis(); }
        }

        [MenuItem("GameObject/Avatar Refinery/Check This Avatar", false, 20)]
        private static void DiagnoseFromHierarchy(MenuCommand cmd)
        {
            var go = cmd.context as GameObject ?? Selection.activeGameObject;
            var w = GetWindow<RefineryWindow>();
            w.titleContent = new GUIContent("Avatar Refinery");
            w.Show();
            var root = FindAvatarRoot(go);
            if (root != null) { w._avatar = root; w.RunDiagnosis(); }
        }

        // ---- state
        private GameObject _avatar;
        private DiagnosisReport _report;
        /// <summary>修正→再診断の Before/After 用に保持する直前の結果(製品版が使う)</summary>
        private DiagnosisReport _previousReport;
        private string _lastError;
        private bool _detailed;
        private Vector2 _scroll;
        private readonly Dictionary<string, bool> _expanded = new Dictionary<string, bool>();
        private readonly HashSet<string> _ignored = new HashSet<string>();
        private readonly bool[] _showSeverity = { true, true, true, true };
        private double _lastRunSeconds;

        // ---- styles
        private GUIStyle _title, _rankBox, _rankText, _wrap, _issueHeader, _small, _sectionLabel, _severityToggle;
        private bool _stylesReady;

        private void OnEnable()
        {
            _detailed = EditorPrefs.GetBool("AvatarRefinery.Detailed", false);
            if (_avatar == null) _avatar = FindAvatarRoot(Selection.activeGameObject);
            Selection.selectionChanged += OnSelectionChanged;
        }
        private void OnDisable() { Selection.selectionChanged -= OnSelectionChanged; }

        private void OnSelectionChanged()
        {
            // まだ診断していない時だけ、選択に追従する
            if (_report == null)
            {
                var root = FindAvatarRoot(Selection.activeGameObject);
                if (root != null) { _avatar = root; Repaint(); }
            }
        }

        private void EnsureStyles()
        {
            if (_stylesReady) return;
            _title = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 };
            _rankBox = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(16, 10, 8, 8) };
            _rankText = new GUIStyle(EditorStyles.boldLabel) { fontSize = 18, alignment = TextAnchor.MiddleLeft, richText = true };
            _wrap = new GUIStyle(EditorStyles.label) { wordWrap = true, richText = true };
            _issueHeader = new GUIStyle(EditorStyles.boldLabel) { wordWrap = true, richText = true, fontSize = 12, alignment = TextAnchor.MiddleLeft };
            _small = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true, richText = true };
            _sectionLabel = new GUIStyle(EditorStyles.miniBoldLabel);
            _severityToggle = new GUIStyle(EditorStyles.toolbarButton) { richText = true };
            _stylesReady = true;
        }

        private void OnGUI()
        {
            EnsureStyles();
            DrawToolbar();
            EditorGUILayout.Space(4);

            if (!string.IsNullOrEmpty(_lastError))
                EditorGUILayout.HelpBox(L10n.T("診断中にエラーが発生しました:\n", "An error occurred during the preflight check:\n") + _lastError, MessageType.Error);

            if (_report == null) { DrawEmptyState(); return; }

            _scroll = EditorGUILayout.BeginScrollView(_scroll);
            DrawSummary();
            EditorGUILayout.Space(6);
            DrawIssues();
            EditorGUILayout.Space(6);
            DrawFooter();
            EditorGUILayout.EndScrollView();
        }

        // ------------------------------------------------------------------ toolbar / empty

        /// <summary>これより狭いと、詳細モード/レポート保存を 2 段目のツールバーへ送る(最小幅 420 でボタンが切れないように)</summary>
        private const float NarrowToolbarWidth = 520f;

        private void DrawToolbar()
        {
            bool narrow = position.width < NarrowToolbarWidth;
            using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
            {
                GUILayout.Label(L10n.T("アバター", "Avatar"), GUILayout.Width(50));
                var newAvatar = (GameObject)EditorGUILayout.ObjectField(_avatar, typeof(GameObject), true, GUILayout.MinWidth(120));
                if (newAvatar != _avatar) { _avatar = newAvatar; _report = null; _previousReport = null; _lastError = null; }
                GUI.enabled = _avatar != null;
                if (GUILayout.Button(L10n.T("診断する", "Check Avatar"), EditorStyles.toolbarButton, GUILayout.Width(L10n.IsEnglish ? 88 : 70))) RunDiagnosis();
                GUI.enabled = true;
                if (!narrow) DrawToolbarRight();
                else GUILayout.FlexibleSpace();
            }
            if (narrow)
            {
                using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) DrawToolbarRight();
            }
        }

        private void DrawToolbarRight()
        {
            if (FixHost.Available) GUILayout.Label(FixHost.Current.ProductLabel, EditorStyles.miniLabel, GUILayout.Width(44));
            GUILayout.FlexibleSpace();
            var oldLanguage = L10n.Preference;
            var language = (RefineryLanguage)EditorGUILayout.Popup((int)oldLanguage, new[] { "Auto", "日本語", "English" }, EditorStyles.toolbarPopup, GUILayout.Width(76));
            if (language != oldLanguage)
            {
                L10n.Preference = language;
                _stylesReady = false;
                if (_report != null && _avatar != null) RunDiagnosis();
                Repaint();
                GUIUtility.ExitGUI();
            }
            var d = GUILayout.Toggle(_detailed, L10n.T("詳細モード", "Details"), EditorStyles.toolbarButton, GUILayout.Width(L10n.IsEnglish ? 62 : 80));
            if (d != _detailed) { _detailed = d; EditorPrefs.SetBool("AvatarRefinery.Detailed", d); }
            GUI.enabled = _report != null;
            if (GUILayout.Button(L10n.T("レポート保存", "Save Report"), EditorStyles.toolbarButton, GUILayout.Width(90))) SaveReport();
            GUI.enabled = true;
        }

        private void DrawEmptyState()
        {
            EditorGUILayout.Space(10);
            GUILayout.Label(L10n.T("Avatar Refinery — アバター健康診断", "Avatar Refinery — VRChat Avatar Preflight Check"), _title);
            EditorGUILayout.Space(4);
            EditorGUILayout.HelpBox(UiText.Wrap(
                L10n.T(
                    "アップロード前に、アバターの問題点をまとめて診断します。\n" +
                    "1. Hierarchy でアバターのルート(一番上のオブジェクト)を選ぶ\n" +
                    "2. 「診断する」を押す\n\n" +
                    "診断は読み取り専用です。アバターやアセットは一切変更しません。",
                    "Check common avatar problems before uploading to VRChat.\n" +
                    "1. Select the avatar root in the Hierarchy.\n" +
                    "2. Click Check Avatar.\n\n" +
                    "The preflight check is read-only. It does not change your avatar or assets.")),
                MessageType.Info);

            var candidates = FindAvatarCandidates();
            if (candidates.Count > 0)
            {
                EditorGUILayout.Space(6);
                GUILayout.Label(L10n.T("シーン内のアバター候補", "Avatar candidates in this Scene"), _sectionLabel);
                foreach (var c in candidates)
                {
                    using (new EditorGUILayout.HorizontalScope())
                    {
                        GUILayout.Label(c.name, GUILayout.MinWidth(120));
                        if (GUILayout.Button(L10n.T("これを診断", "Check This"), GUILayout.Width(90))) { _avatar = c; RunDiagnosis(); GUIUtility.ExitGUI(); }
                    }
                }
            }
            if (_avatar != null)
            {
                EditorGUILayout.Space(8);
                if (GUILayout.Button(L10n.IsEnglish ? $"Check “{_avatar.name}”" : $"「{_avatar.name}」を診断する", GUILayout.Height(32))) RunDiagnosis();
            }
        }

        // ------------------------------------------------------------------ summary

        private void DrawSummary()
        {
            var r = _report;
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.Label($"{r.AvatarName}", _title);
                GUILayout.FlexibleSpace();
                var badge = r.StatsFromBakedAvatar ? L10n.T("ビルド後の姿で診断", "Built avatar checked") : L10n.T("シーン上で診断", "Scene avatar checked");
                GUILayout.Label(new GUIContent(badge, r.StatsFromBakedAvatar
                    ? L10n.T("Modular Avatar / AAO などの非破壊ツールを適用した後の状態(=実際にアップロードされる姿)で性能を算定しました", "Performance was measured after applying Modular Avatar, AAO, and other NDMF build steps—the avatar that will actually be uploaded.")
                    : L10n.T("非破壊ツールの適用前の状態で算定しています", "Performance was measured from the Scene avatar before NDMF build steps.") + (string.IsNullOrEmpty(r.BakeError) ? "" : L10n.T("\n理由: ", "\nReason: ") + r.BakeError)), EditorStyles.miniLabel);
            }

            if (r.HasVrcSdk)
            {
                using (new EditorGUILayout.HorizontalScope())
                {
                    DrawRankCard("PC", r.PcOverall, MarkdownReport.PcMeaning(r.PcOverall));
                    DrawRankCard(L10n.T("Quest / スマホ", "Quest / Mobile"), r.AndroidOverall, MarkdownReport.AndroidMeaning(r.AndroidOverall));
                }
            }
            else
            {
                EditorGUILayout.HelpBox(L10n.T("VRChat SDK が無いため、汎用診断(ジオメトリ・マテリアル・リグ)のみ実行しました。", "VRChat SDK is not installed, so only general geometry, material, and rig checks were run."), MessageType.Warning);
            }

            EditorGUILayout.Space(2);
            using (new EditorGUILayout.HorizontalScope())
            {
                for (int i = 0; i < 4; i++)
                {
                    var s = (Severity)i;
                    int n = r.Issues.Count(x => x.Severity == s && !_ignored.Contains(Key(x)));
                    var label = $"{UiText.SeverityIconRich(s)} {ShortLabel(s)} {n}";
                    _showSeverity[i] = GUILayout.Toggle(_showSeverity[i], label, _severityToggle);
                }
            }
            if (_ignored.Count > 0)
            {
                using (new EditorGUILayout.HorizontalScope())
                {
                    GUILayout.Label(L10n.IsEnglish ? $"Ignored: {_ignored.Count}" : $"無視した項目: {_ignored.Count} 件", EditorStyles.miniLabel);
                    if (GUILayout.Button(L10n.T("すべて戻す", "Show All"), EditorStyles.miniButton, GUILayout.Width(80))) _ignored.Clear();
                }
            }

            // 製品版が入っていれば、まとめて修正 / Before-After などをここに描画する
            if (FixHost.Available)
            {
                EditorGUILayout.Space(2);
                FixHost.Current.DrawSummary(FixContext());
                return;
            }

            // 自動修正(製品版)で直せる件数を可視化 + 製品版への導線
            int fixable = r.Issues.Count(x => x.AutoFixable && !_ignored.Contains(Key(x)));
            if (fixable > 0)
            {
                using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox))
                {
                    GUILayout.Label(UiText.Wrap(L10n.IsEnglish ? $"Supported one-click fixes: {fixable} (Avatar Refinery Pro)" : $"🔧 自動修正で直せる項目: {fixable} 件(製品版 Avatar Refinery Pro)"), _wrap);
                    if (GUILayout.Button(new GUIContent(L10n.T("製品版を見る", "View Pro"), L10n.T("製品版(Avatar Refinery Pro)の商品ページをブラウザで開きます。実行前のプレビュー・自動バックアップ・すべて元に戻す 付き", "Open the Avatar Refinery Pro product page. Pro includes fix previews, automatic backups, and rollback.")), GUILayout.Width(90)))
                        Application.OpenURL(FeedbackLinks.ProductPage);
                }
            }
        }

        private void DrawRankCard(string platform, string rank, string meaning)
        {
            var color = RankColor(rank);
            using (var vs = new EditorGUILayout.VerticalScope(_rankBox))
            {
                // 左端の色帯(ランクを一目で)
                if (Event.current.type == EventType.Repaint)
                    EditorGUI.DrawRect(new Rect(vs.rect.x + 1, vs.rect.y + 1, 7, vs.rect.height - 2), color);
                GUILayout.Label(platform, EditorStyles.miniBoldLabel);
                GUILayout.Label($"<color=#{ColorUtility.ToHtmlStringRGB(color)}>{PerformanceRule.JpRank(rank)}</color>", _rankText);
                GUILayout.Label(UiText.Wrap(meaning), _small);
            }
        }

        // ------------------------------------------------------------------ issues

        private void DrawIssues()
        {
            var visible = _report.Sorted().Where(i => _showSeverity[(int)i.Severity] && !_ignored.Contains(Key(i))).ToList();
            if (visible.Count == 0)
            {
                EditorGUILayout.HelpBox(_report.Issues.Count == 0 ? L10n.T("問題は見つかりませんでした。", "No problems found.") : L10n.T("表示中の条件に一致する項目はありません。", "No items match the current filters."), MessageType.Info);
                return;
            }
            foreach (var issue in visible) DrawIssue(issue);
        }

        private void DrawIssue(Issue issue)
        {
            var k = KnowledgeBase.Get(issue.RuleId);
            var key = Key(issue);
            if (!_expanded.TryGetValue(key, out var open)) open = issue.Severity <= Severity.Broken; // 重要なものは開いておく

            using (var vs = new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
            {
                // 左端の重要度色帯
                if (Event.current.type == EventType.Repaint && ColorUtility.TryParseHtmlString(UiText.SeverityColorHex(issue.Severity), out var sc))
                    EditorGUI.DrawRect(new Rect(vs.rect.x + 1, vs.rect.y + 1, 4, vs.rect.height - 2), sc);

                using (new EditorGUILayout.HorizontalScope())
                {
                    GUILayout.Space(4);
                    var arrow = open ? "▼" : "▶";
                    if (GUILayout.Button($"{arrow} {UiText.SeverityIconRich(issue.Severity)} {UiText.Wrap(issue.TitleOverride ?? k.Title)}", _issueHeader))
                        _expanded[key] = open = !open;
                }

                if (!open) return;

                EditorGUILayout.Space(2);
                if (!string.IsNullOrEmpty(k.Symptom)) Field(L10n.T("症状", "Symptom"), k.Symptom);
                // 「意図的な場合もある」注記は、断定に見えないよう症状の直後に見せる
                if (!string.IsNullOrEmpty(k.MaybeIntentional)) GUILayout.Label(UiText.Wrap("※ " + k.MaybeIntentional), _small);
                if (!string.IsNullOrEmpty(issue.Detail)) Field(L10n.T("検出", "Detected"), issue.Detail);
                if (_detailed && !string.IsNullOrEmpty(k.Cause)) Field(L10n.T("原因", "Cause"), k.Cause);
                if (_detailed && !string.IsNullOrEmpty(k.Impact)) Field(L10n.T("影響", "Impact"), k.Impact);
                if (k.FixSteps != null && k.FixSteps.Length > 0)
                {
                    GUILayout.Label(L10n.T("対応", "What to do"), _sectionLabel);
                    for (int i = 0; i < k.FixSteps.Length; i++) GUILayout.Label(UiText.Wrap($"  {i + 1}. {k.FixSteps[i]}"), _wrap);
                }
                if (!string.IsNullOrEmpty(issue.ExtraHint)) Field(L10n.T("ヒント", "Tip"), issue.ExtraHint);

                EditorGUILayout.Space(2);
                using (new EditorGUILayout.HorizontalScope())
                {
                    if (issue.Targets.Count > 0 && issue.Targets.Any(t => t != null))
                    {
                        if (GUILayout.Button(L10n.IsEnglish ? $"Select Targets ({issue.Targets.Count(t => t != null)})" : $"対象を選択 ({issue.Targets.Count(t => t != null)})", GUILayout.Width(120)))
                        {
                            var objs = issue.Targets.Where(t => t != null).ToArray();
                            Selection.objects = objs;
                            EditorGUIUtility.PingObject(objs[0]);
                        }
                    }
                    if (_detailed && !string.IsNullOrEmpty(k.DocUrl))
                    {
                        if (GUILayout.Button(L10n.T("参考ページ", "Reference"), GUILayout.Width(80))) Application.OpenURL(k.DocUrl);
                    }
                    if (FixHost.Available)
                    {
                        // 製品版: 「修正する」等はホスト側が描画(この環境では直せない項目は報告ボタンにフォールバック)
                        var fctx = FixContext();
                        if (FixHost.Current.CanFix(fctx, issue)) FixHost.Current.DrawIssueActions(fctx, issue);
                        else if (GUILayout.Button(new GUIContent(L10n.T("この項目を報告", "Report Item"), L10n.T("誤検出や分かりにくい説明があれば、要望フォーム(匿名可)で教えてください", "Report a false positive or unclear explanation through the optional anonymous feedback form.")), EditorStyles.miniButton, GUILayout.Width(110)))
                            Application.OpenURL(FeedbackLinks.ForIssue(issue.RuleId, _report?.ToolVersion));
                    }
                    else if (issue.AutoFixable)
                    {
                        // 製品版(Avatar Refinery Pro)ならワンクリックで直せる項目。押すと商品ページが開く
                        if (GUILayout.Button(new GUIContent(L10n.T("製品版で自動修正", "Fix with Pro"), L10n.T("この項目は製品版(Avatar Refinery Pro)でワンクリック修正できます。実行前のプレビュー・自動バックアップ・「すべて元に戻す」付き。押すと商品ページが開きます", "This item has a supported Pro fix with a preview, automatic backup, and rollback. Click to open the product page.")), GUILayout.Width(120)))
                            Application.OpenURL(FeedbackLinks.ProductPage);
                    }
                    else
                    {
                        if (GUILayout.Button(new GUIContent(L10n.T("この項目を報告", "Report Item"), L10n.T("誤検出や分かりにくい説明があれば、要望フォーム(匿名可)で教えてください", "Report a false positive or unclear explanation through the optional anonymous feedback form.")), EditorStyles.miniButton, GUILayout.Width(100)))
                            Application.OpenURL(FeedbackLinks.ForIssue(issue.RuleId, _report?.ToolVersion));
                    }
                    GUILayout.FlexibleSpace();
                    if (GUILayout.Button(L10n.T("無視", "Ignore"), EditorStyles.miniButton, GUILayout.Width(L10n.IsEnglish ? 52 : 44))) _ignored.Add(key);
                }
                // 製品版: 修正プレビュー等(複数行)
                if (FixHost.Available) FixHost.Current.DrawIssueBody(FixContext(), issue);
            }
            EditorGUILayout.Space(2);
        }

        private void Field(string label, string text)
        {
            GUILayout.Label(label, _sectionLabel);
            GUILayout.Label(UiText.Wrap(text), _wrap);
        }

        private bool _ndmfMessagesOpen;

        private void DrawFooter()
        {
            var r = _report;
            EditorGUILayout.Space(4);
            if (r.NdmfMessages.Count > 0)
            {
                _ndmfMessagesOpen = EditorGUILayout.Foldout(_ndmfMessagesOpen, L10n.IsEnglish ? $"Messages from NDMF tools ({r.NdmfMessages.Count})" : $"非破壊ツール(NDMF)からのメッセージ ({r.NdmfMessages.Count})", true);
                if (_ndmfMessagesOpen)
                {
                    using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
                    {
                        foreach (var m in r.NdmfMessages)
                            GUILayout.Label(UiText.Wrap($"[{m.Severity}] {m.Message}"), _wrap);
                    }
                }
                EditorGUILayout.Space(2);
            }
            if (FixHost.Available) FixHost.Current.DrawFooter(FixContext());
            GUILayout.Label(UiText.Wrap(L10n.IsEnglish
                ? $"Check {_lastRunSeconds:0.0}s (build {r.BakeMs} ms / stats {r.StatsMs} ms / rules {r.RulesMs} ms) — Unity {r.UnityVersion} / SDK {r.SdkVersion} — Avatar Refinery {r.ToolVersion}"
                : $"診断 {_lastRunSeconds:0.0} 秒(ベイク {r.BakeMs} ms / 統計 {r.StatsMs} ms / ルール {r.RulesMs} ms) — Unity {r.UnityVersion} / SDK {r.SdkVersion} — Avatar Refinery {r.ToolVersion}"), _small);
            GUILayout.Label(UiText.Wrap((FixHost.Available ? FixHost.Current.FooterNote : null) ?? L10n.T("この診断は読み取り専用です。アバター・アセットは変更していません。", "This preflight check is read-only. It did not change the avatar or any assets.")), _small);
            using (new EditorGUILayout.HorizontalScope())
            {
                if (GUILayout.Button(new GUIContent(L10n.T("不具合・要望を送る", "Send Feedback"), L10n.T("フォーム(匿名可・1分)がブラウザで開きます", "Open the optional anonymous feedback form in your browser.")), EditorStyles.miniButton, GUILayout.Width(120)))
                    Application.OpenURL(FeedbackLinks.General());
                if (GUILayout.Button(new GUIContent(L10n.T("使い方・更新情報", "Docs & Updates"), L10n.T("配布ページを開きます", "Open the installation guide and update page.")), EditorStyles.miniButton, GUILayout.Width(110)))
                    Application.OpenURL(FeedbackLinks.DistributionPage);
                GUILayout.Label(UiText.Wrap("X DM: @runsol_ai"), _small);
            }
            if (r.RuleErrors.Count > 0)
                EditorGUILayout.HelpBox(UiText.Wrap(L10n.T("診断ツール内部のエラー(不具合報告にご協力ください):\n", "Internal preflight-check errors (please include these in a bug report):\n") + string.Join("\n", r.RuleErrors)), MessageType.Warning);
        }

        // ------------------------------------------------------------------ actions

        private void RunDiagnosis() => RunDiagnosis(keepPrevious: false);

        /// <summary>診断を実行。keepPrevious=true なら直前の結果を Before/After 用に保持し、展開/無視の状態も維持する(修正後の再診断)。</summary>
        private void RunDiagnosis(bool keepPrevious)
        {
            _lastError = null;
            if (_avatar == null) return;
            try
            {
                var sw = System.Diagnostics.Stopwatch.StartNew();
                EditorUtility.DisplayProgressBar("Avatar Refinery", keepPrevious ? L10n.T("再診断中…", "Checking again...") : L10n.T("診断中…(アバターは変更しません)", "Checking... (the avatar will not be changed)"), 0.3f);
                var previous = _report;
                _report = DiagnosisRunner.Run(_avatar);
                _previousReport = keepPrevious ? previous : null;
                _lastRunSeconds = sw.Elapsed.TotalSeconds;
                if (!keepPrevious) { _expanded.Clear(); _ignored.Clear(); }
            }
            catch (Exception e)
            {
                _lastError = e.ToString();
                _report = null;
                Debug.LogException(e);
            }
            finally { EditorUtility.ClearProgressBar(); }
            Repaint();
        }

        private FixUiContext FixContext()
        {
            return new FixUiContext
            {
                Window = this,
                Avatar = _avatar,
                Report = _report,
                Previous = _previousReport,
                Detailed = _detailed,
                Rediagnose = () => RunDiagnosis(keepPrevious: true),
                Repaint = Repaint,
                IsIgnored = i => _ignored.Contains(Key(i)),
                IssueKey = Key,
            };
        }

        private void SaveReport()
        {
            if (_report == null) return;
            var dir = Path.Combine(Path.GetDirectoryName(Application.dataPath) ?? ".", "AvatarRefinery_Reports");
            Directory.CreateDirectory(dir);
            var def = $"{San(_report.AvatarName)}_{_report.Timestamp:yyyyMMdd_HHmm}.md";
            var path = EditorUtility.SaveFilePanel(L10n.T("診断レポートを保存", "Save Preflight Report"), dir, def, "md");
            if (string.IsNullOrEmpty(path)) return;
            File.WriteAllText(path, MarkdownReport.Build(_report, detailed: true), new UTF8Encoding(false));
            EditorUtility.RevealInFinder(path);
        }

        // ------------------------------------------------------------------ helpers

        private static string Key(Issue i) => i.RuleId + "|" + (i.TitleOverride ?? "") + "|" + (i.Detail ?? "").GetHashCode();
        private static string San(string s) { foreach (var c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_'); return s; }
        private static string ShortLabel(Severity s)
        {
            switch (s)
            {
                case Severity.Blocker: return L10n.T("止まる", "Blocker");
                case Severity.Broken: return L10n.T("壊れる", "Broken");
                case Severity.Performance: return L10n.T("重い", "Performance");
                default: return L10n.T("情報", "Info");
            }
        }
        public static Color RankColor(string rank)
        {
            switch (rank)
            {
                case "Excellent": return new Color(0.55f, 1f, 0.6f);
                case "Good": return new Color(0.7f, 1f, 0.6f);
                case "Medium": return new Color(1f, 0.95f, 0.5f);
                case "Poor": return new Color(1f, 0.75f, 0.45f);
                case "VeryPoor": return new Color(1f, 0.5f, 0.5f);
                default: return Color.white;
            }
        }

        /// <summary>選択オブジェクトからアバターのルートを推定(Descriptor → Humanoid Animator → 最上位)</summary>
        public static GameObject FindAvatarRoot(GameObject go)
        {
            if (go == null) return null;
            var t = go.transform;
            Transform withDescriptor = null, withAnimator = null;
            while (t != null)
            {
                if (withDescriptor == null && t.GetComponents<Component>().Any(c => c != null && c.GetType().Name == "VRCAvatarDescriptor")) withDescriptor = t;
                var a = t.GetComponent<Animator>();
                if (a != null && a.isHuman) withAnimator = t;
                t = t.parent;
            }
            return (withDescriptor ?? withAnimator ?? go.transform.root).gameObject;
        }

        private static List<GameObject> FindAvatarCandidates()
        {
            var list = new List<GameObject>();
            foreach (var a in Object.FindObjectsOfType<Animator>(true))
            {
                if (a == null) continue;
                bool hasDescriptor = a.GetComponents<Component>().Any(c => c != null && c.GetType().Name == "VRCAvatarDescriptor");
                if (hasDescriptor || a.isHuman) list.Add(a.gameObject);
            }
            return list.Distinct().OrderBy(g => g.name).ToList();
        }
    }
}
