using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using HSUI.Config;
using HSUI.Interface.GeneralElements;
using KamiToolKit.Controllers;
using static FFXIVClientStructs.FFXIV.Client.Game.ActionManager;

namespace HSUI.Helpers
{
    public sealed class ActionBarsManager : IDisposable
    {
        public static ActionBarsManager Instance { get; private set; } = null!;

        private AddonController? _addonController;

        private ActionBarsManager()
        {
            _addonController = new AddonController("_ActionBar");
            _addonController.Enable();
        }

        public static void Initialize()
        {
            Instance = new ActionBarsManager();
        }

        public void Dispose()
        {
            _addonController?.Disable();
            _addonController = null;
            Instance = null!;
        }

        /// <summary>
        /// Slot data for ImGui drawing. Updated each call from game state.
        /// </summary>
        public readonly struct SlotInfo
        {
            public uint IconId { get; }
            public bool IsEmpty { get; }
            public bool IsUsable { get; }
            public int CooldownPercent { get; }
            public int CooldownSecondsLeft { get; }
            public uint ActionId { get; }
            public RaptureHotbarModule.HotbarSlotType SlotType { get; }
            /// <summary>Keybind hint from game (user's keybind settings). Empty if unavailable.</summary>
            public string KeybindHint { get; }
            /// <summary>Current charges for charge-based actions; 0 when not applicable.</summary>
            public int CurrentCharges { get; }
            /// <summary>Max charges for charge-based actions; &gt;1 only for actions with charges.</summary>
            public int MaxCharges { get; }

            public SlotInfo(uint iconId, bool isEmpty, bool usable, int cooldownPct, int cooldownSecs, uint actionId = 0, RaptureHotbarModule.HotbarSlotType slotType = 0, string keybindHint = "", int currentCharges = 0, int maxCharges = 0)
            {
                IconId = iconId;
                IsEmpty = isEmpty;
                IsUsable = usable;
                CooldownPercent = cooldownPct;
                CooldownSecondsLeft = cooldownSecs;
                ActionId = actionId;
                SlotType = slotType;
                KeybindHint = keybindHint ?? "";
                CurrentCharges = currentCharges;
                MaxCharges = maxCharges;
            }
        }

        /// <summary>
        /// Reads keybind hint from HotbarSlot. Uses _keybindHint (slot display) then _popUpKeybindHint (trimmed) as fallback.
        /// Offsets match RaptureHotbarModule.HotbarSlot: _keybindHint 0xA8, _popUpKeybindHint 0x88.
        /// </summary>
        private static unsafe string ReadKeybindHintFromSlot(RaptureHotbarModule.HotbarSlot* slot)
        {
            if (slot == null) return "";
            var sb = (byte*)slot;
            string? h = Marshal.PtrToStringUTF8((IntPtr)(sb + 0xA8));
            if (!string.IsNullOrWhiteSpace(h))
                return h.Trim();
            string? p = Marshal.PtrToStringUTF8((IntPtr)(sb + 0x88));
            if (string.IsNullOrWhiteSpace(p)) return "";
            p = p!.Trim();
            if (p.StartsWith(" [", StringComparison.Ordinal) && p.EndsWith("]", StringComparison.Ordinal))
                p = p[2..^1].Trim();
            return p;
        }

        /// <summary>Track PvP state so we can restore PvE bars when leaving PvP (game sometimes leaves PvP data in StandardHotbars).</summary>
        private static bool _pvpHotbarsActiveLastFrame;
        private static bool _clientStatePvPLastFrame;
        /// <summary>After leaving PvP, keep re-applying PvE load for this many frames in case the game overwrites.</summary>
        private static int _restorePvEFramesLeft;

        /// <summary>
        /// Call once per frame (e.g. from Framework Update). When we detect leaving PvP, loads saved PvE hotbars into the live bars
        /// and keeps re-applying for a short window so PvE actions are not overwritten by stale PvP state.
        /// </summary>
        public static unsafe void TryRestorePvEHotbarsAfterLeavePvP()
        {
            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady)
                return;

            bool pvpActive = module->PvPHotbarsActive;
            bool clientPvP = Plugin.ClientState.IsPvP;

            if (!pvpActive && !clientPvP)
            {
                bool justLeftPvP = _pvpHotbarsActiveLastFrame || _clientStatePvPLastFrame;
                if (justLeftPvP)
                    _restorePvEFramesLeft = 120; // ~2s at 60fps

                if (_restorePvEFramesLeft > 0)
                {
                    uint classJobId = (uint)(module->ActiveHotbarClassJobId & 0x7F);
                    for (uint barId = 0; barId < 10; barId++)
                        module->LoadSavedHotbar(classJobId, barId);
                    _restorePvEFramesLeft--;
                }
            }

            _pvpHotbarsActiveLastFrame = pvpActive;
            _clientStatePvPLastFrame = clientPvP;
        }

        /// <summary>
        /// Reads hotbar slot data from RaptureHotbarModule. Returns up to slotCount slots.
        /// hotbarIndex 1-10 maps to StandardHotbars 0-9.
        /// Always reads from live Hotbars; when leaving PvP, TryRestorePvEHotbarsAfterLeavePvP loads saved PvE into live so we then show PvE.
        /// </summary>
        public unsafe List<SlotInfo> GetSlotData(int hotbarIndex, int slotCount)
        {
            var list = new List<SlotInfo>(slotCount);
            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady)
                return list;

            int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
            int count = Math.Clamp(slotCount, 1, 12);
            var hotbars = module->StandardHotbars;
            ref var bar = ref hotbars[barIdx];

            for (int i = 0; i < count; i++)
            {
                var slot = bar.GetHotbarSlot((uint)i);
                string keybind = ReadKeybindHintFromSlot(slot);

                if (slot == null)
                {
                    list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, keybind, 0, 0));
                    continue;
                }

                bool isEmpty = slot->IsEmpty && slot->CommandType != RaptureHotbarModule.HotbarSlotType.GearSet;
                if (isEmpty)
                {
                    list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, keybind, 0, 0));
                    continue;
                }

                var slotType = slot->CommandType == RaptureHotbarModule.HotbarSlotType.GearSet
                    ? RaptureHotbarModule.HotbarSlotType.GearSet
                    : slot->ApparentSlotType;
                uint actionId = slot->CommandType == RaptureHotbarModule.HotbarSlotType.GearSet
                    ? slot->CommandId
                    : slot->ApparentActionId;

                if (slotType == RaptureHotbarModule.HotbarSlotType.GearSet)
                    slot->LoadIconId();

                bool usable = slot->IsSlotUsable(slotType, actionId);
                uint iconId = slot->IconId;
                if (slotType == RaptureHotbarModule.HotbarSlotType.GearSet && iconId == 0)
                {
                    int resolved = slot->GetIconIdForSlot(slotType, actionId);
                    if (resolved > 0)
                        iconId = (uint)resolved;
                }

                (int pct, int secsLeft) = GetSlotCooldown(slot);
                (int currentCharges, int maxCharges) = GetSlotCharges(slotType, actionId);
                uint apparentCharges = slotType == RaptureHotbarModule.HotbarSlotType.Action ? slot->GetApparentIconRecastCharges() : 0;
                if (maxCharges > 1 && (apparentCharges > 0 || currentCharges > 0))
                    usable = true;
                list.Add(new SlotInfo(iconId, false, usable, pct, secsLeft, actionId, slotType, keybind, currentCharges, maxCharges));
            }

            return list;
        }

        /// <summary>Scratch bar/slot used to build SlotInfo or execute from (type, id) without touching game bars 1-8. Bar 10 slot 0.</summary>
        internal const int ScratchBarIndex = 10;
        internal const int ScratchSlotIndex = 0;

        /// <summary>Get SlotInfo for a single (type, id) by temporarily writing to scratch slot and reading. Used for controller bars.</summary>
        private unsafe SlotInfo GetSlotInfoViaScratch(RaptureHotbarModule.HotbarSlotType slotType, uint commandId)
        {
            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady)
                return new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0);
            int barIdx = ScratchBarIndex - 1;
            var slotPtr = module->GetSlotById((uint)barIdx, (uint)ScratchSlotIndex);
            if (slotPtr == null)
                return new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0);
            uint saveType = (uint)slotPtr->CommandType;
            uint saveId = slotPtr->CommandId;
            try
            {
                slotPtr->Set(slotType, commandId);
                slotPtr->LoadIconId();
                if (slotType == RaptureHotbarModule.HotbarSlotType.Item)
                    slotPtr->LoadCostDataForSlot(true);
                var list = GetSlotData(ScratchBarIndex, 1);
                return list.Count > 0 ? list[0] : new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0);
            }
            finally
            {
                slotPtr->Set((RaptureHotbarModule.HotbarSlotType)saveType, saveId);
                slotPtr->LoadIconId();
            }
        }

        /// <summary>Get slot data for a controller cross bar (1-8) from persisted controller storage. Returns 8 SlotInfos.</summary>
        public unsafe List<SlotInfo> GetControllerSlotData(int barIndex, int slotCount)
        {
            var list = new List<SlotInfo>(slotCount);
            var barConfig = ControllerBarConfig.GetBarConfig(barIndex);
            if (barConfig == null)
            {
                for (int i = 0; i < slotCount; i++)
                    list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0));
                return list;
            }
            barConfig.EnsureSlotsPadded();
            int count = Math.Min(slotCount, barConfig.Slots.Count);
            for (int i = 0; i < count; i++)
            {
                var entry = barConfig.Slots[i];
                var type = (RaptureHotbarModule.HotbarSlotType)entry.SlotType;
                uint id = entry.CommandId;
                if (entry.IsEmpty)
                {
                    list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0));
                    continue;
                }
                list.Add(GetSlotInfoViaScratch(type, id));
            }
            while (list.Count < slotCount)
                list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0));
            return list;
        }

        /// <summary>Place payload on a controller bar slot (persisted in config only). Bar 1-8, slot 0-7.</summary>
        public void PlacePayloadOnControllerSlot(int barIndex, int slotIndex, RaptureHotbarModule.HotbarSlotType slotType, uint commandId)
        {
            var barConfig = ControllerBarConfig.GetBarConfig(barIndex);
            if (barConfig == null) return;
            barConfig.EnsureSlotsPadded();
            int s = Math.Clamp(slotIndex, 0, ControllerBarConfig.CrossSlotCount - 1);
            while (barConfig.Slots.Count <= s)
                barConfig.Slots.Add(new ControllerSlotData { SlotType = (int)RaptureHotbarModule.HotbarSlotType.Empty, CommandId = 0 });
            barConfig.Slots[s] = new ControllerSlotData { SlotType = (int)slotType, CommandId = commandId };
            ConfigurationManager.Instance?.SaveConfigurations();
        }

        /// <summary>Clear a controller bar slot. Bar 1-8, slot 0-7.</summary>
        public bool ClearControllerSlot(int barIndex, int slotIndex)
        {
            var barConfig = ControllerBarConfig.GetBarConfig(barIndex);
            if (barConfig == null) return false;
            barConfig.EnsureSlotsPadded();
            int s = Math.Clamp(slotIndex, 0, ControllerBarConfig.CrossSlotCount - 1);
            if (s >= barConfig.Slots.Count) return true;
            barConfig.Slots[s] = new ControllerSlotData { SlotType = (int)RaptureHotbarModule.HotbarSlotType.Empty, CommandId = 0 };
            ConfigurationManager.Instance?.SaveConfigurations();
            return true;
        }

        /// <summary>Swap two controller bar slots. Bars 1-8, slots 0-7.</summary>
        public bool SwapControllerSlots(int barA, int slotA, int barB, int slotB)
        {
            var configA = ControllerBarConfig.GetBarConfig(barA);
            var configB = ControllerBarConfig.GetBarConfig(barB);
            if (configA == null || configB == null) return false;
            configA.EnsureSlotsPadded();
            configB.EnsureSlotsPadded();
            int a = Math.Clamp(slotA, 0, ControllerBarConfig.CrossSlotCount - 1);
            int b = Math.Clamp(slotB, 0, ControllerBarConfig.CrossSlotCount - 1);
            if (barA == barB && a == b) return true;
            if (a >= configA.Slots.Count || b >= configB.Slots.Count) return false;
            var tmp = configA.Slots[a];
            configA.Slots[a] = configB.Slots[b];
            configB.Slots[b] = tmp;
            ConfigurationManager.Instance?.SaveConfigurations();
            return true;
        }

        /// <summary>Execute a controller bar slot by (type, id) via scratch slot. Bar 1-8, slot 0-7.</summary>
        public unsafe bool ExecuteControllerSlot(int barIndex, int slotIndex)
        {
            var barConfig = ControllerBarConfig.GetBarConfig(barIndex);
            if (barConfig == null) return false;
            barConfig.EnsureSlotsPadded();
            int s = Math.Clamp(slotIndex, 0, ControllerBarConfig.CrossSlotCount - 1);
            if (s >= barConfig.Slots.Count) return false;
            var entry = barConfig.Slots[s];
            if (entry.IsEmpty) return false;
            var slotType = (RaptureHotbarModule.HotbarSlotType)entry.SlotType;
            uint commandId = entry.CommandId;

            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady) return false;
            var slotPtr = module->GetSlotById((uint)(ScratchBarIndex - 1), (uint)ScratchSlotIndex);
            if (slotPtr == null) return false;
            uint saveType = (uint)slotPtr->CommandType;
            uint saveId = slotPtr->CommandId;
            try
            {
                slotPtr->Set(slotType, commandId);
                slotPtr->LoadIconId();
                if (slotType == RaptureHotbarModule.HotbarSlotType.Item)
                    slotPtr->LoadCostDataForSlot(true);
                module->ExecuteSlotById((uint)(ScratchBarIndex - 1), (uint)ScratchSlotIndex);
                return true;
            }
            finally
            {
                slotPtr->Set((RaptureHotbarModule.HotbarSlotType)saveType, saveId);
                slotPtr->LoadIconId();
            }
        }

        /// <summary>
        /// Gets cooldown for a hotbar slot. For Action/GeneralAction/PetAction, uses ActionManager recast API
        /// (more accurate for adjusted IDs, recast groups). Falls back to slot's GetSlotActionCooldownPercentage for Items/Macros.
        /// </summary>
        /// <summary>
        /// Gets current and max charges for a slot. Only applies to Action type; returns (0,0) otherwise.
        /// </summary>
        private static unsafe (int CurrentCharges, int MaxCharges) GetSlotCharges(RaptureHotbarModule.HotbarSlotType slotType, uint actionId)
        {
            if (slotType != RaptureHotbarModule.HotbarSlotType.Action || actionId == 0)
                return (0, 0);

            var actionManager = ActionManager.Instance();
            if (actionManager == null)
                return (0, 0);

            uint effectiveId = actionManager->GetAdjustedActionId(actionId);
            ushort maxCh = ActionManager.GetMaxCharges(effectiveId, 0);
            if (maxCh <= 1)
                return (0, 0);

            uint currentCh = actionManager->GetCurrentCharges(effectiveId);
            return ((int)currentCh, (int)maxCh);
        }

        private static unsafe (int CooldownPercent, int SecondsLeft) GetSlotCooldown(RaptureHotbarModule.HotbarSlot* slot)
        {
            if (slot == null) return (0, 0);

            var slotType = slot->ApparentSlotType;
            uint actionId = slot->ApparentActionId;
            if (actionId == 0) return GetSlotCooldownFromSlot(slot);

            var actionManager = ActionManager.Instance();
            if (actionManager == null) return GetSlotCooldownFromSlot(slot);

            ActionType? actionType = slotType switch
            {
                RaptureHotbarModule.HotbarSlotType.Action => ActionType.Action,
                RaptureHotbarModule.HotbarSlotType.GeneralAction => ActionType.GeneralAction,
                RaptureHotbarModule.HotbarSlotType.PetAction => ActionType.PetAction,
                RaptureHotbarModule.HotbarSlotType.CraftAction => ActionType.CraftAction,
                RaptureHotbarModule.HotbarSlotType.Item => ActionType.Item,
                _ => null
            };

            if (actionType.HasValue)
            {
                // GetAdjustedActionId resolves Continuation, Egi Assaults, etc. Only applies to Action type
                uint effectiveId = actionType.Value == ActionType.Action
                    ? actionManager->GetAdjustedActionId(actionId)
                    : actionId;
                float total = actionManager->GetRecastTime(actionType.Value, effectiveId);
                float elapsed = actionManager->GetRecastTimeElapsed(actionType.Value, effectiveId);

                if (total > 0.001f && elapsed < total)
                {
                    float remaining = total - elapsed;
                    int pct = (int)Math.Clamp((remaining / total) * 100f, 0, 100);
                    int secsLeft = (int)Math.Ceiling(remaining);
                    return (pct, secsLeft);
                }
                // Cooldown complete (elapsed >= total). Return 0 — do NOT fall through to GetSlotCooldownFromSlot,
                // as the slot can report a stale/false cooldown (e.g. shared recast group or cached state).
                if (total > 0.001f)
                    return (0, 0);
            }

            return GetSlotCooldownFromSlot(slot);
        }

        private static unsafe (int CooldownPercent, int SecondsLeft) GetSlotCooldownFromSlot(RaptureHotbarModule.HotbarSlot* slot)
        {
            if (slot == null) return (0, 0);
            int secsLeft = 0;
            int pct = slot->GetSlotActionCooldownPercentage(&secsLeft, 0);
            return (pct, secsLeft);
        }

        /// <summary>
        /// Returns the default game keybind label for a hotbar slot (Hotbar 1: 1,2,...,0,-,=; Bar 2: Ctrl+1..12; etc.).
        /// hotbarIndex 1–10, slotIndex 0–11. Used to mirror the default hotbar keybind display.
        /// </summary>
        public static string GetDefaultKeybindLabel(int hotbarIndex, int slotIndex)
        {
            int s = Math.Clamp(slotIndex, 0, 11);
            string k = s switch
            {
                0 => "1", 1 => "2", 2 => "3", 3 => "4", 4 => "5", 5 => "6",
                6 => "7", 7 => "8", 8 => "9", 9 => "0", 10 => "-", 11 => "=",
                _ => (s + 1).ToString()
            };
            int b = Math.Clamp(hotbarIndex, 1, 10);
            return b switch
            {
                1 => k,
                2 => "Ctrl+" + k,
                3 => "Shift+" + k,
                4 => "Alt+" + k,
                _ => $"{b}-{s + 1}"
            };
        }

        /// <summary>
        /// Execute a hotbar slot. hotbarIndex 1-10, slotIndex 0-based.
        /// </summary>
        public unsafe bool ExecuteSlot(int hotbarIndex, int slotIndex)
        {
            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady)
                return false;

            int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
            int slot = Math.Clamp(slotIndex, 0, 11);
            module->ExecuteSlotById((uint)barIdx, (uint)slot);
            return true;
        }

        /// <summary>
        /// Swap two hotbar slots. Supports cross-hotbar swap when hotbarA != hotbarB.
        /// Uses CommandType/CommandId from the slot (not Apparent*) so items and macros swap correctly.
        /// hotbarIndex 1-10, slot indices 0-based.
        /// </summary>
        /// <param name="debugLog">When non-null, receives diagnostic info for logging.</param>
        public unsafe bool SwapSlots(int hotbarA, int slotA, int hotbarB, int slotB, Action<string>? debugLog = null)
        {
            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady)
            {
                debugLog?.Invoke($"[SwapSlots] FAIL: module null or not ready");
                return false;
            }

            int barA = Math.Clamp(hotbarA, 1, 10);
            int barB = Math.Clamp(hotbarB, 1, 10);
            int a = Math.Clamp(slotA, 0, 11);
            int b = Math.Clamp(slotB, 0, 11);
            if (barA == barB && a == b)
            {
                debugLog?.Invoke($"[SwapSlots] NOOP: same slot bar={barA} slot={a}");
                return true;
            }

            var slotPtrA = module->GetSlotById((uint)(barA - 1), (uint)a);
            var slotPtrB = module->GetSlotById((uint)(barB - 1), (uint)b);
            if (slotPtrA == null || slotPtrB == null)
            {
                debugLog?.Invoke($"[SwapSlots] FAIL: slotPtr null A={slotPtrA != null} B={slotPtrB != null}");
                return false;
            }

            var typeA = slotPtrA->IsEmpty ? RaptureHotbarModule.HotbarSlotType.Empty : slotPtrA->CommandType;
            var idA = slotPtrA->IsEmpty ? 0u : slotPtrA->CommandId;
            var typeB = slotPtrB->IsEmpty ? RaptureHotbarModule.HotbarSlotType.Empty : slotPtrB->CommandType;
            var idB = slotPtrB->IsEmpty ? 0u : slotPtrB->CommandId;

            debugLog?.Invoke($"[SwapSlots] BEFORE: A(bar{barA} slot{a}) type={typeA} id={idA} | B(bar{barB} slot{b}) type={typeB} id={idB}");

            // Update live slots first (like game drag-drop), then persist
            slotPtrA->Set(typeB, idB);
            slotPtrA->LoadIconId();
            if (typeB == RaptureHotbarModule.HotbarSlotType.Item)
                slotPtrA->LoadCostDataForSlot(true);
            slotPtrB->Set(typeA, idA);
            slotPtrB->LoadIconId();
            if (typeA == RaptureHotbarModule.HotbarSlotType.Item)
                slotPtrB->LoadCostDataForSlot(true);
            SetAndSaveSlotInternal(module, (uint)(barA - 1), (uint)a, typeB, idB, slotPtrA);
            SetAndSaveSlotInternal(module, (uint)(barB - 1), (uint)b, typeA, idA, slotPtrB);

            // Read back to verify
            slotPtrA = module->GetSlotById((uint)(barA - 1), (uint)a);
            slotPtrB = module->GetSlotById((uint)(barB - 1), (uint)b);
            if (slotPtrA != null && slotPtrB != null)
            {
                var afterA = slotPtrA->IsEmpty ? "empty" : $"{slotPtrA->CommandType} id={slotPtrA->CommandId}";
                var afterB = slotPtrB->IsEmpty ? "empty" : $"{slotPtrB->CommandType} id={slotPtrB->CommandId}";
                debugLog?.Invoke($"[SwapSlots] AFTER:  A(bar{barA} slot{a}) {afterA} | B(bar{barB} slot{b}) {afterB}");
            }
            return true;
        }

        /// <summary>Dump HotbarSlot and RaptureMacroModule.Macro memory for a slot (for macro persistence debugging).</summary>
        /// <param name="hotbarIndex">1-10</param>
        /// <param name="slotIndex">0-11</param>
        public unsafe void DumpMacroSlotMemoryToLog(int hotbarIndex, int slotIndex)
        {
            var hotbarModule = RaptureHotbarModule.Instance();
            if (hotbarModule == null || !hotbarModule->ModuleReady)
            {
                Plugin.Logger.Information("[HSUI Macro DBG] RaptureHotbarModule not ready");
                return;
            }
            int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
            int slot = Math.Clamp(slotIndex, 0, 11);
            var slotPtr = hotbarModule->GetSlotById((uint)barIdx, (uint)slot);
            if (slotPtr == null)
            {
                Plugin.Logger.Information($"[HSUI Macro DBG] Bar {hotbarIndex} slot {slotIndex}: slot is null");
                return;
            }
            var ct = slotPtr->CommandType;
            var cid = slotPtr->CommandId;
            Plugin.Logger.Information($"[HSUI Macro DBG] Bar {hotbarIndex} slot {slotIndex}: CommandType={ct} CommandId={cid} ApparentActionId={slotPtr->ApparentActionId} ApparentSlotType={slotPtr->ApparentSlotType} IconId={slotPtr->IconId}");
            if (ct == RaptureHotbarModule.HotbarSlotType.Macro && cid is >= 1 and <= 200)
            {
                byte macroSet = (byte)((cid - 1) / 100);
                byte macroIdx0Based = (byte)((cid - 1) % 100);
                uint macroIdx1Based = (uint)(macroIdx0Based + 1);  // GetMacro expects 1-based index
                var macroModule = RaptureMacroModule.Instance();
                if (macroModule != null)
                {
                    var macro = macroModule->GetMacro(macroSet, macroIdx1Based);
                    if (macro != null)
                    {
                        string name = macro->Name.ToString() ?? "(null)";
                        Plugin.Logger.Information($"[HSUI Macro DBG] RaptureMacroModule.Macro set={macroSet} idx1Based={macroIdx1Based}: IconId={macro->IconId} MacroIconRowId={macro->MacroIconRowId} Name='{name}'");

                        // Raw byte dump (first 0x80 bytes: IconId, MacroIconRowId, start of Name/Utf8String)
                        var sb = new StringBuilder();
                        var ptr = (byte*)macro;
                        for (int i = 0; i < 0x80 && i < 0x688; i += 16)
                        {
                            sb.Clear();
                            sb.Append($"[HSUI Macro DBG]   Macro+0x{i:X3}:");
                            for (int j = 0; j < 16 && i + j < 0x80; j++)
                                sb.Append($" {(ptr[i + j]):X2}");
                            Plugin.Logger.Information(sb.ToString());
                        }

                        // Try AddonMacro _macroName (offset 0x798, Utf8String=0x68 each, 100 entries)
                        try
                        {
                            var addonAddr = Plugin.GameGui?.GetAddonByName("Macro", 1).Address ?? IntPtr.Zero;
                            if (addonAddr != IntPtr.Zero)
                            {
                                var addon = (AddonMacro*)addonAddr;
                                int utf8Size = 0x68;
                                int nameBase = 0x798;
                                var namePtr = (Utf8String*)((byte*)addon + nameBase + macroIdx0Based * utf8Size);
                                string addonName = namePtr->ToString() ?? "(null)";
                                Plugin.Logger.Information($"[HSUI Macro DBG] AddonMacro._macroName[{macroIdx0Based}]: '{addonName}'");
                            }
                            else
                                Plugin.Logger.Information("[HSUI Macro DBG] AddonMacro not found or not open");
                        }
                        catch (Exception ex)
                        {
                            Plugin.Logger.Information($"[HSUI Macro DBG] AddonMacro read failed: {ex.Message}");
                        }

                        // Try AgentMacro SelectedMacroSet/Index (only relevant when that macro is selected)
                        try
                        {
                            var agentModule = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentModule.Instance();
                            if (agentModule != null)
                            {
                                var macroAgent = agentModule->GetAgentByInternalId(FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId.Macro);
                                if (macroAgent != null)
                                {
                                    var agentMacro = (FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentMacro*)macroAgent;
                                    Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro SelectedMacroSet={agentMacro->SelectedMacroSet} SelectedMacroIndex={agentMacro->SelectedMacroIndex}");
                                    if (agentMacro->SelectedMacroSet == macroSet && agentMacro->SelectedMacroIndex == macroIdx1Based)
                                    {
                                        string rawStr = agentMacro->RawMacroString.ToString() ?? "(null)";
                                        Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro (matches): RawMacroString(len={rawStr.Length})='{(rawStr.Length > 60 ? rawStr[..60] + "..." : rawStr)}'");
                                    }
                                }
                                else
                                    Plugin.Logger.Information("[HSUI Macro DBG] AgentMacro agent is null");
                            }
                            else
                                Plugin.Logger.Information("[HSUI Macro DBG] AgentModule.Instance() is null");
                        }
                        catch (Exception ex)
                        {
                            Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro read failed: {ex.Message}");
                        }
                    }
                    else
                        Plugin.Logger.Information($"[HSUI Macro DBG] RaptureMacroModule.GetMacro({macroSet},{macroIdx1Based}) returned null");
                }
                else
                    Plugin.Logger.Information("[HSUI Macro DBG] RaptureMacroModule.Instance() is null");
            }
        }

        /// <summary>Dump AddonMacro and AgentMacro state (open Macro menu first for best data).</summary>
        public unsafe void DumpMacroMenuToLog()
        {
            Plugin.Logger.Information("[HSUI MacroMenu DBG] === Macro menu debug ===");

            // AddonMacro
            try
            {
                var addonAddr = Plugin.GameGui?.GetAddonByName("Macro", 1).Address ?? IntPtr.Zero;
                if (addonAddr != IntPtr.Zero)
                {
                    var addon = (AddonMacro*)addonAddr;
                    Plugin.Logger.Information($"[HSUI MacroMenu DBG] AddonMacro: SelectedPage={addon->SelectedPage} SelectedMacroIndex={addon->SelectedMacroIndex} DefaultIcon={addon->DefaultIcon}");
                    const int utf8Size = 0x68;
                    const int nameBase = 0x798;
                    const int iconBase = 0x604;
                    const int createdBase = 0x3038;
                    for (int i = 0; i < 10; i++)
                    {
                        var namePtr = (Utf8String*)((byte*)addon + nameBase + i * utf8Size);
                        int icon = *(int*)((byte*)addon + iconBase + i * 4);
                        bool created = *((byte*)addon + createdBase + i) != 0;
                        string name = namePtr->ToString() ?? "(empty)";
                        Plugin.Logger.Information($"[HSUI MacroMenu DBG]   AddonMacro[{i}]: Name='{name}' Icon={icon} Created={created}");
                    }
                    Plugin.Logger.Information("[HSUI MacroMenu DBG]   ... (showing first 10 of 100)");
                }
                else
                    Plugin.Logger.Information("[HSUI MacroMenu DBG] AddonMacro not found - open Macro menu (Character Config > Hotbars > Macros) first");
            }
            catch (Exception ex)
            {
                Plugin.Logger.Information($"[HSUI MacroMenu DBG] AddonMacro failed: {ex.Message}");
            }

            // AgentMacro
            try
            {
                var agentModule = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentModule.Instance();
                if (agentModule != null)
                {
                    var macroAgent = agentModule->GetAgentByInternalId(FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId.Macro);
                    if (macroAgent != null)
                    {
                        var agent = (FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentMacro*)macroAgent;
                        Plugin.Logger.Information($"[HSUI MacroMenu DBG] AgentMacro: SelectedMacroSet={agent->SelectedMacroSet} (0=Individual,1=Shared) SelectedMacroIndex={agent->SelectedMacroIndex}");
                        string raw = agent->RawMacroString.ToString() ?? "(null)";
                        string parsed = agent->ParsedMacroString.ToString() ?? "(null)";
                        Plugin.Logger.Information($"[HSUI MacroMenu DBG]   RawMacroString(len={raw.Length}): '{(raw.Length > 80 ? raw[..80] + "..." : raw)}'");
                        Plugin.Logger.Information($"[HSUI MacroMenu DBG]   ParsedMacroString(len={parsed.Length}): '{(parsed.Length > 80 ? parsed[..80] + "..." : parsed)}'");
                        Plugin.Logger.Information($"[HSUI MacroMenu DBG]   MacroIconCount={agent->MacroIconCount}");

                        var clip = &agent->ClipboardMacro;
                        string clipName = clip->Name.ToString() ?? "(null)";
                        Plugin.Logger.Information($"[HSUI MacroMenu DBG]   ClipboardMacro: IconId={clip->IconId} MacroIconRowId={clip->MacroIconRowId} Name='{clipName}'");

                        var rm = RaptureMacroModule.Instance();
                        if (rm != null)
                        {
                            var selectedMacro = rm->GetMacro(agent->SelectedMacroSet, agent->SelectedMacroIndex);
                            if (selectedMacro != null)
                            {
                                string rName = selectedMacro->Name.ToString() ?? "(null)";
                                Plugin.Logger.Information($"[HSUI MacroMenu DBG] RaptureMacroModule.GetMacro({agent->SelectedMacroSet},{agent->SelectedMacroIndex}): IconId={selectedMacro->IconId} MacroIconRowId={selectedMacro->MacroIconRowId} Name='{rName}'");
                            }
                            else
                                Plugin.Logger.Information($"[HSUI MacroMenu DBG] RaptureMacroModule.GetMacro returned null");
                        }
                    }
                    else
                        Plugin.Logger.Information("[HSUI MacroMenu DBG] AgentMacro agent is null");
                }
                else
                    Plugin.Logger.Information("[HSUI MacroMenu DBG] AgentModule.Instance() is null");
            }
            catch (Exception ex)
            {
                Plugin.Logger.Information($"[HSUI MacroMenu DBG] AgentMacro failed: {ex.Message}");
            }

            Plugin.Logger.Information("[HSUI MacroMenu DBG] === end ===");
        }

        /// <summary>Dump all hotbar slot CommandType/CommandId to the log for SwapSlots diagnosis.</summary>
        public unsafe void DumpSlotStateToLog()
        {
            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady) { Plugin.Logger.Information("[HSUI HotbarSlots] Module not ready"); return; }
            for (int bar = 1; bar <= 10; bar++)
            {
                var sb = new System.Text.StringBuilder();
                for (int s = 0; s < 12; s++)
                {
                    var slot = module->GetSlotById((uint)(bar - 1), (uint)s);
                    if (slot == null) { sb.Append("? "); continue; }
                    if (slot->IsEmpty) { sb.Append("-- "); continue; }
                    sb.Append($"{slot->CommandType}:{slot->CommandId} ");
                }
                Plugin.Logger.Information($"[HSUI HotbarSlots] Bar {bar}: {sb}");
            }
        }

        /// <summary>Place a drag-drop payload (game or HSUI) onto a slot. hotbarIndex 1-10, slotIndex 0-based.</summary>
        public static unsafe void PlacePayloadOnSlot(int hotbarIndex, int slotIndex, RaptureHotbarModule.HotbarSlotType slotType, uint commandId)
        {
            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady) return;
            int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
            int slot = Math.Clamp(slotIndex, 0, 11);
            ref var displayBar = ref module->StandardHotbars[barIdx];
            var slotPtr = displayBar.GetHotbarSlot((uint)slot);
            if (slotPtr == null) return;
            slotPtr->Set(slotType, commandId);
            slotPtr->LoadIconId();
            if (slotType == RaptureHotbarModule.HotbarSlotType.Item)
                slotPtr->LoadCostDataForSlot(true);
            SetAndSaveSlotInternal(module, (uint)barIdx, (uint)slot, slotType, commandId, slotPtr);
        }

        public unsafe bool ClearSlot(int hotbarIndex, int slotIndex)
        {
            var module = RaptureHotbarModule.Instance();
            if (module == null || !module->ModuleReady) return false;
            int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
            int slot = Math.Clamp(slotIndex, 0, 11);
            var slotPtr = module->GetSlotById((uint)barIdx, (uint)slot);
            if (slotPtr != null)
            {
                slotPtr->Set(RaptureHotbarModule.HotbarSlotType.Empty, 0);
                slotPtr->LoadIconId();
            }
            SetAndSaveSlotInternal(module, (uint)barIdx, (uint)slot, RaptureHotbarModule.HotbarSlotType.Empty, 0, slotPtr);
            return true;
        }

        /// <summary>Sets a slot and persists to disk. For shared hotbars, explicitly writes to classJobId 0.
        /// For job-specific hotbars, explicitly writes to current class job ID to ensure persistence when switching jobs.</summary>
        public static unsafe void SetAndSaveSlotInternal(RaptureHotbarModule* module, uint barId, uint slotId, RaptureHotbarModule.HotbarSlotType slotType, uint commandId, RaptureHotbarModule.HotbarSlot* slotPtr = null)
        {
            var ptr = slotPtr != null ? slotPtr : module->GetSlotById(barId, slotId);
            if (ptr == null) return;

            if (module->IsHotbarShared(barId))
            {
                // Shared hotbars: explicitly write to classJobId 0 (shared storage) for persistence across job changes and teleports
                module->WriteSavedSlot(0, barId, slotId, ptr, false, false);
            }
            else
            {
                // Job-specific hotbars: explicitly write to current class job ID so saves persist when switching jobs
                uint classJobId = (uint)(module->ActiveHotbarClassJobId & 0x7F); // strip 0x80 flag if set
                if (classJobId == 0)
                {
                    var player = Plugin.ObjectTable?.LocalPlayer;
                    if (player != null)
                        classJobId = player.ClassJob.RowId;
                }
                if (classJobId != 0)
                    module->WriteSavedSlot(classJobId, barId, slotId, ptr, false, false);
            }
            // SetAndSaveSlot updates live slot and triggers file save
            module->SetAndSaveSlot(barId, slotId, slotType, commandId, ignoreSharedHotbars: false, allowSaveToPvP: true);
        }
    }
}
