using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using UnityEngine;
namespace ServiceSim
{
///
/// 通し台帳の1行を、その回の summary.json / run_info.json から作って足す
/// (2026-10-26・依頼 v12 の 2〜3 章)。
///
/// なぜ分けたか
/// 段階Bの Run All(Editor)と、夜の画面なしの実行ファイル(player)の
/// 両方が台帳に1行書く。列の作り方が2通りに分かれると、
/// 同じ回でも書く値が違う、という最悪の壊れ方をする。
/// なので **行を作る所はここ1つだけ** にして、
/// Editor だけで取れる値(.asset・EditorPrefs)は呼ぶ側が Inputs に入れて渡す。
///
/// Editor 側:ServiceSimStageB.WriteLedgerRow が AssetDatabase と
/// EditorPrefs から詰める
/// player 側:HeadlessRun がシーンの参照と build_info.json から詰める
///
/// 列の中身・status の決め方・2度書かない決まりは、移すときに変えていない。
///
public static class LedgerRow
{
/// 行を作るのに要る、外から渡す値
public class Inputs
{
/// サイトの試算の値(版・店の形の版・条件ごとの site_result)
public SimParams simParams;
/// この回の条件(store・staff・robots・arrivals_per_h・mode・trays・fragile_by)
public SimParams.Condition cond;
/// run_info.json に無いときの控え(決まりの入切を読む)
public RobotSpec robotSpec;
public PeopleSpec peopleSpec;
/// "ServiceSim/cs:12" の形
public string unityCs;
/// 出力フォルダの名前(照合キー)と、その場所
public string runFolder;
public string runDir;
/// 種
public int seed;
/// 実時間(秒)。run_info.json に wall_seconds があればそちらを使う
public float wallS;
/// 台帳の実体(unity_ledger_v1.csv)
public string ledgerPath;
}
///
/// その回の summary.json / run_info.json から台帳の1行を作って足す。
/// 戻り値はログ用の短い1行。status も返す
///
public static string BuildAndAppend(Inputs inp, out string status)
{
if (inp == null) throw new ArgumentNullException("inp");
SimParams sp = inp.simParams;
SimParams.Condition cond = inp.cond;
int seed = inp.seed;
string folder = inp.runFolder;
string runDir = inp.runDir;
float wallS = inp.wallS;
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 = inp.robotSpec;
PeopleSpec ps = inp.peopleSpec;
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 row = new Dictionary();
row["timestamp"] = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
row["run_folder"] = folder;
row["status"] = status;
row["unity_cs"] = inp.unityCs != null ? inp.unityCs : "";
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);
// ---- 依頼 v10b の 1:会計待ち・片付け待ちを試算と並べる(schema 2)----
// 詰まる場所が試算(レジ)と Unity(片付け)で違うので、
// 会計待ち・片付け待ちの卓・卓が使えるまでを、Unity とサイトの両方で並べる
row["pay_wait_mean"] = Val(siteM, "pay_wait_mean");
row["blocked_wait_mean"] = Val(siteM, "blocked_wait_mean");
row["turn_mean"] = Val(siteM, "turn_mean");
if (cond != null)
{
row["site_pay_wait_mean"] = Site(cond, "pay_wait_mean");
row["site_blocked_wait_mean"] = Site(cond, "blocked_wait_mean");
row["site_turn_mean"] = Site(cond, "turn_mean");
}
// 仕事が板に載ってから取られるまで(依頼 v10 の 2 で summary.json に入れた欄)。
// 仕事の名前は StaffAgent.JobKindName と同じものを使う(字を手で書かない)
JsonValue jobMean = sum != null ? sum["job_wait_mean_s"] : null;
JsonValue jobMax = sum != null ? sum["job_wait_max_s"] : null;
JsonValue jobOver = sum != null ? sum["job_wait_over_count"] : null;
string clearName = StaffAgent.JobKindName(StaffJobKind.Clear);
string payName = StaffAgent.JobKindName(StaffJobKind.Pay);
row["clear_wait_mean_s"] = Val(jobMean, clearName);
row["clear_wait_max_s"] = Val(jobMax, clearName);
// 600 秒を超えた片付けが1件も無いときは、summary.json にその名前が出ないので 0 を書く
row["clear_wait_over_count"] = ValOrZero(jobOver, clearName);
row["pay_job_wait_mean_s"] = Val(jobMean, payName);
string ledger = inp.ledgerPath;
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));
}
///
/// 数を返すが、入れ物はあって名前が無いときは 0 を返す(空欄にしない)。
/// job_wait_over_count は「1件も無い」と「読めなかった」を分けたいので使う
///
static string ValOrZero(JsonValue v, string key)
{
if (v == null || v.IsNull) return "";
JsonValue x = v[key];
return ServiceSimLedger.Num(x.IsNull ? 0f : 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 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; }
}
}
}