using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 通し台帳の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度書かない決まりは、移すときに変えていない。
    /// </summary>
    public static class LedgerRow
    {
        /// <summary>行を作るのに要る、外から渡す値</summary>
        public class Inputs
        {
            /// <summary>サイトの試算の値（版・店の形の版・条件ごとの site_result）</summary>
            public SimParams simParams;

            /// <summary>この回の条件（store・staff・robots・arrivals_per_h・mode・trays・fragile_by）</summary>
            public SimParams.Condition cond;

            /// <summary>run_info.json に無いときの控え（決まりの入切を読む）</summary>
            public RobotSpec robotSpec;
            public PeopleSpec peopleSpec;

            /// <summary>"ServiceSim/cs:12" の形</summary>
            public string unityCs;

            /// <summary>出力フォルダの名前（照合キー）と、その場所</summary>
            public string runFolder;
            public string runDir;

            /// <summary>種</summary>
            public int seed;

            /// <summary>実時間（秒）。run_info.json に wall_seconds があればそちらを使う</summary>
            public float wallS;

            /// <summary>台帳の実体（unity_ledger_v1.csv）</summary>
            public string ledgerPath;
        }


        /// <summary>
        /// その回の summary.json / run_info.json から台帳の1行を作って足す。
        /// 戻り値はログ用の短い1行。status も返す
        /// </summary>
        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<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"] = 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));
        }

        /// <summary>
        /// 数を返すが、入れ物はあって名前が無いときは 0 を返す（空欄にしない）。
        /// job_wait_over_count は「1件も無い」と「読めなかった」を分けたいので使う
        /// </summary>
        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<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; }
        }
    }
}
