using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;

namespace ServiceSim.EditorTools
{
    /// <summary>
    /// サイトの JSON から建てた店（JsonStoreLayout）を確かめるメニュー。
    ///
    /// することは「建てて、正しく建ったかを確かめる」まで。回さない（段階1）。
    /// 建てるところと画面の撮影は ServiceSim/Build Store をそのまま呼ぶ（作り直さない）。
    /// ここで足すのは、JSON と突き合わせる確かめだけ:
    ///   1. 卓の数・席の数・細い通路の本数と幅（summary.json とも突き合わせる）
    ///   2. 止まる位置・座る位置・席に着く点・場所の点が NavMesh に乗るか
    ///   3. 受け渡し口の前から各卓の止まる位置までの道のりと、JSON の dist の照合（CSV）
    ///   4. 席に入れない卓（席に着く点へ、入口から歩いて行けるか）
    /// 書き出し先は ExperimentPlan.screenshotDir（Build Store と同じ）。
    /// </summary>
    public static class ServiceSimJsonStore
    {
        const string P1Path = "Assets/ServiceSim/Store/StoreLayout_p1_efficient_corner.asset";
        const string P5Path = "Assets/ServiceSim/Store/StoreLayout_p5_crammed.asset";

        /// <summary>道のりを測るときに、点から NavMesh を探す半径（m）</summary>
        const float SampleRadius = 0.5f;

        /// <summary>点が NavMesh に乗っているとみなす距離（m）</summary>
        const float OnMeshRadius = 0.2f;

        /// <summary>差がこれを超えた卓を報告に並べる</summary>
        const float BigDiffPct = 30f;

        [MenuItem("ServiceSim/JSON Store/③ p1_efficient_corner を建てて確かめる", false, 30)]
        public static void CheckP1() { Run(P1Path); }

        [MenuItem("ServiceSim/JSON Store/⑦ p5_crammed を建てて確かめる", false, 31)]
        public static void CheckP5() { Run(P5Path); }

        [MenuItem("ServiceSim/JSON Store/③⑦ を続けて確かめる", false, 32)]
        public static void CheckBoth() { if (Run(P1Path)) Run(P5Path); }

        const string SimParamsPath = "Assets/ServiceSim/Experiment/SimParams.asset";

        /// <summary>
        /// サイトの試算の値（serving-robot-params.json）を読んで、中身と版の番号を確かめる（v5）。
        /// 回さない。読めるかどうかと、店の形の版と合っているかだけを見る。
        /// </summary>
        [MenuItem("ServiceSim/JSON Store/試算の値を読んで確かめる（v5）", false, 33)]
        public static void CheckSimParams()
        {
            SimParams sp = AssetDatabase.LoadAssetAtPath<SimParams>(SimParamsPath);
            if (sp == null)
            {
                Debug.LogError("[ServiceSim] SimParams が見つかりません: " + SimParamsPath);
                return;
            }

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("# 試算の値の読み込み（v5・回していない）");
            sb.AppendLine("作った日時: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
            sb.AppendLine();

            // 店の形の版と突き合わせる（③⑦の両方）
            string[] layouts = { P1Path, P5Path };
            for (int i = 0; i < layouts.Length; i++)
            {
                JsonStoreLayout layout = AssetDatabase.LoadAssetAtPath<JsonStoreLayout>(layouts[i]);
                if (layout == null || layout.layoutJson == null) continue;
                string err;
                JsonValue root = JsonValue.Parse(layout.layoutJson.text, out err);
                string gen = root != null ? root["meta"]["generation"].S("") : "（読めない）";
                bool ok = sp.Load(gen);
                sb.AppendLine("## " + layout.layoutFileName + " と突き合わせ → " +
                              (ok ? "合っている" : "合っていない"));
                sb.AppendLine("  店の形の版 (meta.generation): " + gen);
                sb.AppendLine(sp.Report());
                sb.AppendLine();
            }

            // 読む値（site_rule）を並べて、何を使うかを見えるようにする
            sp.Load(null);
            string[] keys = { "guide_s", "pay_s", "call_s", "staff_place_s", "robot_handoff_s",
                              "clear_s", "wipe_s", "load_s", "guest_load_s", "rescue_s",
                              "staff_speed", "cold_min", "dish_extra_m", "max_extra_min" };
            sb.AppendLine("## Unity が使う値（role=site_rule）");
            for (int i = 0; i < keys.Length; i++)
                sb.AppendLine("  " + keys[i] + " = " + F(sp.SiteRule(keys[i], float.NaN)));
            float[] fc = sp.SiteRuleArray("fragile_cook", null);
            sb.AppendLine("  fragile_cook = " + (fc == null ? "（無い）"
                : F(fc[0]) + " / " + F(fc[1]) + " / " + F(fc[2])));

            sb.AppendLine();
            sb.AppendLine("## 店ごとの値（stores）");
            string[] stores = { "fam_p1", "fam_p5" };
            for (int i = 0; i < stores.Length; i++)
            {
                string k = stores[i];
                sb.AppendLine("  " + k + ": 卓 " + F(sp.StoreValue(k, "tables", 0f)) +
                              " / peak_min " + F(sp.StoreValue(k, "peak_min", 0f)) +
                              " / 来客（真ん中）" + sp.StoreMiddle(k, "arrivals", 0) + " 組毎時" +
                              " / スタッフ（真ん中）" + sp.StoreMiddle(k, "staff", 0) + " 人" +
                              " / eat_min " + sp.StoreTriangle(k, "eat_min", Vector3.zero) +
                              " / call_rate " + F(sp.StoreValue(k, "call_rate", 0f)) +
                              " / bus_trips " + F(sp.StoreValue(k, "bus_trips", 0f)) +
                              " / fragile " + F(sp.StoreValue(k, "fragile", 0f)));
            }

            sb.AppendLine();
            sb.AppendLine("## 出す数字の名前と意味（meta.metrics）");
            List<string> mk = sp.MetricKeys();
            for (int i = 0; i < mk.Count; i++)
                sb.AppendLine("  " + mk[i] + ": " + sp.MetricNote(mk[i]));

            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(ServiceSimSetup.ExperimentPlanPath);
            string outDir = plan != null ? plan.screenshotDir : null;
            if (!string.IsNullOrEmpty(outDir))
            {
                try
                {
                    Directory.CreateDirectory(outDir);
                    File.WriteAllText(Path.Combine(outDir,
                        DateTime.Now.ToString("yyyyMMdd") + "_sim_params_check.txt"), sb.ToString());
                }
                catch (Exception e) { Debug.LogError("[ServiceSim] 書けません: " + e.Message); }
            }
            Debug.Log("[ServiceSim] 試算の値の読み込み\n" + sb.ToString());
        }

        static bool Run(string assetPath)
        {
            JsonStoreLayout layout = AssetDatabase.LoadAssetAtPath<JsonStoreLayout>(assetPath);
            if (layout == null)
            {
                Debug.LogError("[ServiceSim] 店の形が見つかりません: " + assetPath);
                return false;
            }

            if (EditorSceneManager.GetActiveScene().path != ServiceSimSetup.ScenePath)
            {
                if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return false;
                EditorSceneManager.OpenScene(ServiceSimSetup.ScenePath);
            }

            StoreBuilder builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
            NavMeshBaker baker = UnityEngine.Object.FindObjectOfType<NavMeshBaker>();
            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(ServiceSimSetup.ExperimentPlanPath);
            if (builder == null || baker == null || plan == null)
            {
                Debug.LogError("[ServiceSim] StoreBuilder / NavMeshBaker / ExperimentPlan が見つかりません。"
                               + "先に ServiceSim/Setup を実行してください");
                return false;
            }

            // 建てるのは Build Store にまかせる（組み立て・ベイク・既存の確かめ・撮影）
            builder.layout = layout;
            baker.layout = layout;
            ServiceSimMenu.BuildStore();

            builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
            baker = UnityEngine.Object.FindObjectOfType<NavMeshBaker>();
            if (builder == null || baker == null || builder.Plan == null)
            {
                Debug.LogError("[ServiceSim] 建てられませんでした。Console の [ServiceSim] の行を見てください");
                return false;
            }
            if (builder.layout != layout)
            {
                Debug.LogError("[ServiceSim] Build Store のあとで店の形が " + builder.layout +
                               " に戻っています。確かめを中止しました");
                return false;
            }

            string err;
            JsonValue root = JsonValue.Parse(layout.layoutJson != null ? layout.layoutJson.text : "", out err);
            if (root == null)
            {
                Debug.LogError("[ServiceSim] JSON を読めません: " + err);
                return false;
            }

            string outDir = plan.screenshotDir;
            try { Directory.CreateDirectory(outDir); }
            catch (Exception e)
            {
                Debug.LogError("[ServiceSim] 書き出し先を作れません: " + outDir + "\n" + e.Message);
                return false;
            }

            string stem = DateTime.Now.ToString("yyyyMMdd") + "_" + layout.layoutFileName;
            string csvPath = Path.Combine(outDir, stem + "_table_dist.csv");
            string txtPath = Path.Combine(outDir, stem + "_json_check.txt");

            NavMeshQueryFilter people = baker.FilterFor(baker.PeopleAgentTypeId);
            // v5b: 道順はサイトの routes を使う。NavMesh は区画を足さない段階1 と同じ。
            // 点から点の区間だけを NavMesh でつないで確かめる
            RobotSpec robotSpec = AssetDatabase.LoadAssetAtPath<RobotSpec>(ServiceSimSetup.RobotSpecPath);
            NavMeshQueryFilter robot = baker.FilterFor(baker.RobotAgentTypeId);
            _snapRadius = robotSpec != null ? robotSpec.Radius * 4f : SampleRadius;

            StringBuilder report = new StringBuilder();
            report.AppendLine("# JSON から建てた店の確かめ（段階1・回していない）");
            report.AppendLine("店の形: " + layout.name + " / " + layout.StoreKindName);
            report.AppendLine("作った日時: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
            report.AppendLine();
            report.AppendLine(layout.FrontFitReport());
            report.AppendLine();

            report.AppendLine(CountReport(builder, root, layout));
            report.AppendLine();
            report.AppendLine(OnMeshReport(builder, robot, people));
            report.AppendLine();
            report.AppendLine(DistReport(builder, root, robot, csvPath));
            report.AppendLine();
            report.AppendLine(SeatReachReport(builder, people));

            try { File.WriteAllText(txtPath, report.ToString()); }
            catch (Exception e) { Debug.LogError("[ServiceSim] 報告を書けません: " + e.Message); }

            Debug.Log("[ServiceSim] JSON Store の確かめ おわり（" + layout.layoutFileName + "）\n"
                      + report.ToString()
                      + "\n  書き出し: " + txtPath + "\n            " + csvPath);
            return true;
        }

        // ---------------- 1. 数 ----------------

        static string CountReport(StoreBuilder builder, JsonValue root, JsonStoreLayout layout)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("## 1. 数");

            int tables = builder.Tables.Count;
            int seats = 0;
            for (int i = 0; i < builder.Tables.Count; i++) seats += builder.Tables[i].seatCount;

            int jsonTables = root["tables"].Count;
            int jsonSeats = 0;
            for (int i = 0; i < jsonTables; i++) jsonSeats += root["tables"][i]["seats"].I(0);

            int sumTables = -1, sumSeats = -1;
            string err;
            JsonValue sum = layout.summaryJson != null
                ? JsonValue.Parse(layout.summaryJson.text, out err) : null;
            if (sum != null)
                for (int i = 0; i < sum.Count; i++)
                    if (sum[i]["file"].S("") == layout.layoutFileName)
                    {
                        sumTables = sum[i]["tables"].I(-1);
                        sumSeats = sum[i]["seats"].I(-1);
                    }

            sb.AppendLine("  卓: Unity " + tables + " / JSON " + jsonTables +
                          " / summary.json " + sumTables + "  " + Same(tables, jsonTables, sumTables));
            sb.AppendLine("  席: Unity " + seats + " / JSON " + jsonSeats +
                          " / summary.json " + sumSeats + "  " + Same(seats, jsonSeats, sumSeats));

            int narrow = 0;
            StringBuilder widths = new StringBuilder();
            for (int i = 0; i < builder.Aisles.Count; i++)
            {
                StoreBuilder.AisleInfo a = builder.Aisles[i];
                if (!a.narrow) continue;
                narrow++;
                if (widths.Length > 0) widths.Append(" / ");
                widths.Append(a.name).Append(" ").Append(F(a.width)).Append(" m");
            }
            sb.AppendLine("  細い通路: " + narrow + " 本（" + widths + "）");
            sb.AppendLine("  組み立てた幅 (plan.aisleWidth): " + F(builder.BuiltAisleWidth) + " m");
            sb.AppendLine("  外寸: " + F(builder.OuterSize.x) + " × " + F(builder.OuterSize.y) + " m" +
                          " / JSON outer " + F(root["outer"]["w"].F(0f)) + " × " + F(root["outer"]["d"].F(0f)) + " m");
            return sb.ToString().TrimEnd();
        }

        static string Same(int a, int b, int c)
        {
            bool ok = a == b && (c < 0 || a == c);
            return ok ? "一致" : "← ちがう";
        }

        // ---------------- 2. 点が NavMesh に乗るか ----------------

        static string OnMeshReport(StoreBuilder builder, NavMeshQueryFilter robot, NavMeshQueryFilter people)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("## 2. 点が NavMesh の上に乗るか（乗らないものだけ並べる）");

            List<string> offDelivery = new List<string>();
            List<string> offSpot = new List<string>();
            List<string> offSeat = new List<string>();
            List<string> offPlace = new List<string>();

            StorePlan plan = builder.Plan;
            for (int i = 0; i < plan.seats.Count; i++)
            {
                PlanSeat s = plan.seats[i];
                string tid = "T" + s.tableId.ToString("00");
                if (!OnMesh(s.deliveryPoint, robot)) offDelivery.Add(tid + " " + P(s.deliveryPoint));
                for (int k = 0; k < s.spots.Count; k++)
                    if (!OnMesh(s.spots[k].point, people))
                        offSpot.Add(tid + " " + s.spots[k].name + " " + P(s.spots[k].point));
                for (int k = 0; k < s.seatPoints.Count; k++)
                    if (!OnMesh(s.seatPoints[k], people))
                        offSeat.Add(tid + "#" + (k + 1) + " " + P(s.seatPoints[k]));
            }
            for (int i = 0; i < plan.places.Count; i++)
            {
                PlanPlace p = plan.places[i];
                if (p.solid) continue;                      // 部屋そのものの点は通れない箱の中
                bool forRobot = p.kind == PlaceKind.FoodPass || p.kind == PlaceKind.DishReturn ||
                                p.kind == PlaceKind.RobotStandby;
                if (!OnMesh(p.point, forRobot ? robot : people))
                    offPlace.Add(p.name + " " + P(p.point) + (forRobot ? "（ロボット用）" : "（人用）"));
            }

            sb.AppendLine("  止まる位置 (deliveryPoint, ロボット用): 乗らない " + offDelivery.Count +
                          " / " + plan.seats.Count);
            Lines(sb, offDelivery);
            sb.AppendLine("  席に着く点 (spots, 人用): 乗らない " + offSpot.Count);
            Lines(sb, offSpot);
            sb.AppendLine("  座る位置 (seatPoints, 人用): 乗らない " + offSeat.Count +
                          "  ※ 座面やベンチの上の点は NavMesh に乗らないのがふつう（見た目に使う点）");
            Lines(sb, offSeat, 8);
            sb.AppendLine("  場所の点 (places): 乗らない " + offPlace.Count);
            Lines(sb, offPlace);
            return sb.ToString().TrimEnd();
        }

        static void Lines(StringBuilder sb, List<string> items, int max = 40)
        {
            for (int i = 0; i < items.Count && i < max; i++) sb.AppendLine("    - " + items[i]);
            if (items.Count > max) sb.AppendLine("    - ほか " + (items.Count - max) + " 件");
        }

        static bool OnMesh(Vector3 p, NavMeshQueryFilter filter)
        {
            NavMeshHit hit;
            return NavMesh.SamplePosition(p, out hit, OnMeshRadius, filter);
        }

        // ---------------- 3. 道のりの照合 ----------------

        /// <summary>区間の NavMesh の道のりが、まっすぐの長さのこの倍を超えたら並べる</summary>
        const float SegmentDetourLimit = 1.05f;

        static float _snapRadius = SampleRadius;

        /// <summary>
        /// 3. 道順の照合（v5b）。
        /// サイトが決めた道順（曲がり角の点）を、Unity の NavMesh でつなぎ直して確かめる。
        ///   ・道順の長さ（点を結んだ長さ）と JSON の dist
        ///   ・点から点の区間が NavMesh で完全につながるか
        ///   ・区間の NavMesh の道のりが、まっすぐの長さの 1.05 倍を超えないか
        ///   ・use（normal / narrow）＝ via_narrow
        /// </summary>
        static string DistReport(StoreBuilder builder, JsonValue root, NavMeshQueryFilter robot, string csvPath)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("## 3. 道順の照合（受け渡し口の前 → 止まる位置。サイトの routes をたどる）");

            RouteBook book = builder.Routes;
            if (book == null)
            {
                sb.AppendLine("  routes がありません（v5b の版の JSON を写してください）");
                return sb.ToString().TrimEnd();
            }
            sb.AppendLine("  道順: " + book.Summary());
            sb.AppendLine("  止まる位置を面へ寄せる半径: " + F(_snapRadius) + " m");

            Dictionary<int, JsonValue> byId = new Dictionary<int, JsonValue>();
            JsonValue tables = root["tables"];
            for (int i = 0; i < tables.Count; i++)
            {
                int id = NumberFromId(tables[i]["id"].S(""), i + 1);
                if (!byId.ContainsKey(id)) byId[id] = tables[i];
            }

            StringBuilder csv = new StringBuilder();
            csv.AppendLine("table,category,aisle,use,json_dist_m,route_len_m,unity_path_m," +
                           "diff_m,diff_pct,via_narrow,segments,bad_segments,status");

            List<string> brokenSeg = new List<string>();
            List<string> detourSeg = new List<string>();
            List<string> big = new List<string>();
            List<float> diffs = new List<float>();
            int narrowTables = 0, okTables = 0;
            float sumAbs = 0f, maxAbs = 0f; string maxAt = "";

            StorePlan plan = builder.Plan;
            for (int i = 0; i < plan.seats.Count; i++)
            {
                PlanSeat s = plan.seats[i];
                string tid = "T" + s.tableId.ToString("00");
                JsonValue t;
                string category = "", aisle = "";
                float jsonDist = -1f;
                if (byId.TryGetValue(s.tableId, out t))
                {
                    jsonDist = t["dist"].F(-1f);
                    category = t["category"].S("");
                    aisle = t["aisle"].S("");
                }

                RouteBook.Route r = book.FromPass(s.tableId);
                if (r == null)
                {
                    brokenSeg.Add(tid + " 道順が無い");
                    csv.AppendLine(tid + "," + category + "," + aisle + ",,,,,,,,,,道順が無い");
                    continue;
                }
                bool narrow = r.IsNarrow;
                if (narrow) narrowTables++;

                float routeLen = r.PolylineLength;
                float unityLen = 0f;
                int bad = 0;
                bool allConnected = true;
                for (int k = 1; k < r.points.Count; k++)
                {
                    Vector3 a = r.points[k - 1], b = r.points[k];
                    string st;
                    float len = PathLen(a, b, robot, out st);
                    float straight = Vector3.Distance(a, b);
                    if (len < 0f)
                    {
                        allConnected = false; bad++;
                        brokenSeg.Add(tid + " #" + k + " " + P(a) + "→" + P(b) + " " + st);
                        continue;
                    }
                    unityLen += len;
                    if (straight > 0.05f && len > straight * SegmentDetourLimit)
                    {
                        bad++;
                        detourSeg.Add(tid + " #" + k + " まっすぐ " + F(straight) + " m → NavMesh " +
                                      F(len) + " m（" + F(len / straight) + " 倍）");
                    }
                }

                string status = allConnected ? "ok" : "つながらない区間あり";
                if (allConnected)
                {
                    okTables++;
                    float diff = unityLen - jsonDist;
                    float pct = jsonDist > 0.0005f ? diff / jsonDist * 100f : 0f;
                    diffs.Add(diff);
                    sumAbs += Mathf.Abs(diff);
                    if (Mathf.Abs(diff) > maxAbs) { maxAbs = Mathf.Abs(diff); maxAt = tid; }
                    if (Mathf.Abs(pct) > BigDiffPct)
                        big.Add(tid + " JSON " + F(jsonDist) + " m / Unity " + F(unityLen) + " m（" + F(pct) + "%）");
                    csv.AppendLine(tid + "," + category + "," + aisle + "," + r.use + "," +
                                   F(jsonDist) + "," + F(routeLen) + "," + F(unityLen) + "," +
                                   F(diff) + "," + F(pct) + "," + (narrow ? "1" : "0") + "," +
                                   (r.points.Count - 1) + "," + bad + ",ok");
                }
                else
                {
                    csv.AppendLine(tid + "," + category + "," + aisle + "," + r.use + "," +
                                   F(jsonDist) + "," + F(routeLen) + ",," + ",," +
                                   (narrow ? "1" : "0") + "," + (r.points.Count - 1) + "," + bad + "," + status);
                }
            }

            try { File.WriteAllText(csvPath, csv.ToString()); }
            catch (Exception e) { sb.AppendLine("  CSV を書けません: " + e.Message); }

            sb.AppendLine("  すべての区間がつながった卓: " + okTables + " / " + plan.seats.Count);
            if (diffs.Count > 0)
            {
                float mean = 0f;
                for (int i = 0; i < diffs.Count; i++) mean += diffs[i];
                mean /= diffs.Count;
                sb.AppendLine("  差（Unity − JSON）の平均: " + F(mean) + " m");
                sb.AppendLine("  差の大きさの平均: " + F(sumAbs / diffs.Count) + " m / 最大: " +
                              F(maxAbs) + " m（" + maxAt + "）");
            }
            sb.AppendLine("  差が " + F(BigDiffPct) + "% を超えた卓: " + big.Count);
            Lines(sb, big);
            sb.AppendLine("  via_narrow（use=narrow）の卓: " + narrowTables + " / " + plan.seats.Count);
            sb.AppendLine("  つながらない区間: " + brokenSeg.Count);
            Lines(sb, brokenSeg);
            sb.AppendLine("  まっすぐの " + F(SegmentDetourLimit) + " 倍を超えた区間: " + detourSeg.Count);
            Lines(sb, detourSeg);
            sb.AppendLine("  CSV: " + csvPath);
            return sb.ToString().TrimEnd();
        }

        /// <summary>その絞り込みで道のりを測る。完全につながらなければ -1</summary>
        static float PathLen(Vector3 from, Vector3 to, NavMeshQueryFilter filter, out string status)
        {
            NavMeshHit hitFrom, hitTo;
            if (!NavMesh.SamplePosition(from, out hitFrom, _snapRadius, filter))
            { status = "出発点が面の上にない"; return -1f; }
            if (!NavMesh.SamplePosition(to, out hitTo, _snapRadius, filter))
            { status = "行き先が面の上にない"; return -1f; }

            NavMeshPath path = new NavMeshPath();
            if (!NavMesh.CalculatePath(hitFrom.position, hitTo.position, filter, path))
            { status = "経路を引けない"; return -1f; }
            if (path.status != NavMeshPathStatus.PathComplete)
            {
                status = path.status == NavMeshPathStatus.PathPartial ? "途中まで" : "無効";
                return -1f;
            }

            float len = 0f;
            Vector3[] c = path.corners;
            for (int i = 1; i < c.Length; i++) len += Vector3.Distance(c[i - 1], c[i]);
            status = "ok";
            return len;
        }

        // ---------------- 4. 席に入れない卓 ----------------

        static string SeatReachReport(StoreBuilder builder, NavMeshQueryFilter people)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("## 4. 席に入れない卓（入口から、席に着く点まで歩けるか）");

            Vector3 from = builder.PointOf(PlaceKind.Entrance);
            sb.AppendLine("  出発点 (Entrance): " + P(from));

            List<string> bad = new List<string>();
            int checkedSpots = 0;
            StorePlan plan = builder.Plan;
            for (int i = 0; i < plan.seats.Count; i++)
            {
                PlanSeat s = plan.seats[i];
                if (s.spots.Count == 0)
                {
                    bad.Add("T" + s.tableId.ToString("00") + " 席に着く点が無い");
                    continue;
                }
                bool any = false;
                for (int k = 0; k < s.spots.Count; k++)
                {
                    checkedSpots++;
                    string status;
                    float len = NavMeshBaker.PathLength(from, s.spots[k].point, SampleRadius, people, out status);
                    if (len >= 0f) { any = true; break; }
                }
                if (!any) bad.Add("T" + s.tableId.ToString("00") + " どの席に着く点へも歩けない");
            }
            sb.AppendLine("  確かめた点: " + checkedSpots + " / 卓 " + plan.seats.Count);
            sb.AppendLine("  入れない卓: " + bad.Count);
            Lines(sb, bad);
            return sb.ToString().TrimEnd();
        }

        // ---------------- 小さな道具 ----------------

        static int NumberFromId(string id, int fallback)
        {
            if (string.IsNullOrEmpty(id)) return fallback;
            int n = 0; bool any = false;
            for (int i = 0; i < id.Length; i++)
            {
                char c = id[i];
                if (c < '0' || c > '9') continue;
                n = n * 10 + (c - '0');
                any = true;
            }
            return any ? n : fallback;
        }

        static string P(Vector3 v)
        {
            return "(" + F(v.x) + ", " + F(v.z) + ")";
        }

        static string F(float v)
        {
            return v.ToString("0.##", CultureInfo.InvariantCulture);
        }
    }
}
