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

namespace ServiceSim
{
    /// <summary>
    /// 時間帯ごとの記録（依頼 v10b の 2。2026-10-25）。
    ///
    /// 店の中の時間で 60 秒ごとに、そのコマの様子をそのまま 1 行書く（平均にしない）。
    /// 「ピークのどの時間帯に、誰が何をしていて、何が板に残っていたか」を
    /// 試算と並べて調べるための記録で、店の回し方は 1 つも変えない。
    ///
    /// 1 回の出力フォルダに timeline.csv が 1 つできる（段階Bと Site Run で同じ作り）。
    ///
    /// 卓の数え方は TableSlot の中身での場合分けなので、取りこぼし・二重数えが出ない:
    ///   tables_free          groupId &lt; 0 かつ clearing でない（TableSlot.Free）
    ///   tables_dirty_untaken clearing で、片付けの仕事を誰も取っていない
    ///   tables_clearing      clearing で、片付けの仕事をスタッフが取っている
    ///   seated_groups        groupId ≧ 0 かつ clearing でない（組が座っている卓）
    /// この 4 つを足すと、いつでも卓の数になる（③42・⑦48）。CheckSums で確かめる。
    ///
    /// スタッフも同じで、StaffAgent.All を「仕事なし（待機）」と仕事の種類 6 つに分ける
    /// （Guide・Serve・Call・Pay・Clear・Rescue。StaffJobKind のぜんぶ）。
    /// 足すとスタッフの人数になる。
    /// </summary>
    public class TimelineLogger
    {
        /// <summary>60 秒ごとの 1 行</summary>
        public struct Row
        {
            public float tS;
            public int arrived, waitingGroups, seatedGroups, payWaitingSeat, payInLine;
            public int tablesFree, tablesDirtyUntaken, tablesClearing;
            public int boardGuide, boardServe, boardCall, boardPay, boardClear;
            public int staffIdle, staffGuide, staffServe, staffCall, staffPay, staffClear, staffRescue;
            public int robotsBusy;

            /// <summary>卓の 4 つの数え方の合計（卓の数と同じになるはず）</summary>
            public int TableSum
            {
                get { return seatedGroups + tablesFree + tablesDirtyUntaken + tablesClearing; }
            }

            /// <summary>スタッフの人数の合計（スタッフの人数と同じになるはず）</summary>
            public int StaffSum
            {
                get
                {
                    return staffIdle + staffGuide + staffServe + staffCall +
                           staffPay + staffClear + staffRescue;
                }
            }
        }

        public const string Header =
            "condition_id,seed,t_s,arrived,waiting_groups,seated_groups," +
            "pay_waiting_seat,pay_in_line," +
            "tables_free,tables_dirty_untaken,tables_clearing," +
            "board_guide,board_serve,board_call,board_pay,board_clear," +
            "staff_idle,staff_guide,staff_serve,staff_call,staff_pay,staff_clear,staff_rescue," +
            "robots_busy";

        readonly List<Row> _rows = new List<Row>();

        public int RowCount { get { return _rows.Count; } }
        public IReadOnlyList<Row> Rows { get { return _rows; } }
        public void Clear() { _rows.Clear(); }

        // ---------------- 1 行ぶん数える ----------------

        /// <summary>
        /// そのコマの様子を 1 行足す。ExperimentRunner が 60 秒ごとに呼ぶ。
        /// 数えるだけで、何も書き換えない
        /// </summary>
        public void Sample(float tS, GroupManager groups, ServiceOps ops)
        {
            if (groups == null || ops == null) return;

            Row r = Build(tS, groups.GroupsArrived, groups.WaitingGroups,
                          ops.Tables, ops.Jobs, groups.Groups, CountRobotsBusy(ops));
            CountStaff(StaffAgent.All, ref r);
            _rows.Add(r);
        }

        /// <summary>
        /// 1 行ぶんを組み立てる（スタッフ以外）。
        /// MonoBehaviour を触らないので、Unity 抜きでも試せる（確かめ用に public）。
        /// スタッフの数は、呼んだ側が CountStaff か AddStaff で足す
        /// </summary>
        public static Row Build(float tS, int arrived, int waitingGroups,
                                IList<TableSlot> tables, IList<StaffJob> jobs,
                                IList<GroupManager.Group> groups, int robotsBusy)
        {
            Row r = new Row();
            r.tS = tS;
            r.arrived = arrived;
            r.waitingGroups = waitingGroups;
            r.robotsBusy = robotsBusy;

            CountTables(tables, jobs, ref r);
            CountGroups(groups, ref r);
            CountBoard(jobs, ref r);
            return r;
        }

        /// <summary>組み立てた 1 行を足す（Sample と、確かめの試しが使う）</summary>
        public void AddRow(Row r) { _rows.Add(r); }

        /// <summary>
        /// 卓を 4 つに分ける。clearing の卓は、片付けの仕事が取られているかで分ける
        /// （取られていない＝ふつうの片付け待ち。これが長いと卓が回らない）。
        ///
        /// MonoBehaviour を触らないので、Unity 抜きでも試せる（確かめ用に public）
        /// </summary>
        public static void CountTables(IList<TableSlot> tables, IList<StaffJob> jobs, ref Row r)
        {
            for (int i = 0; i < tables.Count; i++)
            {
                TableSlot t = tables[i];
                if (t == null) continue;

                if (t.clearing)
                {
                    if (ClearJobTaken(jobs, t.index)) r.tablesClearing++;
                    else r.tablesDirtyUntaken++;
                }
                else if (t.groupId >= 0) r.seatedGroups++;
                else r.tablesFree++;
            }
        }

        /// <summary>その卓の片付けの仕事を、スタッフが取っているか</summary>
        static bool ClearJobTaken(IList<StaffJob> jobs, int tableIndex)
        {
            for (int i = 0; i < jobs.Count; i++)
            {
                StaffJob j = jobs[i];
                if (j == null || j.kind != StaffJobKind.Clear) continue;
                if (j.tableIndex != tableIndex) continue;
                if (j.taken) return true;
            }
            return false;
        }

        /// <summary>
        /// 会計を待っている組を 2 つに分ける。
        ///   pay_waiting_seat  食べ終えたが、レジの並ぶ点が取れず席で待っている
        ///   pay_in_line       並ぶ点に立っている（会計の最中も入る）
        /// レジの前に人を立たせない版（payAtRegisterByOne が false）では
        /// 並ぶ点を取らないので、ぜんぶ pay_waiting_seat に入る
        /// </summary>
        static void CountGroups(IList<GroupManager.Group> gs, ref Row r)
        {
            if (gs == null) return;
            for (int i = 0; i < gs.Count; i++)
            {
                GroupManager.Group g = gs[i];
                if (g == null || g.state != GroupManager.GState.Paying) continue;
                if (g.payerSpot >= 0) r.payInLine++;
                else r.payWaitingSeat++;
            }
        }

        /// <summary>板に載っていて、まだ誰も取っていない仕事の数（種類ごと。確かめ用に public）</summary>
        public static void CountBoard(IList<StaffJob> jobs, ref Row r)
        {
            for (int i = 0; i < jobs.Count; i++)
            {
                StaffJob j = jobs[i];
                if (j == null || j.taken) continue;
                switch (j.kind)
                {
                    case StaffJobKind.Guide: r.boardGuide++; break;
                    case StaffJobKind.Serve: r.boardServe++; break;
                    case StaffJobKind.Call: r.boardCall++; break;
                    case StaffJobKind.Pay: r.boardPay++; break;
                    case StaffJobKind.Clear: r.boardClear++; break;
                }
            }
        }

        /// <summary>いまその仕事をしているスタッフの人数（仕事を持っていない人は待機）</summary>
        static void CountStaff(IReadOnlyList<StaffAgent> all, ref Row r)
        {
            for (int i = 0; i < all.Count; i++)
            {
                StaffAgent a = all[i];
                if (a == null) continue;
                AddStaff(a.CurrentJob, ref r);
            }
        }

        /// <summary>
        /// スタッフ1人を、持っている仕事の種類の欄に1つ足す（仕事が無ければ待機）。
        /// StaffJobKind のどの値でも必ず1つだけ増えるので、
        /// staff_* の合計はスタッフの人数と同じになる（確かめ用に public）
        /// </summary>
        public static void AddStaff(StaffJob j, ref Row r)
        {
            if (j == null) { r.staffIdle++; return; }
            switch (j.kind)
            {
                case StaffJobKind.Guide: r.staffGuide++; break;
                case StaffJobKind.Serve: r.staffServe++; break;
                case StaffJobKind.Call: r.staffCall++; break;
                case StaffJobKind.Pay: r.staffPay++; break;
                case StaffJobKind.Clear: r.staffClear++; break;
                case StaffJobKind.Rescue: r.staffRescue++; break;
                default: r.staffIdle++; break;   // 種類が増えたら待機に入れて、合計を崩さない
            }
        }

        /// <summary>
        /// 走っている・卓で待っているロボットの台数。
        /// robot_util と同じ見方（待機場所に下がっている Idle と、
        /// 往復を終えた Done は「空いている」）
        /// </summary>
        static int CountRobotsBusy(ServiceOps ops)
        {
            int n = 0;
            IList<ServingRobotController> rs = ops.Robots;
            for (int i = 0; i < rs.Count; i++)
            {
                ServingRobotController rb = rs[i];
                if (rb == null) continue;
                ServingRobotController.Phase p = rb.CurrentPhase;
                if (p != ServingRobotController.Phase.Idle &&
                    p != ServingRobotController.Phase.Done) n++;
            }
            return n;
        }

        // ---------------- 確かめ（依頼 v10b の 3 A）----------------

        /// <summary>
        /// どの行でも、卓の 4 つの合計が卓の数と同じで、
        /// スタッフの人数の合計がスタッフの人数と同じになっているか。
        /// 合わない行があれば、いちばん早い行を why に書く
        /// </summary>
        public bool CheckSums(int tables, int staff, out int badTableRows, out int badStaffRows,
                              out string why)
        {
            badTableRows = 0;
            badStaffRows = 0;
            why = "";

            string firstBad = "";
            for (int i = 0; i < _rows.Count; i++)
            {
                Row r = _rows[i];
                bool tOk = tables <= 0 || r.TableSum == tables;
                bool sOk = staff <= 0 || r.StaffSum == staff;
                if (!tOk) badTableRows++;
                if (!sOk) badStaffRows++;
                if ((!tOk || !sOk) && firstBad.Length == 0)
                    firstBad = "t_s " + F(r.tS) +
                               " で 卓 " + r.TableSum + "（座 " + r.seatedGroups +
                               "・空 " + r.tablesFree + "・片付け待ち " + r.tablesDirtyUntaken +
                               "・片付け中 " + r.tablesClearing + "）/ " + tables +
                               "、スタッフ " + r.StaffSum + " / " + staff;
            }

            why = firstBad;
            return badTableRows == 0 && badStaffRows == 0;
        }

        // ---------------- 書き出し ----------------

        public void Write(string path, string conditionId, int seed)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(Header).Append('\n');
            for (int i = 0; i < _rows.Count; i++)
            {
                Row r = _rows[i];
                sb.Append(conditionId).Append(',').Append(seed).Append(',')
                  .Append(F(r.tS)).Append(',')
                  .Append(r.arrived).Append(',')
                  .Append(r.waitingGroups).Append(',')
                  .Append(r.seatedGroups).Append(',')
                  .Append(r.payWaitingSeat).Append(',')
                  .Append(r.payInLine).Append(',')
                  .Append(r.tablesFree).Append(',')
                  .Append(r.tablesDirtyUntaken).Append(',')
                  .Append(r.tablesClearing).Append(',')
                  .Append(r.boardGuide).Append(',')
                  .Append(r.boardServe).Append(',')
                  .Append(r.boardCall).Append(',')
                  .Append(r.boardPay).Append(',')
                  .Append(r.boardClear).Append(',')
                  .Append(r.staffIdle).Append(',')
                  .Append(r.staffGuide).Append(',')
                  .Append(r.staffServe).Append(',')
                  .Append(r.staffCall).Append(',')
                  .Append(r.staffPay).Append(',')
                  .Append(r.staffClear).Append(',')
                  .Append(r.staffRescue).Append(',')
                  .Append(r.robotsBusy).Append('\n');
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        static string F(float v)
        {
            return v.ToString("0.###", CultureInfo.InvariantCulture);
        }
    }
}
