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

namespace ServiceSim.EditorTools
{
    /// <summary>
    /// 段階Bの通し台帳（2026-10-23 エイイチと決めた。依頼 v7）。
    ///
    /// 実体: {kioxiaOutputRoot}/unity_ledger_v1.csv
    ///       （MASTER の保存先「service_sim/unity/」の中。新しい置き場所は作らない）
    ///
    /// 決まり:
    ///   ・1回 = 1行・**追記のみ**。既存の行は書き換えない・消さない
    ///   ・同じ出力フォルダの行を2度書かない（run_folder が照合キー）
    ///   ・**追記ガード**：実体のヘッダとこのコードの列の並びが1字でも違えば追記しない
    ///   ・止めた回・失敗した回も status を付けて1行残す
    ///
    /// 列の名前と並びを変えるときは、先にエイイチに聞く（依頼 v7 の 2）。
    /// </summary>
    public static class ServiceSimLedger
    {
        public const int SchemaVersion = 1;
        public const string FileName = "unity_ledger_v1.csv";

        /// <summary>列（この順。名前を変えるときは先にエイイチに聞く）</summary>
        public static readonly string[] Columns =
        {
            "timestamp", "run_folder", "status", "unity_cs", "layout_generation", "params_model",
            "store", "staff", "robots", "arrivals_per_h", "mode", "trays", "fragile_by", "seed",
            "staff_first_yield", "yield_order_enabled", "one_robot_per_aisle", "robot_first_wait_s",
            "everyone_left", "total_minutes", "wall_seconds",
            "entry_wait_mean", "staff_wait_mean", "dish_wait_mean", "cold_share", "robot_share",
            "staff_util", "robot_util", "walk_km_per_staff",
            "site_entry_wait_mean", "site_staff_wait_mean", "site_dish_wait_mean", "site_robot_share",
            "robot_speed_mps", "pause_rate", "pause_s_mean", "stuck_rate",
            "trips", "timeouts", "stuck_count", "stop_s_total", "corner_stop_time_share",
            "gave_up", "warped", "table_freed", "rescues",
            "late_staff_dishes", "staff_yield_share_of_staff_time",
            "ledger_schema_version"
        };

        public static string HeaderLine { get { return string.Join(",", Columns); } }

        /// <summary>台帳の実体の場所</summary>
        public static string PathOf(ExperimentPlan plan)
        {
            string root = plan != null && !string.IsNullOrEmpty(plan.kioxiaOutputRoot)
                ? plan.kioxiaOutputRoot
                : "/Volumes/KIOXIA/r2r2r-data/service_sim/unity";
            return Path.Combine(root, FileName);
        }

        // ---------------- 追記ガード ----------------

        /// <summary>
        /// 追記していいか。
        ///   ・実体が無い … いい（header を書いて作る）
        ///   ・1行めがコードの列と同じ … いい
        ///   ・違う … だめ（why に理由）
        /// </summary>
        public static bool CanAppend(string path, out string why)
        {
            why = "";
            if (!File.Exists(path)) return true;

            string first;
            try
            {
                using (StreamReader r = new StreamReader(path, new UTF8Encoding(false)))
                    first = r.ReadLine();
            }
            catch (Exception e)
            {
                why = "台帳を読めません: " + path + "\n  " + e.Message;
                return false;
            }

            if (first == null)
            {
                why = "台帳が空です（ヘッダがありません）: " + path +
                      "\n  いちど消すか、ヘッダを書いてください";
                return false;
            }

            string have = first.TrimEnd('\r', '\n');
            if (have == HeaderLine) return true;

            why = "台帳のヘッダがコードの列と違うので追記しません（追記ガード）: " + path +
                  "\n  実体: " + have +
                  "\n  コード: " + HeaderLine +
                  "\n  列をそろえるか、エイイチに聞いてから直してください";
            return false;
        }

        /// <summary>その出力フォルダの行がもうあるか（run_folder が照合キー）</summary>
        public static bool HasRow(string path, string runFolder)
        {
            if (!File.Exists(path) || string.IsNullOrEmpty(runFolder)) return false;

            int col = IndexOf("run_folder");
            try
            {
                using (StreamReader r = new StreamReader(path, new UTF8Encoding(false)))
                {
                    r.ReadLine();                       // ヘッダ
                    string line;
                    while ((line = r.ReadLine()) != null)
                    {
                        if (line.Length == 0) continue;
                        string[] f = line.Split(',');
                        if (col < f.Length && f[col] == runFolder) return true;
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] 台帳を読めませんでした（照合を飛ばします）: " + e.Message);
            }
            return false;
        }

        /// <summary>もう台帳にある出力フォルダの名前をぜんぶ返す（2度書きの歯止めに使う）</summary>
        public static HashSet<string> DoneFolders(string path)
        {
            HashSet<string> done = new HashSet<string>();
            if (!File.Exists(path)) return done;

            int col = IndexOf("run_folder");
            int statusCol = IndexOf("status");
            try
            {
                using (StreamReader r = new StreamReader(path, new UTF8Encoding(false)))
                {
                    r.ReadLine();
                    string line;
                    while ((line = r.ReadLine()) != null)
                    {
                        if (line.Length == 0) continue;
                        string[] f = line.Split(',');
                        if (col >= f.Length) continue;
                        // status に関係なく、1行あるものは「回した」とみなす
                        // （止めた回・失敗した回を何度も回し直さないため）
                        if (statusCol < f.Length && f[statusCol].Length == 0) continue;
                        done.Add(f[col]);
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] 台帳を読めませんでした: " + e.Message);
            }
            return done;
        }

        /// <summary>
        /// 回した回を照らし合わせるキー（依頼 v8 の 3）。
        ///
        /// **店・ロボットの台数・乱数の種・Unity の版** でそろえる。
        /// 出力フォルダ名（日付入り）では照らし合わせない。
        ///   ・同じ版・同じ条件・同じ種の行があれば、日付が違っても回さない
        ///   ・版が違えば回す（直したあとに同じ18回をもう一度回すため）
        /// </summary>
        public static string RunKey(string store, int robots, int seed, string unityCs)
        {
            return (store ?? "") + "|r" + robots + "|s" + seed + "|" + (unityCs ?? "");
        }

        /// <summary>台帳にある回のキーをぜんぶ返す（続きから回すのに使う）</summary>
        public static HashSet<string> DoneKeys(string path)
        {
            HashSet<string> done = new HashSet<string>();
            if (!File.Exists(path)) return done;

            int cStore = IndexOf("store");
            int cRobots = IndexOf("robots");
            int cSeed = IndexOf("seed");
            int cCs = IndexOf("unity_cs");
            int cStatus = IndexOf("status");

            try
            {
                using (StreamReader r = new StreamReader(path, new UTF8Encoding(false)))
                {
                    r.ReadLine();
                    string line;
                    while ((line = r.ReadLine()) != null)
                    {
                        if (line.Length == 0) continue;
                        string[] f = line.Split(',');
                        int need = Mathf.Max(Mathf.Max(cStore, cRobots), Mathf.Max(cSeed, cCs));
                        if (need >= f.Length) continue;
                        if (cStatus < f.Length && f[cStatus].Length == 0) continue;

                        int robots, seed;
                        if (!int.TryParse(f[cRobots], out robots)) continue;
                        if (!int.TryParse(f[cSeed], out seed)) continue;
                        done.Add(RunKey(f[cStore], robots, seed, f[cCs]));
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] 台帳を読めませんでした: " + e.Message);
            }
            return done;
        }

        static int IndexOf(string name)
        {
            for (int i = 0; i < Columns.Length; i++) if (Columns[i] == name) return i;
            return -1;
        }

        // ---------------- 1行足す ----------------

        /// <summary>
        /// 1行追記する。ヘッダが合わない・同じ出力フォルダの行がもうある場合は足さない。
        /// row は列名 → 値。足りない列は空欄にする
        /// </summary>
        public static bool Append(string path, IDictionary<string, string> row, out string why)
        {
            why = "";
            if (!CanAppend(path, out why)) return false;

            string folder = Get(row, "run_folder");
            if (string.IsNullOrEmpty(folder))
            {
                why = "run_folder が空なので足しません（照合キーです）";
                return false;
            }
            if (HasRow(path, folder))
            {
                why = "もう台帳にあるので足しません（2度書かない）: " + folder;
                return false;
            }

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < Columns.Length; i++)
            {
                if (i > 0) sb.Append(',');
                sb.Append(Clean(Get(row, Columns[i])));
            }

            try
            {
                string dir = Path.GetDirectoryName(path);
                if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir);

                bool makeHeader = !File.Exists(path);
                using (StreamWriter w = new StreamWriter(path, true, new UTF8Encoding(false)))
                {
                    if (makeHeader) w.Write(HeaderLine + "\n");
                    w.Write(sb.ToString() + "\n");
                }
            }
            catch (Exception e)
            {
                why = "台帳に書けません: " + path + "\n  " + e.Message;
                return false;
            }
            return true;
        }

        static string Get(IDictionary<string, string> row, string key)
        {
            string v;
            return row != null && row.TryGetValue(key, out v) && v != null ? v : "";
        }

        /// <summary>CSV を壊さないように、カンマ・改行・引用符は取り除く（値は数と短い名前だけ）</summary>
        static string Clean(string s)
        {
            if (string.IsNullOrEmpty(s)) return "";
            s = s.Replace(',', ' ').Replace('"', ' ');
            s = s.Replace("\r", " ").Replace("\n", " ");
            return s.Trim();
        }

        // ---------------- summary.json から1行を作る ----------------

        /// <summary>数を台帳の書き方にそろえる（小数4桁まで・指数表記にしない）</summary>
        public static string Num(float v)
        {
            return v.ToString("0.####", CultureInfo.InvariantCulture);
        }

        public static string Num(int v)
        {
            return v.ToString(CultureInfo.InvariantCulture);
        }

        public static string Flag(bool v) { return v ? "1" : "0"; }

        /// <summary>summary.json を読む（読めなければ null）</summary>
        public static JsonValue ReadSummary(string runDir)
        {
            string p = Path.Combine(runDir, "summary.json");
            if (!File.Exists(p)) return null;
            try
            {
                string err;
                JsonValue v = JsonValue.Parse(File.ReadAllText(p), out err);
                if (v == null || !v.IsObject)
                {
                    Debug.LogWarning("[ServiceSim] summary.json を読めません: " + p + "\n  " + err);
                    return null;
                }
                return v;
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] summary.json を読めません: " + p + "\n  " + e.Message);
                return null;
            }
        }
    }
}
