using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;

namespace ServiceSim.EditorTools
{
    /// <summary>
    /// 画面なしの実行ファイル（macOS の普通の player）を作る
    /// （2026-10-26・依頼 v12 の 1 章）。
    ///
    /// メニューからも、コマンドからも作れる:
    ///   メニュー  ServiceSim → 画面なしの実行ファイルを作る
    ///   コマンド  /Applications/Unity/Hub/Editor/2022.3.62f3/Unity.app/Contents/MacOS/Unity \
    ///               -batchmode -quit -nographics \
    ///               -projectPath /Users/shimazueiichi/Robotics_UX/ServiceSim \
    ///               -executeMethod ServiceSim.EditorTools.BuildHeadless.Build \
    ///               -logFile /dev/stdout
    ///
    /// 出力（VCS に入れない。ignore.conf に Builds/ を入れてある）
    ///   Builds/headless/ServiceSim.app
    ///   Builds/headless/build_info.json
    ///
    /// 回し方は Dedicated Server ではなく **普通の macOS の player** にした。
    ///   ・-batchmode -nographics で画面を作らずに回せるので、これで足りる
    ///   ・Dedicated Server は別のモジュールが要り、入れる手間が増える
    ///   ・いまのコードは「画面なしでも同じ数字が出る」ことを確かめる段（5 章 B）なので、
    ///     段階Bと同じ player のまま回すほうが、違いの元が増えない
    ///
    /// シーンはここで直接指定する（ビルド設定に何が入っていても、
    /// 作られる実行ファイルの中身が変わらないようにするため。1 章）。
    /// </summary>
    public static class BuildHeadless
    {
        /// <summary>出力の置き場所（プロジェクトの中。VCS に入れない）</summary>
        public const string OutDir = "Builds/headless";

        /// <summary>実行ファイルの名前。tools/night_runner.py の DEFAULT_BUILD と合わせる</summary>
        public const string AppName = "ServiceSim.app";

        /// <summary>そばに置く覚え書き</summary>
        public const string InfoName = "build_info.json";

        [MenuItem("ServiceSim/画面なしの実行ファイルを作る", false, 60)]
        public static void Build()
        {
            string err;
            if (!BuildTo(OutDir, out err))
            {
                Debug.LogError("[ServiceSim] 画面なしの実行ファイルを作れませんでした\n  " + err);
                if (Application.isBatchMode) EditorApplication.Exit(1);
                return;
            }
            if (Application.isBatchMode) EditorApplication.Exit(0);
        }

        /// <summary>
        /// 作る本体。できたら true。
        /// 中で ProjectSettings やビルド設定（EditorBuildSettings）は書き換えない
        /// </summary>
        public static bool BuildTo(string outDir, out string err)
        {
            err = "";

            if (!File.Exists(ServiceSimSetup.ScenePath))
            {
                err = "シーンがありません: " + ServiceSimSetup.ScenePath +
                      "\n  先に ServiceSim/Setup を実行してください";
                return false;
            }

            // 版と保留の数（台帳の unity_cs に入る字。段階Bと同じ関数を使う）
            string cs, pendingNote;
            ServiceSimStageB.ReadChangeset(out cs, out pendingNote);
            if (pendingNote.Length > 0)
                Debug.LogWarning("[ServiceSim] 保留中の変更があるまま作ります: " + pendingNote +
                                 "\n  この実行ファイルの中身は " + cs +
                                 " と同じではありません（台帳の unity_cs は " + cs + " になります）。" +
                                 "\n  夜の回に使うなら、先に check-in して保留 0 にしてから作り直してください");

            string root = Directory.GetCurrentDirectory();
            string dir = Path.IsPathRooted(outDir) ? outDir : Path.Combine(root, outDir);
            string appPath = Path.Combine(dir, AppName);

            try
            {
                if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
                // 前の版が残っていると、古い実行ファイルで夜を回してしまう。消して作り直す
                if (Directory.Exists(appPath)) Directory.Delete(appPath, true);
            }
            catch (Exception e)
            {
                err = "置き場所を用意できません: " + dir + "\n  " + e.Message;
                return false;
            }

            // Apple Silicon で動く形にする（Rosetta で動くと遅くなり、
            // 何本同時に回せるか（5 章 C）の実測が変わってしまう）。
            // 設定は PlaybackEngines の DLL の中にあり、Unity 抜きの組み立てでは
            // 参照できないので、反射で入れる（入らなくてもビルドは続ける）
            string archNote = SetAppleSiliconArchitecture();

            BuildPlayerOptions opt = new BuildPlayerOptions();
            opt.scenes = new string[] { ServiceSimSetup.ScenePath };   // シーンはここで直接指定（1 章）
            opt.locationPathName = appPath;
            opt.target = BuildTarget.StandaloneOSX;
            opt.targetGroup = BuildTargetGroup.Standalone;
            opt.options = BuildOptions.None;

            BuildReport report = BuildPipeline.BuildPlayer(opt);
            if (report == null)
            {
                err = "BuildPipeline が何も返しませんでした";
                return false;
            }
            BuildSummary sum = report.summary;
            if (sum.result != BuildResult.Succeeded)
            {
                err = "ビルドが " + sum.result + " で終わりました（エラー " + sum.totalErrors + " 件）";
                return false;
            }

            string exe = Path.Combine(appPath, "Contents", "MacOS");
            string exeNote = Directory.Exists(exe) ? FirstFile(exe) : "";

            string buildId = BuildId(cs);
            string infoPath = Path.Combine(dir, InfoName);
            if (!WriteInfo(infoPath, buildId, cs, pendingNote, appPath, exeNote, sum, archNote, out err))
                return false;

            Debug.Log("[ServiceSim] 画面なしの実行ファイルを作りました" +
                      "\n  " + appPath +
                      "\n  実行ファイル: " + (exeNote.Length > 0 ? Path.Combine(exe, exeNote) : "（見つかりません）") +
                      "\n  版: " + cs + (pendingNote.Length > 0 ? "（" + pendingNote + "）" : "（保留 0）") +
                      "\n  build_id: " + buildId +
                      "\n  覚え書き: " + infoPath +
                      "\n  大きさ: " + (sum.totalSize / (1024f * 1024f)).ToString("0.0") + " MB" +
                      " / かかった時間: " + sum.totalTime.TotalSeconds.ToString("0") + " 秒" +
                      "\n  作り方: " + archNote +
                      "\n  回し方: " + (exeNote.Length > 0 ? Path.Combine(exe, exeNote) : "<実行ファイル>") +
                      " -batchmode -nographics -ssCondition fam_p1_s4_r1_a42 -ssSeed 1");
            return true;
        }

        /// <summary>
        /// build_info.json（依頼 v12 の 1 章）。
        /// unity_cs と build_id は、夜の実行係（night_runner.py）と
        /// 実行ファイルの中（HeadlessRun）の両方が読む。
        /// 名前を変えるときは、tools/night_runner.py の read_unity_cs も直すこと
        /// </summary>
        static bool WriteInfo(string path, string buildId, string cs, string pendingNote,
                              string appPath, string exeName, BuildSummary sum,
                              string archNote, out string err)
        {
            err = "";
            StringBuilder sb = new StringBuilder();
            sb.Append("{\n");
            sb.Append("  \"schema\": \"servicesim-build-info/0.1\",\n");
            sb.Append("  \"build_id\": ").Append(Str(buildId)).Append(",\n");
            sb.Append("  \"unity_cs\": ").Append(Str(cs)).Append(",\n");
            sb.Append("  \"pending_changes\": ").Append(Str(pendingNote)).Append(",\n");
            sb.Append("  \"built_at\": ")
              .Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture))).Append(",\n");
            sb.Append("  \"editor_version\": ").Append(Str(Application.unityVersion)).Append(",\n");
            sb.Append("  \"target\": ").Append(Str(BuildTarget.StandaloneOSX.ToString())).Append(",\n");
            sb.Append("  \"architecture\": ").Append(Str(archNote)).Append(",\n");
            sb.Append("  \"scene\": ").Append(Str(ServiceSimSetup.ScenePath)).Append(",\n");
            sb.Append("  \"app\": ").Append(Str(Rel(appPath))).Append(",\n");
            sb.Append("  \"exe\": ").Append(Str(exeName)).Append(",\n");
            sb.Append("  \"ledger_schema_version\": ").Append(ServiceSimLedger.SchemaVersion).Append(",\n");
            sb.Append("  \"total_size_bytes\": ").Append(sum.totalSize).Append(",\n");
            sb.Append("  \"build_seconds\": ")
              .Append(sum.totalTime.TotalSeconds.ToString("0.0", CultureInfo.InvariantCulture)).Append("\n");
            sb.Append("}\n");

            try
            {
                File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
            }
            catch (Exception e)
            {
                err = "build_info.json を書けません: " + path + "\n  " + e.Message;
                return false;
            }
            return true;
        }

        /// <summary>
        /// この実行ファイルの名札。版と作った時刻から作る。
        /// 例 cs12_20261026_1305（保留があれば頭に dirty_ を付ける）
        /// </summary>
        static string BuildId(string cs)
        {
            string n = "";
            int at = (cs ?? "").IndexOf("cs:", StringComparison.Ordinal);
            if (at >= 0)
                for (int i = at + 3; i < cs.Length && cs[i] >= '0' && cs[i] <= '9'; i++) n += cs[i];
            if (n.Length == 0) n = "unknown";
            return "cs" + n + "_" + DateTime.Now.ToString("yyyyMMdd_HHmm", CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Apple Silicon 向けにする。
        /// UnityEditor.OSXStandalone.UserBuildSettings.architecture を反射で入れる
        /// （この型は PlaybackEngines の DLL にあり、Unity 抜きの組み立てでは参照できない）。
        /// 入れられなければ、そのことを書いて既定のまま作る
        /// </summary>
        static string SetAppleSiliconArchitecture()
        {
            try
            {
                Type t = null;
                System.Reflection.Assembly[] all = AppDomain.CurrentDomain.GetAssemblies();
                for (int i = 0; i < all.Length && t == null; i++)
                    t = all[i].GetType("UnityEditor.OSXStandalone.UserBuildSettings", false);
                if (t == null) return "既定（UserBuildSettings が見つかりません）";

                System.Reflection.PropertyInfo prop = t.GetProperty("architecture",
                    System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
                if (prop == null) return "既定（architecture が見つかりません）";

                object arm = Enum.Parse(prop.PropertyType, "ARM64");
                prop.SetValue(null, arm, null);
                return "ARM64（Apple Silicon）";
            }
            catch (Exception e)
            {
                return "既定（入れられませんでした: " + e.Message + "）";
            }
        }

        static string FirstFile(string dir)
        {
            try
            {
                string[] files = Directory.GetFiles(dir);
                for (int i = 0; i < files.Length; i++)
                {
                    string name = Path.GetFileName(files[i]);
                    if (name.StartsWith(".", StringComparison.Ordinal)) continue;
                    return name;
                }
            }
            catch (Exception) { }
            return "";
        }

        static string Rel(string full)
        {
            string root = Directory.GetCurrentDirectory();
            return full.StartsWith(root, StringComparison.Ordinal)
                ? full.Substring(root.Length).TrimStart('/') : full;
        }

        static string Str(string s)
        {
            if (s == null) s = "";
            StringBuilder sb = new StringBuilder("\"");
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                if (c == '"' || c == '\\') sb.Append('\\').Append(c);
                else if (c == '\n') sb.Append("\\n");
                else if (c == '\r') sb.Append("\\r");
                else if (c == '\t') sb.Append("\\t");
                else if (c < ' ') sb.Append(' ');
                else sb.Append(c);
            }
            return sb.Append('"').ToString();
        }
    }
}
