using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
// System.Diagnostics をまるごと using すると Debug が UnityEngine.Debug と
// ぶつかるので、要る型だけ別名で入れる
using Process = System.Diagnostics.Process;

namespace ServiceSim.EditorTools
{
    /// <summary>
    /// 段階B（依頼 v7。2026-10-23）：18回まとめて回して、台帳に1行ずつ残す。
    ///
    /// 回すもの:
    ///   サイトの条件 6 つ（③ fam_p1・⑦ fam_p5 × ロボット 0・1・2 台）× 種 0・1・2 ＝ 18 回。
    ///   決まりは固定版のまま。条件ごとに .asset へ入れた値は、ExperimentRunner と SiteValues が
    ///   回し終わりに必ず元へ戻す（途中で止めても戻す）。
    ///
    /// 回す順番:
    ///   ③ロボット2台 種0 → ⑦ロボット2台 種0 → 残りを条件・種の順。
    ///   **最初の2回で失敗したら、残りは回さずに止める**。
    ///
    /// 回しっぱなしにするための作り:
    ///   ・1回ごとに Play を出入りする（前の回の人・ロボット・NavMesh を持ち越さない）。
    ///     Play をまたいで消えないよう、進みは EditorPrefs に置く
    ///   ・1回の実時間の上限は ExperimentRunner.maxWallSecondsPerRun（40 分）。
    ///     超えた回は書き出して stopped で台帳に残し、次へ進む
    ///   ・回しているあいだは caffeinate で Mac のスリープを止める（終わったら戻す）
    ///   ・途中で止めても、もう一度押せば **台帳を見て** まだ回していない回から続ける
    ///
    /// シミュレーションの動きは1行も変えない。入れたのは「まとめて回す仕組み」と
    /// 「台帳への書き出し」だけ。
    /// </summary>
    public static class ServiceSimStageB
    {
        const string P1Path = "Assets/ServiceSim/Store/StoreLayout_p1_efficient_corner.asset";
        const string P5Path = "Assets/ServiceSim/Store/StoreLayout_p5_crammed.asset";

        /// <summary>1回の実時間の上限（秒）。依頼 v7 の 3</summary>
        const float MaxWallSecondsPerRun = 40f * 60f;

        /// <summary>最初の何回かが失敗したら、残りを回さずに止める</summary>
        const int FailFastRuns = 2;

        const string LogFileName = "run_all_log.txt";

        /// <summary>EditorPrefs にためるログの区切り（1文字。行の中に出ない字を使う）</summary>
        const char LogSep = '\u001f';

        // ---- Play をまたいで持つ進み（EditorPrefs）----
        const string KeyActive = "ServiceSim.StageB.active";
        const string KeyIndex = "ServiceSim.StageB.index";
        const string KeyConds = "ServiceSim.StageB.conds";     // "2:0;5:0;..."
        const string KeyFolders = "ServiceSim.StageB.folders";  // ";" 区切り
        const string KeyUnityCs = "ServiceSim.StageB.unityCs";
        const string KeyPending = "ServiceSim.StageB.pending";
        const string KeyLog = "ServiceSim.StageB.log";
        const string KeyRunStart = "ServiceSim.StageB.runStart";
        const string KeyCaffeinate = "ServiceSim.StageB.caffeinate";
        const string KeyBatchStart = "ServiceSim.StageB.batchStart";

        // ---------------- メニュー ----------------

        [MenuItem("ServiceSim/Stage B/Run All（18回）", false, 200)]
        public static void RunAll()
        {
            ExperimentRunner runner; ExperimentPlan plan;
            if (!ServiceSimRunSupport.Prepare(out runner, out plan)) return;

            // --- 押す前の確かめ（合わなければ回し始めない）---
            if (!CheckBeforeRun(plan, out string why))
            {
                Debug.LogError("[ServiceSim] 段階B：回し始めません\n" + why);
                return;
            }

            SimParams sp = LoadParams();
            if (sp == null) return;
            if (sp.Conditions.Count != 6)
            {
                Debug.LogError("[ServiceSim] 段階B：unity_conditions が 6 件ではありません（" +
                               sp.Conditions.Count + " 件）。params を確かめてください");
                return;
            }

            // --- 18回の並びを作る ---
            List<int> condIdx; List<int> seeds;
            BuildOrder(out condIdx, out seeds);

            string ledger = ServiceSimLedger.PathOf(plan);
            string day = DateTime.Now.ToString("yyyyMMdd");

            // --- 回し始めの版（照合キーに入れるので先に読む。依頼 v8 の 3）---
            string cs, pendingNote;
            ReadChangeset(out cs, out pendingNote);

            // 照合は「店・台数・種・版」。出力フォルダ名（日付入り）では照らし合わせない
            HashSet<string> done = ServiceSimLedger.DoneKeys(ledger);

            List<string> conds = new List<string>();
            List<string> folders = new List<string>();
            int skipped = 0;
            for (int i = 0; i < condIdx.Count; i++)
            {
                SimParams.Condition c = sp.ConditionAt(condIdx[i]);
                if (done.Contains(ServiceSimLedger.RunKey(c.store, c.robots, seeds[i], cs)))
                { skipped++; continue; }
                conds.Add(condIdx[i] + ":" + seeds[i]);
                folders.Add(FreeFolderName(plan, day, cs, c, seeds[i]));
            }

            if (conds.Count == 0)
            {
                Debug.Log("[ServiceSim] 段階B：この版（" + cs + "）の18回はぜんぶ台帳にあります（" +
                          skipped + " 回を飛ばしました）。" +
                          "\n  やり直すときは台帳の行を見て、エイイチに相談してください" +
                          "\n  台帳: " + ledger);
                return;
            }

            EditorPrefs.SetBool(KeyActive, true);
            EditorPrefs.SetInt(KeyIndex, 0);
            EditorPrefs.SetString(KeyConds, string.Join(";", conds.ToArray()));
            EditorPrefs.SetString(KeyFolders, string.Join(";", folders.ToArray()));
            EditorPrefs.SetString(KeyUnityCs, cs);
            EditorPrefs.SetString(KeyPending, pendingNote);
            EditorPrefs.SetString(KeyLog, "");
            EditorPrefs.SetString(KeyBatchStart, day);

            StartCaffeinate();

            Debug.Log("[ServiceSim] 段階B：" + conds.Count + " / 18 回を回します" +
                      (skipped > 0 ? "（台帳にある " + skipped + " 回は飛ばします）" : "") +
                      "\n  Unity の版: " + cs + (pendingNote.Length > 0 ? "（" + pendingNote + "）" : "") +
                      "\n  台帳: " + ledger +
                      "\n  1回の実時間の上限: " + (MaxWallSecondsPerRun / 60f).ToString("0") + " 分" +
                      "\n  1回ごとに Play を出入りします。止めたいときは、Play を抜けたところで" +
                      " ServiceSim/Stage B/いまの回で止める を押してください");

            StartNext();
        }

        [MenuItem("ServiceSim/Stage B/押す前の確かめ（回さない）", false, 201)]
        public static void CheckOnly()
        {
            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);
            if (plan == null) { Debug.LogError("[ServiceSim] ExperimentPlan が見つかりません"); return; }

            string why;
            bool ok = CheckBeforeRun(plan, out why);
            string ledger = ServiceSimLedger.PathOf(plan);
            HashSet<string> done = ServiceSimLedger.DoneFolders(ledger);

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("[ServiceSim] 段階B：押す前の確かめ " + (ok ? "→ 回せます" : "→ 回せません"));
            if (!ok) sb.AppendLine(why);
            sb.AppendLine("  台帳: " + ledger);
            sb.AppendLine("  台帳にある回: " + done.Count + " 件");
            sb.AppendLine("  列（" + ServiceSimLedger.Columns.Length + "）: " + ServiceSimLedger.HeaderLine);
            if (ok) Debug.Log(sb.ToString().TrimEnd()); else Debug.LogError(sb.ToString().TrimEnd());
        }

        [MenuItem("ServiceSim/Stage B/いまの回で止める", false, 202)]
        public static void StopBatch()
        {
            if (!EditorPrefs.GetBool(KeyActive, false))
            {
                Debug.Log("[ServiceSim] 段階B：いま回していません");
                return;
            }
            Finish("エイイチが止めました");
        }

        [MenuItem("ServiceSim/Stage B/台帳を開く", false, 210)]
        public static void OpenLedger()
        {
            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);
            if (plan == null) return;
            string p = ServiceSimLedger.PathOf(plan);
            if (!File.Exists(p)) { Debug.LogWarning("[ServiceSim] まだありません: " + p); return; }
            EditorUtility.RevealInFinder(p);
        }

        // ---------------- 押す前の確かめ ----------------

        static bool CheckBeforeRun(ExperimentPlan plan, out string why)
        {
            StringBuilder sb = new StringBuilder();

            if (!Directory.Exists(plan.kioxiaVolumePath))
                sb.AppendLine("  ・KIOXIA がつながっていません: " + plan.kioxiaVolumePath);

            if (!Directory.Exists(plan.kioxiaOutputRoot))
                sb.AppendLine("  ・置き場所がありません: " + plan.kioxiaOutputRoot +
                              "（MASTER の保存先です。新しく作らずに確かめてください）");

            string ledger = ServiceSimLedger.PathOf(plan);
            string headerWhy;
            if (!ServiceSimLedger.CanAppend(ledger, out headerWhy))
                sb.AppendLine("  ・" + headerWhy.Replace("\n", "\n  "));

            StoreBuilder builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
            NavMeshBaker baker = UnityEngine.Object.FindObjectOfType<NavMeshBaker>();
            if (builder == null || baker == null)
                sb.AppendLine("  ・StoreBuilder / NavMeshBaker が見つかりません。ServiceSim/Setup を実行してください");

            why = sb.ToString().TrimEnd();
            return why.Length == 0;
        }

        // ---------------- 18回の並び ----------------

        /// <summary>
        /// ③ロボット2台 種0 → ⑦ロボット2台 種0 → 残りを条件・種の順。
        /// 条件の番号は unity_conditions の並び（0..5）
        /// </summary>
        static void BuildOrder(out List<int> conds, out List<int> seeds)
        {
            conds = new List<int>();
            seeds = new List<int>();

            // 最初の2回（この2回で失敗したら残りを回さない）
            Add(conds, seeds, 2, 0);   // ③ ロボット2台 種0
            Add(conds, seeds, 5, 0);   // ⑦ ロボット2台 種0

            int[] allSeeds = { 0, 1, 2 };
            for (int c = 0; c < 6; c++)
                for (int s = 0; s < allSeeds.Length; s++)
                {
                    if (c == 2 && allSeeds[s] == 0) continue;   // もう入れた
                    if (c == 5 && allSeeds[s] == 0) continue;
                    Add(conds, seeds, c, allSeeds[s]);
                }
        }

        static void Add(List<int> conds, List<int> seeds, int c, int s)
        {
            conds.Add(c);
            seeds.Add(s);
        }

        /// <summary>
        /// 出力フォルダの名前（依頼 v8 の 3）。
        /// {日付}_stageB_cs{版}_{店}_r{台数}_s{種}。
        /// 版が分かるようにしておき、照合は台帳のキー（店・台数・種・版）で行う
        /// </summary>
        static string FolderName(string day, string cs, SimParams.Condition c, int seed)
        {
            // "cs:139" の ":" はフォルダ名に向かないので外す
            string ver = (cs ?? "").Replace("cs:", "").Replace(":", "").Replace("（", "").Replace("）", "");
            if (ver.Length == 0) ver = "unknown";
            return day + "_stageB_cs" + ver + "_" + c.store + "_r" + c.robots + "_s" + seed;
        }

        /// <summary>
        /// 書き出す先が既にあるときは上書きせず、末尾に _2・_3 … を付けた
        /// 新しいフォルダ名を返す（依頼 v8 の 3）
        /// </summary>
        static string FreeFolderName(ExperimentPlan plan, string day, string cs,
                                     SimParams.Condition c, int seed)
        {
            string baseName = FolderName(day, cs, c, seed);
            string name = baseName;
            for (int n = 2; n < 100; n++)
            {
                if (!Directory.Exists(Path.Combine(plan.kioxiaOutputRoot, name))) return name;
                name = baseName + "_" + n;
            }
            return name;
        }

        // ---------------- 1回ずつ回す ----------------

        static void StartNext()
        {
            if (!EditorPrefs.GetBool(KeyActive, false)) return;

            string[] conds = Split(EditorPrefs.GetString(KeyConds, ""));
            string[] folders = Split(EditorPrefs.GetString(KeyFolders, ""));
            int index = EditorPrefs.GetInt(KeyIndex, 0);

            if (index >= conds.Length || index >= folders.Length) { Finish("ぜんぶ終わりました"); return; }

            ExperimentRunner runner; ExperimentPlan plan;
            if (!ServiceSimRunSupport.Prepare(out runner, out plan))
            {
                Finish("Play の下ごしらえができませんでした");
                return;
            }

            SimParams sp = LoadParams();
            if (sp == null) { Finish("params を読めませんでした"); return; }
            if (runner.simParams == null) runner.simParams = sp;

            string[] parts = conds[index].Split(':');
            int condIndex = int.Parse(parts[0], CultureInfo.InvariantCulture);
            int seed = int.Parse(parts[1], CultureInfo.InvariantCulture);

            SimParams.Condition cond = sp.ConditionAt(condIndex);
            if (cond == null) { Finish("条件 " + condIndex + " がありません"); return; }

            string assetPath = cond.store == "fam_p1" ? P1Path : cond.store == "fam_p5" ? P5Path : null;
            JsonStoreLayout layout = assetPath != null
                ? AssetDatabase.LoadAssetAtPath<JsonStoreLayout>(assetPath) : null;
            if (layout == null) { Finish("店の形が見つかりません: " + cond.store); return; }

            StoreBuilder builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
            NavMeshBaker baker = UnityEngine.Object.FindObjectOfType<NavMeshBaker>();
            if (builder == null || baker == null) { Finish("StoreBuilder / NavMeshBaker がありません"); return; }

            builder.layout = layout;
            baker.layout = layout;
            EditorUtility.SetDirty(builder);
            EditorUtility.SetDirty(baker);

            ExperimentRunner.Job job = new ExperimentRunner.Job();
            job.siteRun = true;
            job.conditionIndex = condIndex;
            job.storeKey = cond.store;
            job.robots = cond.robots;
            job.seed = seed;
            job.aisleWidthIndex = layout.aisleWidthIndex;
            job.crowd = CrowdLevel.Normal;
            job.applyDeliveryOffset = false;
            job.folder = Path.Combine(plan.kioxiaOutputRoot, folders[index]);

            runner.SetSingleJob(job);
            runner.timeMode = ExperimentRunner.TimeMode.FixedStep;
            runner.captureDeltaTimeOverride = 0f;
            runner.maxWallSecondsPerRun = MaxWallSecondsPerRun;
            // 回し始めの版と、保留中の変更があったかを run_info に残す（依頼 v7 の 2）
            runner.unityCs = EditorPrefs.GetString(KeyUnityCs, "");
            runner.pendingNote = EditorPrefs.GetString(KeyPending, "");
            runner.runLabel = "stageB";
            runner.viewMode = false;
            runner.runOnPlay = true;
            EditorUtility.SetDirty(runner);
            EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());

            EditorPrefs.SetString(KeyRunStart, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));

            Debug.Log("[ServiceSim] 段階B " + (index + 1) + "/" + conds.Length + " を回します" +
                      "\n  条件 " + condIndex + "：" + cond.Id + "（" + cond.store +
                      "・ロボット " + cond.robots + " 台）/ 種 " + seed +
                      "\n  置き場所: " + job.folder);

            EditorApplication.EnterPlaymode();
        }

        /// <summary>Play を抜けたら、その回を台帳に足して次へ</summary>
        [InitializeOnLoadMethod]
        static void Hook()
        {
            EditorApplication.playModeStateChanged -= OnPlayModeChanged;
            EditorApplication.playModeStateChanged += OnPlayModeChanged;
        }

        static void OnPlayModeChanged(PlayModeStateChange state)
        {
            if (state != PlayModeStateChange.EnteredEditMode) return;
            if (!EditorPrefs.GetBool(KeyActive, false)) return;

            // Play を抜けた直後にそのまま Play へ入れないので、1 コマ置く
            EditorApplication.delayCall += AfterRun;
        }

        static void AfterRun()
        {
            if (!EditorPrefs.GetBool(KeyActive, false)) return;

            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);
            if (plan == null) { Finish("ExperimentPlan が見つかりません"); return; }

            string[] conds = Split(EditorPrefs.GetString(KeyConds, ""));
            string[] folders = Split(EditorPrefs.GetString(KeyFolders, ""));
            int index = EditorPrefs.GetInt(KeyIndex, 0);
            if (index >= folders.Length) { Finish("ぜんぶ終わりました"); return; }

            string folder = folders[index];
            string runDir = Path.Combine(plan.kioxiaOutputRoot, folder);

            float wallS = 0f;
            long ticks;
            if (long.TryParse(EditorPrefs.GetString(KeyRunStart, "0"),
                              NumberStyles.Integer, CultureInfo.InvariantCulture, out ticks) && ticks > 0)
                wallS = (float)(DateTime.UtcNow - new DateTime(ticks, DateTimeKind.Utc)).TotalSeconds;

            string status;
            string line = WriteLedgerRow(plan, conds[index], folder, runDir, wallS, out status);

            // ログに1行ためる
            string log = EditorPrefs.GetString(KeyLog, "");
            EditorPrefs.SetString(KeyLog, log.Length > 0 ? log + LogSep + line : line);

            Debug.Log("[ServiceSim] 段階B " + (index + 1) + "/" + conds.Length + " おわり: " + line);

            // 最初の2回で失敗したら、残りは回さない
            if (status == "failed" && index + 1 <= FailFastRuns)
            {
                Finish("最初の " + FailFastRuns + " 回で失敗したので、残りは回しません（" + folder + "）");
                return;
            }

            EditorPrefs.SetInt(KeyIndex, index + 1);
            if (index + 1 >= conds.Length) { Finish("ぜんぶ終わりました"); return; }

            StartNext();
        }

        // ---------------- 台帳に1行 ----------------

        /// <summary>
        /// その回の summary.json / run_info.json から台帳の1行を作って足す。
        /// 戻り値はログ用の短い1行。status も返す
        /// </summary>
        static string WriteLedgerRow(ExperimentPlan plan, string condSeed, string folder,
                                     string runDir, float wallS, out string status)
        {
            SimParams sp = LoadParams();
            string[] parts = condSeed.Split(':');
            int condIndex = int.Parse(parts[0], CultureInfo.InvariantCulture);
            int seed = int.Parse(parts[1], CultureInfo.InvariantCulture);
            SimParams.Condition cond = sp != null ? sp.ConditionAt(condIndex) : null;

            JsonValue sum = ServiceSimLedger.ReadSummary(runDir);
            JsonValue info = ReadJson(Path.Combine(runDir, "run_info.json"));

            bool stoppedEarly = info != null && info["stopped_early"].B(false);
            status = sum == null ? "failed" : (stoppedEarly ? "stopped" : "completed");

            RobotSpec rs = AssetDatabase.LoadAssetAtPath<RobotSpec>(ServiceSimSetup.RobotSpecPath);
            PeopleSpec ps = AssetDatabase.LoadAssetAtPath<PeopleSpec>(ServiceSimSetup.PeopleSpecPath);

            JsonValue simLike = sum != null ? sum["sim_like"] : null;
            JsonValue siteM = sum != null ? sum["site_metrics"] : null;
            JsonValue siteRun = sum != null ? sum["site_run"] : null;
            JsonValue trips = sum != null ? sum["trips"] : null;
            JsonValue stops = sum != null ? sum["stops"] : null;

            Dictionary<string, string> row = new Dictionary<string, string>();
            row["timestamp"] = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
            row["run_folder"] = folder;
            row["status"] = status;
            row["unity_cs"] = EditorPrefs.GetString(KeyUnityCs, "");
            row["layout_generation"] = sp != null ? sp.LayoutGeneration : "";
            row["params_model"] = sp != null ? sp.Model : "";

            if (cond != null)
            {
                row["store"] = cond.store;
                row["staff"] = ServiceSimLedger.Num(cond.staff);
                row["robots"] = ServiceSimLedger.Num(cond.robots);
                row["arrivals_per_h"] = ServiceSimLedger.Num(cond.arrivalsPerHour);
                row["mode"] = cond.mode;
                row["trays"] = ServiceSimLedger.Num(cond.trays);
                row["fragile_by"] = cond.fragileBy;
            }
            row["seed"] = ServiceSimLedger.Num(seed);

            // 決まり（回したときの設定）。run_info にあればそれを、無ければ .asset から
            row["staff_first_yield"] = info != null && !info["staff_first_yield"].IsNull
                ? ServiceSimLedger.Flag(info["staff_first_yield"].B(false))
                : ServiceSimLedger.Flag(ps != null && ps.staffFirstYield);
            row["yield_order_enabled"] = info != null && !info["yield_order_enabled"].IsNull
                ? ServiceSimLedger.Flag(info["yield_order_enabled"].B(false))
                : ServiceSimLedger.Flag(rs != null && rs.yieldOrderEnabled);
            row["one_robot_per_aisle"] = info != null && !info["one_robot_per_aisle"].IsNull
                ? ServiceSimLedger.Flag(info["one_robot_per_aisle"].B(false))
                : ServiceSimLedger.Flag(rs != null && rs.oneRobotPerAisle);
            row["robot_first_wait_s"] = Val(simLike, "robot_first_wait_s");

            row["everyone_left"] = siteRun != null && !siteRun["everyone_left"].IsNull
                ? ServiceSimLedger.Flag(siteRun["everyone_left"].B(false)) : "";
            row["total_minutes"] = Val(siteRun, "total_minutes");
            row["wall_seconds"] = info != null && !info["wall_seconds"].IsNull
                ? Val(info, "wall_seconds") : ServiceSimLedger.Num(wallS);

            row["entry_wait_mean"] = Val(siteM, "entry_wait_mean");
            row["staff_wait_mean"] = Val(siteM, "staff_wait_mean");
            row["dish_wait_mean"] = Val(siteM, "dish_wait_mean");
            row["cold_share"] = Val(siteM, "cold_share");
            row["robot_share"] = Val(siteM, "robot_share");
            row["staff_util"] = Val(siteM, "staff_util");
            row["robot_util"] = Val(siteM, "robot_util");
            row["walk_km_per_staff"] = Val(siteM, "walk_km_per_staff");

            // サイトの試算（同じ条件の site_result）
            if (cond != null)
            {
                row["site_entry_wait_mean"] = Site(cond, "entry_wait_mean");
                row["site_staff_wait_mean"] = Site(cond, "staff_wait_mean");
                row["site_dish_wait_mean"] = Site(cond, "dish_wait_mean");
                row["site_robot_share"] = Site(cond, "robot_share");
            }

            // サイトの unity_measures と同じ意味で Unity が測った値。
            // pause_rate・stuck_rate は hop_m（role=site_only）が要るので空欄にする
            row["robot_speed_mps"] = Val(trips, "robot_speed_mps");
            row["pause_rate"] = "";
            row["pause_s_mean"] = Val(trips, "pause_s_mean");
            row["stuck_rate"] = "";

            row["trips"] = Val(trips, "trips");
            row["timeouts"] = Val(trips, "timeouts");
            row["stuck_count"] = Val(stops, "stuck_count");
            row["stop_s_total"] = Val(stops, "stop_s_total");
            row["corner_stop_time_share"] = Val(simLike, "corner_stop_time_share");

            row["gave_up"] = Val(simLike, "gave_up");
            row["warped"] = Val(simLike, "warped");
            row["table_freed"] = Val(simLike, "table_freed");
            row["rescues"] = Val(siteM, "rescues");

            row["late_staff_dishes"] = Val(simLike, "late_staff_dishes");
            row["staff_yield_share_of_staff_time"] = Val(simLike, "staff_yield_share_of_staff_time");

            row["ledger_schema_version"] = ServiceSimLedger.Num(ServiceSimLedger.SchemaVersion);

            string ledger = ServiceSimLedger.PathOf(plan);
            string why;
            if (!ServiceSimLedger.Append(ledger, row, out why))
                Debug.LogWarning("[ServiceSim] 台帳に足しませんでした: " + why);

            return folder + " / " + status +
                   " / 実時間 " + (wallS / 60f).ToString("0.0") + " 分" +
                   " / 入店待ち " + Or(row, "entry_wait_mean") + " 分" +
                   " / ロボットが運んだ割合 " + Or(row, "robot_share");
        }

        static string Val(JsonValue v, string key)
        {
            if (v == null || v.IsNull) return "";
            JsonValue x = v[key];
            if (x.IsNull) return "";
            return ServiceSimLedger.Num(x.F(0f));
        }

        static string Site(SimParams.Condition c, string key)
        {
            float f;
            return c != null && c.siteResult.TryGetValue(key, out f) ? ServiceSimLedger.Num(f) : "";
        }

        static string Or(Dictionary<string, string> row, string key)
        {
            string v;
            return row.TryGetValue(key, out v) && v.Length > 0 ? v : "-";
        }

        static JsonValue ReadJson(string path)
        {
            if (!File.Exists(path)) return null;
            try
            {
                string err;
                JsonValue v = JsonValue.Parse(File.ReadAllText(path), out err);
                return v != null && v.IsObject ? v : null;
            }
            catch (Exception) { return null; }
        }

        // ---------------- 終わり ----------------

        static void Finish(string whyDone)
        {
            string[] conds = Split(EditorPrefs.GetString(KeyConds, ""));
            int index = EditorPrefs.GetInt(KeyIndex, 0);
            string log = EditorPrefs.GetString(KeyLog, "");

            StopCaffeinate();

            ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(
                ServiceSimSetup.ExperimentPlanPath);

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("[ServiceSim] 段階B：" + whyDone);
            sb.AppendLine("  回した: " + index + " / " + conds.Length + " 回");
            sb.AppendLine("  台帳: " + (plan != null ? ServiceSimLedger.PathOf(plan) : "-"));
            sb.AppendLine("  一覧（出力フォルダ / status / 実時間 / 入店待ち / ロボットが運んだ割合）:");
            string[] lines = log.Length > 0
                ? log.Split(LogSep) : new string[0];
            for (int i = 0; i < lines.Length; i++) sb.AppendLine("    " + (i + 1) + ". " + lines[i]);

            string text = sb.ToString().TrimEnd();
            Debug.Log(text);

            // run_all_log.txt（service_sim/unity/ の中）
            if (plan != null && Directory.Exists(plan.kioxiaOutputRoot))
            {
                try
                {
                    string p = Path.Combine(plan.kioxiaOutputRoot, LogFileName);
                    string head = "=== " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
                                  " 段階B（Unity " + EditorPrefs.GetString(KeyUnityCs, "") + "）===\n";
                    File.AppendAllText(p, head + text + "\n\n", new UTF8Encoding(false));
                    Debug.Log("[ServiceSim] 一覧を書きました: " + p);
                }
                catch (Exception e)
                {
                    Debug.LogWarning("[ServiceSim] run_all_log.txt を書けませんでした: " + e.Message);
                }
            }

            EditorPrefs.SetBool(KeyActive, false);
            EditorPrefs.DeleteKey(KeyConds);
            EditorPrefs.DeleteKey(KeyFolders);
            EditorPrefs.DeleteKey(KeyIndex);
            EditorPrefs.DeleteKey(KeyLog);
            EditorPrefs.DeleteKey(KeyRunStart);
        }

        // ---------------- スリープを止める ----------------

        static void StartCaffeinate()
        {
            StopCaffeinate();
            try
            {
                Process p = new Process();
                p.StartInfo.FileName = "/usr/bin/caffeinate";
                p.StartInfo.Arguments = "-dimsu";
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.CreateNoWindow = true;
                p.Start();
                EditorPrefs.SetInt(KeyCaffeinate, p.Id);
                Debug.Log("[ServiceSim] スリープを止めました（caffeinate pid " + p.Id + "）");
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] caffeinate を起動できませんでした（スリープしないよう手で設定してください）: "
                                 + e.Message);
            }
        }

        static void StopCaffeinate()
        {
            int pid = EditorPrefs.GetInt(KeyCaffeinate, 0);
            if (pid <= 0) return;
            EditorPrefs.DeleteKey(KeyCaffeinate);
            try
            {
                Process p = Process.GetProcessById(pid);
                if (p != null && !p.HasExited) { p.Kill(); Debug.Log("[ServiceSim] スリープの設定を戻しました"); }
            }
            catch (Exception)
            {
                // もう終わっている
            }
        }

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

        /// <summary>
        /// 回し始めの changeset 番号と、保留中の変更があるか。
        ///
        /// cm status --machinereadable の1行めが
        ///   STATUS 139 Robotics_UX_New/... studio_unity148@unity
        /// なので、そこから版の番号を取り、2行めから先を数える
        /// （PR＝置いてあるだけ は数えない）。
        /// </summary>
        static void ReadChangeset(out string cs, out string pendingNote)
        {
            cs = "";
            pendingNote = "";

            string outp = RunCm("status --machinereadable");
            if (string.IsNullOrEmpty(outp))
            {
                // cm を呼べなかった。数が分からないので「不明」と書く（依頼 v8 の 3）
                cs = "不明";
                pendingNote = "不明（cm status を読めませんでした）";
                return;
            }

            // 保留の数は Unity Version Control の「保留中の変更」と同じ数え方にする。
            // 置いてあるだけ（PR＝Private）も、あの一覧には出るので **数に入れる**
            // （前は PR を外していたので、2件あったのに空になっていた）
            int controlled = 0, privateItems = 0;
            string[] lines = outp.Split('\n');
            for (int i = 0; i < lines.Length; i++)
            {
                string l = lines[i].Trim();
                if (l.Length == 0) continue;
                if (l.StartsWith("STATUS "))
                {
                    string[] f = l.Split(' ');
                    if (f.Length > 1) cs = "cs:" + f[1];
                    continue;
                }
                if (l.StartsWith("PR ")) privateItems++;
                else controlled++;
            }

            int all = controlled + privateItems;
            if (all > 0)
                pendingNote = "保留あり " + all + " 件（管理下 " + controlled +
                              " 件・置いてあるだけ " + privateItems + " 件）";
            if (cs.Length == 0) cs = "不明";
        }

        static string RunCm(string args)
        {
            string[] paths = { "/usr/local/bin/cm", "/opt/homebrew/bin/cm", "cm" };
            for (int i = 0; i < paths.Length; i++)
            {
                try
                {
                    Process p = new Process();
                    p.StartInfo.FileName = paths[i];
                    p.StartInfo.Arguments = args;
                    p.StartInfo.WorkingDirectory = Directory.GetCurrentDirectory();
                    p.StartInfo.UseShellExecute = false;
                    p.StartInfo.RedirectStandardOutput = true;
                    p.StartInfo.RedirectStandardError = true;
                    p.StartInfo.CreateNoWindow = true;
                    p.Start();
                    string outp = p.StandardOutput.ReadToEnd();
                    p.WaitForExit(15000);
                    if (!string.IsNullOrEmpty(outp)) return outp;
                }
                catch (Exception)
                {
                    // 次の場所を試す
                }
            }
            return null;
        }

        static string[] Split(string s)
        {
            if (string.IsNullOrEmpty(s)) return new string[0];
            return s.Split(';');
        }

        static SimParams LoadParams()
        {
            SimParams sp = AssetDatabase.LoadAssetAtPath<SimParams>(ServiceSimSetup.SimParamsPath);
            if (sp == null)
            {
                Debug.LogError("[ServiceSim] SimParams が見つかりません: " + ServiceSimSetup.SimParamsPath);
                return null;
            }
            sp.Load("");
            if (sp.Conditions.Count == 0)
            {
                Debug.LogError("[ServiceSim] unity_conditions を読めません");
                return null;
            }
            return sp;
        }
    }
}
