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

namespace ServiceSim
{
    /// <summary>
    /// 画面なしの実行ファイルで、1条件 × 1種を1回だけ回して終わる
    /// （2026-10-26・依頼 v12 の 2 章）。
    ///
    /// 1本の実行ファイル = 1回。終わったら台帳に1行書いて、終了コードを付けて終わる。
    /// 夜の実行係（tools/night_runner.py）が、これを N 本まで同時に起動する。
    ///
    /// 引数（night_runner.py が渡す形。2 章）
    ///   -ssCondition fam_p1_s4_r1_a42   … 条件の名前（必須。これが無ければ何もしない）
    ///   -ssSeed 3                        … 種（必須）
    ///   -ssLabel night                   … 出力フォルダに入る名札（既定 night）
    ///   -ssNight 20261026                … その夜の名前。フォルダ名の先頭にもなる
    ///   -ssParallel 3                    … そのとき同時に走っていた本数（記録だけ）
    ///   -ssPassBy on|off                 … すれ違いの走り方（既定 off）
    ///   -ssTry 2                          … やり直しの回。2 以上ならフォルダ名に _try2
    ///
    /// 出力フォルダ（tools/night_runner.py の folder_name と同じ字にする）
    ///   {夜}_{名札}_cs{N}_{店}_r{台数}_s{種}[_passon][_try{n}]
    ///   例 20261026_night_cs12_fam_p1_r1_s3
    ///   cs{N} は、実行ファイルのそばの build_info.json の unity_cs（ServiceSim/cs:12）から取る。
    ///   **実行ファイル側が名前を決める**（実行係は同じ式で先に名前を作って、
    ///   もう回した回を飛ばすのに使う）。
    ///
    /// 終わり方（2 章）
    ///   0 … 最後まで回った
    ///   2 … 途中で止まった（実時間の上限など。記録は残る）
    ///   1 … 失敗（引数が読めない・条件が無い・KIOXIA が無い・記録が出ない）
    ///
    /// KIOXIA（2 章）
    ///   **回し始める前に** つながっているかを確かめる。無ければ何も書かずに 1 で終わる。
    ///   Mac の中には書かない。
    ///
    /// 回し方は段階Bの Run All と同じ道を通す（ExperimentRunner.Job を1つ作って
    ///   SetSingleJob する）。台帳の行を作るのも同じ LedgerRow 1つだけ。
    /// </summary>
    public class HeadlessRun : MonoBehaviour
    {
        /// <summary>Setup が作るときの名前</summary>
        public const string RootName = "HeadlessRun";

        /// <summary>終了コード（依頼 v12 の 2 章）</summary>
        public const int ExitOk = 0;
        public const int ExitFailed = 1;
        public const int ExitStopped = 2;

        /// <summary>build_info.json を上に何階層まで探すか（.app の中から外に出るため）</summary>
        const int BuildInfoUpLevels = 6;

        /// <summary>回し始めるまでにこれだけかかったら、何か起きていると見て失敗にする（秒）</summary>
        const float StartTimeoutS = 180f;

        [Header("参照（Setup が入れる）")]
        public ExperimentRunner runner;
        public ExperimentPlan plan;
        public SimParams simParams;
        public StoreBuilder store;
        public NavMeshBaker baker;
        public RobotSpec robotSpec;
        public PeopleSpawner people;
        public KitchenMonitor monitor;

        [Tooltip("条件の店（fam_p1 / fam_p5）に合わせて選ぶ店の形。Setup が入れる")]
        public JsonStoreLayout[] layouts;

        // ---- 引数から読んだもの ----
        string _condition = "", _label = "night", _night = "", _passBy = "off";
        int _seed, _parallel, _try = 1;

        string _unityCs = "", _buildId = "", _buildInfoPath = "";
        string _folder = "", _runDir = "";
        SimParams.Condition _cond;

        bool _active;          // 引数があり、回す気がある
        bool _sawRunning;      // runner が回り始めたのを見た
        bool _finished;        // もう後片付けをした
        float _awakeAt;

        void Awake()
        {
            // 引数に -ssCondition が無ければ、この部品は何もしない。
            // エディタで手で Play したときや、段階Bの Run All のときは必ずこちら
            string[] argv = SafeArgs();
            if (Find(argv, "-ssCondition") == null) return;

            _awakeAt = Time.realtimeSinceStartup;

            int code;
            if (!Setup(argv, out code))
            {
                Quit(code, "回し始める前に止めました");
                return;
            }
            _active = true;
        }

        /// <summary>
        /// 引数を読み、確かめ、1回ぶんの仕事を仕込む。
        /// 駄目なときは false と終了コードを返す（何も書かない）
        /// </summary>
        bool Setup(string[] argv, out int code)
        {
            code = ExitFailed;

            // ---- 引数 ----
            _condition = Find(argv, "-ssCondition") ?? "";
            string seedText = Find(argv, "-ssSeed");
            if (string.IsNullOrEmpty(_condition) || seedText == null ||
                !int.TryParse(seedText, NumberStyles.Integer, CultureInfo.InvariantCulture, out _seed))
            {
                Log("-ssCondition と -ssSeed が要ります（例 -ssCondition fam_p1_s4_r1_a42 -ssSeed 3）");
                return false;
            }
            _label = Find(argv, "-ssLabel") ?? "night";
            _night = Find(argv, "-ssNight") ?? DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
            _passBy = (Find(argv, "-ssPassBy") ?? "off").ToLowerInvariant() == "on" ? "on" : "off";
            int.TryParse(Find(argv, "-ssParallel") ?? "0", NumberStyles.Integer,
                         CultureInfo.InvariantCulture, out _parallel);
            if (!int.TryParse(Find(argv, "-ssTry") ?? "1", NumberStyles.Integer,
                              CultureInfo.InvariantCulture, out _try) || _try < 1) _try = 1;

            // ---- シーンの参照 ----
            if (runner == null) runner = FindObjectOfType<ExperimentRunner>();
            if (plan == null && runner != null) plan = runner.plan;
            if (simParams == null && runner != null) simParams = runner.simParams;
            if (store == null && runner != null) store = runner.store;
            if (baker == null && runner != null) baker = runner.baker;
            if (robotSpec == null && runner != null) robotSpec = runner.robotSpec;
            if (people == null && runner != null) people = runner.people;
            if (monitor == null && runner != null) monitor = runner.monitor;

            if (runner == null || plan == null || simParams == null || store == null || baker == null)
            {
                Log("シーンの仕掛けが足りません（ExperimentRunner / ExperimentPlan / SimParams / " +
                    "StoreBuilder / NavMeshBaker）。ServiceSim/Setup を実行したシーンで作り直してください");
                return false;
            }

            // ---- 条件 ----
            // ★ player では誰も SimParams.Load を呼んでいないので、
            //   自分で読む（段階Bの LoadParams と同じ sp.Load("")）。
            //   読んだ中身（_conditions）は .asset に残らない作りなので、
            //   Play のたび・起動のたびに読み直す必要がある。
            //   このあと ExperimentRunner が店の形の版を渡して読み直すが、
            //   同じ JSON を読むので値は変わらない
            if (!simParams.Load("") || simParams.Conditions.Count == 0)
            {
                Log("serving-robot-params.json の unity_conditions を読めません" +
                    (simParams.Problems.Count > 0
                        ? "\n  " + string.Join("\n  ", ToArray(simParams.Problems)) : ""));
                return false;
            }

            _cond = ConditionById(_condition);
            if (_cond == null)
            {
                Log("知らない条件です: " + _condition + "\n  使える条件: " + KnownConditions());
                return false;
            }

            // ---- 店の形 ----
            JsonStoreLayout layout = LayoutFor(_cond.store);
            if (layout == null)
            {
                Log("店の形が見つかりません: " + _cond.store +
                    "（HeadlessRun の layouts に入っていません。ServiceSim/Setup を実行してください）");
                return false;
            }

            // ---- 版（出力フォルダの字と、台帳の unity_cs）----
            ReadBuildInfo();
            if (string.IsNullOrEmpty(_unityCs))
                Log("注意：build_info.json の unity_cs が読めませんでした。" +
                    "出力フォルダは csunknown になります（" + _buildInfoPath + "）");

            _folder = FolderName();
            _runDir = Path.Combine(plan.kioxiaOutputRoot, _folder);

            // ---- KIOXIA（書く前に確かめる。無ければ何も書かない）----
            if (!Directory.Exists(plan.kioxiaVolumePath))
            {
                Log("KIOXIA がつながっていません: " + plan.kioxiaVolumePath +
                    "\n  何も書かずに終わります（Mac の中には書きません）");
                return false;
            }
            if (!Directory.Exists(plan.kioxiaOutputRoot))
            {
                Log("置き場所がありません: " + plan.kioxiaOutputRoot +
                    "\n  MASTER の保存先です。新しく作らずに確かめてください");
                return false;
            }
            if (Directory.Exists(_runDir))
            {
                Log("その出力フォルダはもうあります: " + _runDir +
                    "\n  上書きしません（やり直しは -ssTry 2 で別の名前にしてください）");
                return false;
            }

            // ---- 台帳のヘッダ（書けない形なら、回す前に止める）----
            string ledgerPath = ServiceSimLedger.PathOf(plan);
            string headerWhy;
            if (File.Exists(ledgerPath) && !ServiceSimLedger.CanAppend(ledgerPath, out headerWhy))
            {
                Log("台帳に書けない形です。回す前に止めます\n  " + headerWhy.Replace("\n", "\n  "));
                return false;
            }

            // ---- 仕込む（段階Bの StartNext と同じ形）----
            store.layout = layout;
            baker.layout = layout;

            if (robotSpec != null) robotSpec.passByEnabled = _passBy == "on";

            ExperimentRunner.Job job = new ExperimentRunner.Job();
            job.siteRun = true;
            job.conditionIndex = ConditionIndexOf(_cond);
            job.storeKey = _cond.store;
            job.robots = _cond.robots;
            job.seed = _seed;
            job.aisleWidthIndex = layout.aisleWidthIndex;
            job.crowd = CrowdLevel.Normal;
            job.applyDeliveryOffset = false;
            job.folder = _runDir;

            runner.SetSingleJob(job);
            runner.timeMode = ExperimentRunner.TimeMode.FixedStep;
            runner.captureDeltaTimeOverride = 0f;
            // 1回の実時間の上限は段階Bと同じ（値は ExperimentRunner に1つだけ置いてある）
            runner.maxWallSecondsPerRun = ExperimentRunner.DefaultMaxWallSecondsPerRun;
            runner.unityCs = _unityCs;
            runner.pendingNote = "";
            runner.runLabel = _label;
            runner.viewMode = false;
            runner.runnerName = "headless";
            runner.buildId = _buildId;
            runner.night = _night;
            runner.parallel = _parallel;
            runner.runOnPlay = true;        // Awake なので、runner の Start より前に立つ

            if (monitor != null) monitor.show = false;   // 画面のものは切る（2 章）

            Log("回します: " + _condition + " 種 " + _seed +
                " / すれ違い " + (_passBy == "on" ? "入" : "切") +
                (_try > 1 ? " / やり直し " + _try + " 回め" : "") +
                "\n  版 " + (_unityCs.Length > 0 ? _unityCs : "（分からない）") +
                " / build " + (_buildId.Length > 0 ? _buildId : "（分からない）") +
                " / 夜 " + _night + " / 同時 " + _parallel +
                "\n  置き場所 " + _runDir);

            code = ExitOk;
            return true;
        }

        void Update()
        {
            if (!_active || _finished) return;

            if (!_sawRunning)
            {
                if (runner.Running) { _sawRunning = true; return; }

                // 合わない値で回さずに抜けた（ExperimentRunner.AbortRun）。
                // 待っても始まらないので、すぐ失敗にする
                if (runner.Aborted)
                {
                    Quit(ExitFailed, "合わない値があって回せませんでした" +
                                     "（ログの上の行に理由が出ています）");
                    return;
                }
                if (Time.realtimeSinceStartup - _awakeAt > StartTimeoutS)
                {
                    Quit(ExitFailed, "回し始められませんでした（" + StartTimeoutS + " 秒待ちました）");
                }
                return;
            }

            if (runner.Running) return;     // まだ回っている

            // 回り終わった
            Finish();
        }

        /// <summary>
        /// 回り終わったあと：台帳に1行書いて、終了コードを決めて終わる。
        /// 行を作るのは段階Bと同じ LedgerRow 1つだけ
        /// </summary>
        void Finish()
        {
            _finished = true;

            // ExperimentRunner が店の形の版で SimParams を読み直しているので、
            // 条件の入れ物は作り直されている。名前で引き直す
            // （引けなければ回し始めに取ったものを使う。値は同じ JSON から来る）
            SimParams.Condition cond = ConditionById(_condition);
            if (cond == null) cond = _cond;

            LedgerRow.Inputs inp = new LedgerRow.Inputs();
            inp.simParams = simParams;
            inp.cond = cond;
            inp.robotSpec = robotSpec;
            inp.peopleSpec = people != null ? people.spec : null;
            inp.unityCs = _unityCs;
            inp.runFolder = _folder;
            inp.runDir = _runDir;
            inp.seed = _seed;
            inp.wallS = Time.realtimeSinceStartup - _awakeAt;
            inp.ledgerPath = ServiceSimLedger.PathOf(plan);

            string status = "failed";
            string line;
            try
            {
                line = LedgerRow.BuildAndAppend(inp, out status);
            }
            catch (Exception e)
            {
                line = "台帳の行を作れませんでした: " + e.Message;
                status = "failed";
            }

            int code = status == "completed" ? ExitOk
                     : status == "stopped" ? ExitStopped
                     : ExitFailed;
            Quit(code, "おわり: " + line);
        }

        // ---------------- 細かい道具 ----------------

        /// <summary>
        /// 出力フォルダの名前。
        /// tools/night_runner.py の folder_name と **同じ字** にする
        /// （実行係は同じ名前を先に作って、もう回した回を飛ばすのに使う）
        /// </summary>
        string FolderName()
        {
            return FolderName(_night, _label, CsToken(_unityCs),
                              _cond.store, _cond.robots, _seed, _passBy, _try);
        }

        /// <summary>
        /// 出力フォルダの名前（計算だけ。確かめのために外からも呼べるようにしてある）。
        ///
        /// ★ tools/night_runner.py の folder_name と **1字でも違ってはいけない**。
        ///   実行係はこの名前を先に作って「もう回した回」を飛ばすので、
        ///   食い違うと同じ回を2度回したり、回したのに飛ばしたりする
        /// </summary>
        public static string FolderName(string night, string label, string csToken,
                                        string store, int robots, int seed,
                                        string passBy, int tryNo)
        {
            string name = night + "_" + label + "_" + csToken + "_" +
                          store + "_r" + robots.ToString(CultureInfo.InvariantCulture) +
                          "_s" + seed.ToString(CultureInfo.InvariantCulture);
            if (passBy == "on") name += "_passon";
            if (tryNo > 1) name += "_try" + tryNo.ToString(CultureInfo.InvariantCulture);
            return name;
        }

        /// <summary>
        /// "ServiceSim/cs:12" → "cs12"。読めなければ "csunknown"。
        /// tools/night_runner.py の cs_token（既定 "cs{N}"）と同じ字にする
        /// </summary>
        public static string CsToken(string unityCs)
        {
            string n = "";
            int at = (unityCs ?? "").IndexOf("cs:", StringComparison.Ordinal);
            if (at >= 0)
            {
                for (int i = at + 3; i < unityCs.Length; i++)
                {
                    char c = unityCs[i];
                    if (c < '0' || c > '9') break;
                    n += c;
                }
            }
            return "cs" + (n.Length > 0 ? n : "unknown");
        }

        /// <summary>
        /// 実行ファイルのそばの build_info.json から、版（unity_cs）と build_id を読む。
        ///
        /// macOS の .app は Application.dataPath が
        ///   …/ServiceSim.app/Contents/Resources/Data
        /// なので、そこから上に何階層か探す（Builds/headless/ に置いてある形）
        /// </summary>
        void ReadBuildInfo()
        {
            string dir = Application.dataPath;
            for (int i = 0; i <= BuildInfoUpLevels && !string.IsNullOrEmpty(dir); i++)
            {
                string p = Path.Combine(dir, "build_info.json");
                if (File.Exists(p))
                {
                    _buildInfoPath = p;
                    try
                    {
                        string err;
                        JsonValue v = JsonValue.Parse(File.ReadAllText(p), out err);
                        if (v != null && v.IsObject)
                        {
                            _unityCs = v["unity_cs"].S("");
                            _buildId = v["build_id"].S("");
                            return;
                        }
                        Log("build_info.json が読めません: " + p + "\n  " + err);
                    }
                    catch (Exception e)
                    {
                        Log("build_info.json が読めません: " + p + "\n  " + e.Message);
                    }
                    return;
                }
                dir = Path.GetDirectoryName(dir);
            }
            _buildInfoPath = "（見つかりません。" + Application.dataPath + " から上に " +
                             BuildInfoUpLevels + " 階層まで探しました）";
        }

        SimParams.Condition ConditionById(string id)
        {
            if (simParams == null) return null;
            for (int i = 0; ; i++)
            {
                SimParams.Condition c = simParams.ConditionAt(i);
                if (c == null) return null;
                if (c.Id == id) return c;
            }
        }

        int ConditionIndexOf(SimParams.Condition cond)
        {
            if (simParams == null || cond == null) return -1;
            for (int i = 0; ; i++)
            {
                SimParams.Condition c = simParams.ConditionAt(i);
                if (c == null) return -1;
                if (c == cond) return i;
            }
        }

        string KnownConditions()
        {
            List<string> ids = new List<string>();
            for (int i = 0; simParams != null; i++)
            {
                SimParams.Condition c = simParams.ConditionAt(i);
                if (c == null) break;
                ids.Add(c.Id);
            }
            return ids.Count > 0 ? string.Join(", ", ids.ToArray()) : "（読めません）";
        }

        /// <summary>条件の店（fam_p1 / fam_p5）に合う店の形を選ぶ</summary>
        JsonStoreLayout LayoutFor(string storeKey)
        {
            if (layouts == null) return null;
            for (int i = 0; i < layouts.Length; i++)
            {
                if (layouts[i] == null) continue;
                if (layouts[i].ParamsStoreKey == storeKey) return layouts[i];
            }
            return null;
        }

        /// <summary>引数 name の次の字を返す。無ければ null</summary>
        static string Find(string[] argv, string name)
        {
            if (argv == null) return null;
            for (int i = 0; i < argv.Length - 1; i++)
                if (argv[i] == name) return argv[i + 1];
            return null;
        }

        static string[] ToArray(System.Collections.Generic.IList<string> list)
        {
            string[] a = new string[list != null ? list.Count : 0];
            for (int i = 0; i < a.Length; i++) a[i] = list[i];
            return a;
        }

        static string[] SafeArgs()
        {
            try { return Environment.GetCommandLineArgs(); }
            catch (Exception) { return new string[0]; }
        }

        static void Log(string msg)
        {
            Debug.Log("[ServiceSim/headless] " + msg);
        }

        void Quit(int code, string why)
        {
            _finished = true;
            if (code == ExitOk) Log(why + "\n  終了コード 0（最後まで回りました）");
            else if (code == ExitStopped) Debug.LogWarning("[ServiceSim/headless] " + why +
                                                           "\n  終了コード 2（途中で止まりました）");
            else Debug.LogError("[ServiceSim/headless] " + why + "\n  終了コード 1（失敗）");

#if UNITY_EDITOR
            // エディタでは Play を抜けるだけ（終了コードは付かない）
            UnityEditor.EditorApplication.isPlaying = false;
#else
            Application.Quit(code);
#endif
        }
    }
}
