// PanelBloodMoonAgent — writes a tiny status JSON the panel scheduler
// reads to decide whether to delay a restart through a blood-moon horde.
//
// Status file path: /game-saves/.panel-bloodmoon-status.json
//   (lives next to .panel-telnet-password — same volume the entrypoint
//   uses for panel-owned state)
//
// Shape:
//   {
//     "schema":           1,
//     "written_at_unix":  1763500000,
//     "in_game_day":      14,
//     "in_game_hour":     22,
//     "in_game_minute":   3,
//     "bloodmoon_freq":   7,
//     "bloodmoon_range":  0,
//     "day_night_length": 60,
//     "next_horde_day":   14,
//     "active":           true,
//     "ends_at_unix":     1763500900,   // best-effort wall-clock end (0 = unknown)
//     "source":           "ai_director" // or "math_fallback"
//   }
//
// We bind IModApi + Mod at compile time so the 7DTD loader finds our entry
// point through its normal reflection probe. EVERY other game type
// (GamePrefs, World, AIDirector, AIDirectorBloodMoonComponent, GameStats,
// GameUtils) goes through string-keyed reflection — the refugebotserver
// Agent's 2026-04-29 outage taught us that a hard reference to a missing
// EnumGamePrefs member makes Mono refuse to load the whole assembly.
// Type-name lookup keeps us version-tolerant.

using System;
using System.IO;
using System.Reflection;
using System.Threading;

namespace Panel.BloodMoonAgent
{
    // IModApi is in Assembly-CSharp. The 7DTD ModManager scans loaded mod
    // assemblies for public, non-abstract types implementing this interface
    // and instantiates the first one it finds, then calls InitMod(Mod).
    public class BloodMoonAgent : IModApi
    {
        public void InitMod(Mod _modInstance)
        {
            try
            {
                if (_instance != null) return; // idempotent — Mod manager only calls once, but be safe
                _instance = this;
                Start();
                TryLog("InitMod completed; status file: " + OutputPath);
            }
            catch (Exception ex)
            {
                TryLog("InitMod failed: " + ex.Message);
            }
        }

        private static BloodMoonAgent? _instance;

        // Reflection handles — looked up once, cached. Null means "not in
        // this game build" and we silently skip that data source.
        private Type? _enumPrefsT;
        private Type? _gamePrefsT;
        private Type? _gameManagerT;
        private Type? _gameUtilsT;
        private Type? _gameStatsT;
        private Type? _enumGameStatsT;

        private Thread? _worker;
        private volatile bool _stopRequested;

        private const string OutputPath = "/game-saves/.panel-bloodmoon-status.json";
        private const string OutputPathTmp = "/game-saves/.panel-bloodmoon-status.json.tmp";
        private const int TickSeconds = 5;

        private void Start()
        {
            _enumPrefsT     = ResolveType("EnumGamePrefs");
            _gamePrefsT     = ResolveType("GamePrefs");
            _gameManagerT   = ResolveType("GameManager");
            _gameUtilsT     = ResolveType("GameUtils");
            _gameStatsT     = ResolveType("GameStats");
            _enumGameStatsT = ResolveType("EnumGameStats");

            _worker = new Thread(WorkerLoop) { IsBackground = true, Name = "PanelBloodMoonAgent" };
            _worker.Start();
        }

        private static Type? ResolveType(string name)
        {
            try
            {
                var t = Type.GetType(name + ", Assembly-CSharp");
                if (t != null) return t;
                t = Type.GetType(name);
                if (t != null) return t;
                // Fallback: scan loaded assemblies. Some 7DTD builds put types
                // in unexpected assemblies after Unity merges.
                foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
                {
                    try
                    {
                        var hit = asm.GetType(name);
                        if (hit != null) return hit;
                    }
                    catch { }
                }
            }
            catch { }
            return null;
        }

        private void WorkerLoop()
        {
            // Wait briefly so GameManager.Instance.World has populated. World
            // is null during early boot; writing "active=false" too early is
            // fine (panel falls back to "treat unknown as not-bloodmoon" =
            // restart proceeds normally) but we'd rather get real data ASAP.
            while (!_stopRequested)
            {
                try
                {
                    var status = Snapshot();
                    WriteStatus(status);
                }
                catch (Exception ex)
                {
                    TryLog("tick failed: " + ex.Message);
                }
                for (int i = 0; i < TickSeconds * 10 && !_stopRequested; i++)
                {
                    Thread.Sleep(100);
                }
            }
        }

        // ── Snapshot ───────────────────────────────────────────────────────
        private Status Snapshot()
        {
            var s = new Status
            {
                Schema = 1,
                WrittenAtUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                Source = "math_fallback",
            };

            // Game prefs (string-keyed, safe on missing members)
            s.BloodMoonFreq    = PrefInt("BloodMoonFrequency", 7);
            s.BloodMoonRange   = PrefInt("BloodMoonRange", 0);
            s.DayNightLength   = PrefInt("DayNightLength", 60);

            // World time → day/hour/minute via GameUtils.WorldTimeToDays/Hours
            ulong worldTime = 0;
            object? world = GetWorld();
            if (world != null)
            {
                worldTime = (ulong)(GetFieldValue(world, "worldTime") ?? 0UL);
            }
            if (worldTime > 0 && _gameUtilsT != null)
            {
                s.InGameDay    = InvokeStatic<int>(_gameUtilsT, "WorldTimeToDays", worldTime);
                s.InGameHour   = InvokeStatic<int>(_gameUtilsT, "WorldTimeToHours", worldTime);
                s.InGameMinute = (int)((worldTime / 60UL) % 60UL);
            }

            // Authoritative: ask the AIDirector's BloodMoonComponent.
            // Path: GameManager.Instance.World.aiDirector.BloodMoonComponent.BloodMoonActive
            //       GameStats.GetInt(EnumGameStats.BloodMoonDay) → next horde day
            bool gotAuthoritative = false;
            try
            {
                if (world != null)
                {
                    var aiDirector = GetFieldOrProp(world, "aiDirector");
                    if (aiDirector != null)
                    {
                        var bmComponent = GetFieldOrProp(aiDirector, "BloodMoonComponent");
                        if (bmComponent != null)
                        {
                            var activeObj = GetFieldOrProp(bmComponent, "BloodMoonActive");
                            if (activeObj is bool active)
                            {
                                s.Active = active;
                                gotAuthoritative = true;
                                s.Source = "ai_director";
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                TryLog("authoritative bloodmoon lookup failed: " + ex.Message);
            }

            // Next horde day via GameStats.GetInt(EnumGameStats.BloodMoonDay)
            try
            {
                if (_gameStatsT != null && _enumGameStatsT != null
                    && Enum.IsDefined(_enumGameStatsT, "BloodMoonDay"))
                {
                    var key = Enum.Parse(_enumGameStatsT, "BloodMoonDay");
                    var m = _gameStatsT.GetMethod("GetInt", new[] { _enumGameStatsT });
                    if (m != null)
                    {
                        var v = m.Invoke(null, new[] { key });
                        s.NextHordeDay = Convert.ToInt32(v ?? 0);
                    }
                }
            }
            catch (Exception ex)
            {
                TryLog("GameStats.GetInt(BloodMoonDay) failed: " + ex.Message);
            }

            // Math fallback for Active if AIDirector wasn't available.
            // Window is in-game 22:00 (bloodmoon day) → 04:00 (next day).
            // This is wrong when BloodMoonRange > 0 — the authoritative path
            // is the correct answer in that case. We treat math as best-effort.
            if (!gotAuthoritative && s.BloodMoonFreq > 0 && s.InGameDay > 0)
            {
                int day = s.InGameDay;
                int freq = s.BloodMoonFreq;
                bool todayIsBM = (day % freq == 0);
                bool yesterdayWasBM = (day > 1) && ((day - 1) % freq == 0);
                if (todayIsBM && s.InGameHour >= 22) s.Active = true;
                else if (yesterdayWasBM && s.InGameHour < 4) s.Active = true;
                // Also derive NextHordeDay from math if we didn't get it from GameStats
                if (s.NextHordeDay == 0)
                {
                    int rem = freq - (day % freq);
                    s.NextHordeDay = day + ((rem == 0) ? freq : rem);
                }
            }

            // Compute EndsAtUnix when active: time until in-game 04:00 of the
            // morning after the bloodmoon day, scaled by DayNightLength.
            // DayNightLength = real minutes per in-game 24h day.
            if (s.Active && s.DayNightLength > 0 && worldTime > 0)
            {
                int gameMinutesLeft;
                if (s.InGameHour >= 22)
                {
                    // 22:00 → 28:00 (04:00 next day) = up to 6h = 360 game-minutes
                    gameMinutesLeft = ((28 - s.InGameHour) * 60) - s.InGameMinute;
                }
                else if (s.InGameHour < 4)
                {
                    gameMinutesLeft = ((4 - s.InGameHour) * 60) - s.InGameMinute;
                }
                else
                {
                    // Mid-day bloodmoon? Only possible if AIDirector says so
                    // outside the normal window. Conservative: assume 360 game-min.
                    gameMinutesLeft = 360;
                }
                if (gameMinutesLeft < 0) gameMinutesLeft = 0;
                // 1440 game-minutes per in-game day → real seconds per game-min:
                //   realSecPerGameMin = (DayNightLength * 60) / 1440 = DayNightLength / 24
                double realSecondsLeft = (double)gameMinutesLeft * s.DayNightLength / 24.0;
                s.EndsAtUnix = s.WrittenAtUnix + (long)Math.Ceiling(realSecondsLeft);
            }

            return s;
        }

        // ── GamePrefs helpers (mirrors refugebotserver's pattern) ──────────
        private int PrefInt(string name, int fallback)
        {
            try
            {
                if (_enumPrefsT == null || _gamePrefsT == null) return fallback;
                if (!Enum.IsDefined(_enumPrefsT, name)) return fallback;
                var key = Enum.Parse(_enumPrefsT, name);
                var m = _gamePrefsT.GetMethod("GetInt", new[] { _enumPrefsT });
                if (m == null) return fallback;
                return Convert.ToInt32(m.Invoke(null, new[] { key }) ?? fallback);
            }
            catch { return fallback; }
        }

        // ── Reflection helpers ─────────────────────────────────────────────
        private object? GetWorld()
        {
            try
            {
                if (_gameManagerT == null) return null;
                // GameManager.Instance is a static FIELD (not a property) in
                // current 7DTD builds. Try field first, then property as a
                // safety net for older/newer builds.
                object? instance = null;
                var instF = _gameManagerT.GetField("Instance",
                    BindingFlags.Public | BindingFlags.Static);
                if (instF != null) instance = instF.GetValue(null);
                if (instance == null)
                {
                    var instP = _gameManagerT.GetProperty("Instance",
                        BindingFlags.Public | BindingFlags.Static);
                    if (instP != null) instance = instP.GetValue(null);
                }
                if (instance == null) return null;
                return GetFieldOrProp(instance, "World");
            }
            catch { return null; }
        }

        private static object? GetFieldOrProp(object target, string name)
        {
            if (target == null) return null;
            var t = target.GetType();
            var f = t.GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (f != null) return f.GetValue(target);
            var p = t.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (p != null) return p.GetValue(target);
            return null;
        }

        private static object? GetFieldValue(object target, string name)
        {
            return GetFieldOrProp(target, name);
        }

        private static T InvokeStatic<T>(Type t, string methodName, params object[] args)
        {
            try
            {
                var argTypes = new Type[args.Length];
                for (int i = 0; i < args.Length; i++) argTypes[i] = args[i].GetType();
                var m = t.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static, null, argTypes, null);
                if (m == null) return default!;
                var v = m.Invoke(null, args);
                return v == null ? default! : (T)Convert.ChangeType(v, typeof(T));
            }
            catch { return default!; }
        }

        // ── Output ─────────────────────────────────────────────────────────
        private void WriteStatus(Status s)
        {
            string json = s.ToJson();
            try
            {
                // Atomic write: write to .tmp, then rename. Reader sees either
                // the old file or the new one, never a torn write.
                File.WriteAllText(OutputPathTmp, json);
                // File.Replace fails on Mono if destination doesn't exist; use
                // a plain Move+Delete fallback.
                if (File.Exists(OutputPath))
                {
                    File.Delete(OutputPath);
                }
                File.Move(OutputPathTmp, OutputPath);
            }
            catch (Exception ex)
            {
                TryLog("write status failed: " + ex.Message);
            }
        }

        // ── Logging ────────────────────────────────────────────────────────
        // 7DTD's Log.Out is in Assembly-CSharp. We do best-effort reflection
        // so logs surface in the game console; fall back to stderr otherwise.
        private static Type? _logT;
        private static MethodInfo? _logOutM;
        private static bool _logResolved;
        private static void TryLog(string msg)
        {
            try
            {
                if (!_logResolved)
                {
                    _logT = Type.GetType("Log, Assembly-CSharp") ?? Type.GetType("Log");
                    if (_logT != null)
                    {
                        _logOutM = _logT.GetMethod("Out", new[] { typeof(string) });
                    }
                    _logResolved = true;
                }
                var line = "[PanelBloodMoonAgent] " + msg;
                if (_logOutM != null)
                {
                    _logOutM.Invoke(null, new object[] { line });
                }
                else
                {
                    Console.Error.WriteLine(line);
                }
            }
            catch { }
        }

        // ── Status DTO ─────────────────────────────────────────────────────
        // Hand-rolled JSON — keeps the mod dependency-free (no Newtonsoft
        // load, no risk of clashing with 7DTD's bundled copy).
        private class Status
        {
            public int Schema;
            public long WrittenAtUnix;
            public int InGameDay;
            public int InGameHour;
            public int InGameMinute;
            public int BloodMoonFreq;
            public int BloodMoonRange;
            public int DayNightLength;
            public int NextHordeDay;
            public bool Active;
            public long EndsAtUnix;
            public string Source = "";

            public string ToJson()
            {
                return "{"
                    + "\"schema\":" + Schema
                    + ",\"written_at_unix\":" + WrittenAtUnix
                    + ",\"in_game_day\":" + InGameDay
                    + ",\"in_game_hour\":" + InGameHour
                    + ",\"in_game_minute\":" + InGameMinute
                    + ",\"bloodmoon_freq\":" + BloodMoonFreq
                    + ",\"bloodmoon_range\":" + BloodMoonRange
                    + ",\"day_night_length\":" + DayNightLength
                    + ",\"next_horde_day\":" + NextHordeDay
                    + ",\"active\":" + (Active ? "true" : "false")
                    + ",\"ends_at_unix\":" + EndsAtUnix
                    + ",\"source\":\"" + JsonEscape(Source) + "\""
                    + "}";
            }

            private static string JsonEscape(string s)
            {
                if (string.IsNullOrEmpty(s)) return "";
                return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
            }
        }
    }
}
