using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Gui;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.Bars;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LuminaAction = Lumina.Excel.Sheets.Action;

namespace HSUI.Interface.GeneralElements
{
    public class ActionBarsHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig
    {
        private HotbarBarConfig Config => (HotbarBarConfig)_config;

        private bool _wasDragging;
        private uint _dragId;
        private RaptureHotbarModule.HotbarSlotType _dragSlotType;
        private int _pendingSlotIconIndex = -1;
        private uint _pendingSlotIconId;
        private int _pendingSlotIconFramesLeft;
        /// <summary>When we process a game drop on a slot, we must suppress the InvisibleButton "click" that would trigger ExecuteSlot.</summary>
        private int _lastFrameDroppedOnSlot = -1;
        /// <summary>When we had a game drag and the user released, suppress all slot clicks this frame (release is not a click).</summary>
        private bool _suppressSlotClicksAfterDragRelease;

        internal const string SlotPayloadType = "HSUI_HOTBAR_SLOT";
        private const int SlotsPerBar = 12;

        internal static int _imGuiDragSourceSlotId = -1;
        internal static bool _anyHotbarAcceptedDrop;
        internal static int _lastOverlayFrame = -1;
        /// <summary>Deferred clear when release-outside: set when no overlay accepted, executed next frame.</summary>
        internal static int _pendingReleaseOutsideSlotId = -1;
        /// <summary>To avoid PICKUP log spam: only log when we first start dragging a new slot.</summary>
        internal static int _lastLoggedPickupSlotId = -1;

        /// <summary>Encode hotbar (1-10) and slot (0-11) into internal slot id (0-119). Controller bars use 1-8 and 0-7.</summary>
        internal static int ToSlotId(int hotbarIndex, int slotIndex)
        {
            int bar = Math.Clamp(hotbarIndex, 1, 10) - 1;
            int slot = Math.Clamp(slotIndex, 0, 11);
            return bar * SlotsPerBar + slot;
        }

        /// <summary>Decode internal slot id to hotbar (1-10) and slot (0-11). Returns (-1,-1) if invalid.</summary>
        internal static (int hotbarIndex, int slotIndex) FromSlotId(int slotId)
        {
            if (slotId < 0 || slotId >= 10 * SlotsPerBar) return (-1, -1);
            return (slotId / SlotsPerBar + 1, slotId % SlotsPerBar);
        }

        [StructLayout(LayoutKind.Sequential)]
        internal struct SlotDragPayload
        {
            public int SlotId;
        }

        /// <summary>
        /// Visibility for HSUI Action Bars is configured per hotbar in each Hotbar 1–10 menu.
        /// </summary>
        public VisibilityConfig VisibilityConfig => Config.VisibilityConfig;

        public IGameObject? Actor { get; set; }

        public ActionBarsHud(HotbarBarConfig config, string displayName) : base(config, displayName) { }

        protected override (List<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
        {
            var size = BarSize();
            return (new List<Vector2> { Config.Position }, new List<Vector2> { size });
        }

        private Vector2 BarSize()
        {
            var (cols, rows) = Config.GetLayoutGrid();
            int effectiveCols = Math.Min(cols, Config.SlotCount);
            int effectiveRows = (Config.SlotCount + effectiveCols - 1) / effectiveCols;
            float w = effectiveCols * Config.SlotSize.X + (effectiveCols - 1) * Config.SlotPadding;
            float h = effectiveRows * Config.SlotSize.Y + (effectiveRows - 1) * Config.SlotPadding;
            return new Vector2(w, h);
        }

        private Vector2 GetSlotPosition(int slotIndex, Vector2 slotSize, int pad)
        {
            var (cols, _) = Config.GetLayoutGrid();
            int col = slotIndex % cols;
            int row = slotIndex / cols;
            return new Vector2(col * (slotSize.X + pad), row * (slotSize.Y + pad));
        }

        public override void DrawChildren(Vector2 origin)
        {
            if (!Config.Enabled || Actor == null)
                return;
            // When controller hotbars are effectively enabled (manual or sync with game), normal hotbars are hidden
            if (ControllerHotbarsConfig.GetEffectiveControllerHotbarsEnabled())
                return;

            if (ActionBarsManager.Instance == null)
                return;

            _lastFrameDroppedOnSlot = -1;
            _suppressSlotClicksAfterDragRelease = false;

            var barSize = BarSize();
            var topLeft = Utils.GetAnchoredPosition(origin + Config.Position, barSize, Config.Anchor);
            var slotSize = Config.SlotSize;
            int pad = Config.SlotPadding;
            bool showCd = Config.ShowCooldownOverlay;
            bool showCdNumbers = Config.ShowCooldownNumbers;
            bool showBorder = Config.ShowBorder;

            AddDrawAction(Config.StrataLevel, () =>
            {
                HandleDragDrop(topLeft, barSize, slotSize, pad);
                // Refresh slot data after placement so overwrite shows correctly
                var slots = ActionBarsManager.Instance.GetSlotData(Config.HotbarIndex, Config.SlotCount);

                void DrawBarContent(ImDrawListPtr drawList)
                {
                    using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey))
                    {
                        for (int i = 0; i < slots.Count; i++)
                        {
                            var slot = slots[i];
                            uint displayIconId = slot.IconId;
                            bool isEmpty = slot.IsEmpty;
                            bool usePending = _pendingSlotIconIndex == i && _pendingSlotIconId != 0;
                            if (usePending)
                            {
                                displayIconId = _pendingSlotIconId;
                                isEmpty = false;
                            }

                            var slotOffset = GetSlotPosition(i, slotSize, pad);
                            var pos = topLeft + slotOffset;
                            var size = new Vector2(slotSize.X, slotSize.Y);
                            bool hovered = !isEmpty && ImGui.IsMouseHoveringRect(pos, pos + size);

                            if (isEmpty)
                            {
                                drawList.AddRectFilled(pos, pos + size, 0x22000000);
                                if (showBorder)
                                    drawList.AddRect(pos, pos + size, 0xFF444444);
                            }
                            else
                            {
                                uint color = hovered ? 0xFFFFFFFF : (slot.IsUsable ? 0xFFFFFFFF : 0xAA888888);
                                bool useHighlightBorder = hovered && showBorder;
                                bool isComboNext = Config.ShowComboHighlight && IsComboNextAction(slot);
                                DrawHelper.DrawIcon(displayIconId, pos, size, showBorder && !useHighlightBorder && !isComboNext, color, drawList);

                                if (useHighlightBorder)
                                    drawList.AddRect(pos, pos + size, 0xFF88CCFF, 0, ImDrawFlags.None, 2);
                                else if (isComboNext)
                                {
                                    var ch = Config.ComboHighlightConfig;
                                    DrawHelper.DrawComboHighlightRect(drawList, pos, pos + size,
                                        ch.Color.Base, ch.Thickness, ch.ShowGlow, ch.LineStyle);
                                }

                                if (showCd && slot.CooldownPercent > 0 && _pendingSlotIconIndex != i)
                                {
                                    float total = 100f;
                                    float elapsed = total - slot.CooldownPercent;
                                    DrawHelper.DrawIconCooldown(pos, size, elapsed, total, drawList);
                                    if (showCdNumbers && slot.CooldownSecondsLeft > 0)
                                    {
                                        string cdText = slot.CooldownSecondsLeft.ToString();
                                        Vector2 textSize = ImGui.CalcTextSize(cdText);
                                        Vector2 textPos = pos + (size - textSize) * 0.5f;
                                        DrawHelper.DrawOutlinedText(cdText, textPos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
                                    }
                                }

                                if (Config.ShowKeypressFlash)
                                {
                                    float flashAlpha = InputsHelper.GetKeypressFlashAlpha(Config.HotbarIndex, i, 0.2);
                                    if (flashAlpha > 0.001f)
                                    {
                                        byte a = (byte)(flashAlpha * 0.5f * 255);
                                        drawList.AddRectFilled(pos, pos + size, (uint)(a << 24 | 0xFFFFFF));
                                    }
                                }
                            }

                            if (Config.ShowSlotNumbers && !slot.IsEmpty)
                            {
                                string label = !string.IsNullOrWhiteSpace(slot.KeybindHint)
                                    ? slot.KeybindHint
                                    : ActionBarsManager.GetDefaultKeybindLabel(Config.HotbarIndex, i);
                                Vector2 labelSize = ImGui.CalcTextSize(label);
                                Vector2 labelPos = new Vector2(pos.X + size.X - labelSize.X - 2, pos.Y + size.Y - labelSize.Y - 2);
                                DrawHelper.DrawOutlinedText(label, labelPos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
                            }

                            if (Config.ShowChargeCount && !slot.IsEmpty && slot.MaxCharges > 1)
                            {
                                string chargeText = slot.CurrentCharges.ToString();
                                Vector2 chargeSize = ImGui.CalcTextSize(chargeText);
                                Vector2 chargePos = new Vector2(pos.X + 2, pos.Y + 2);
                                DrawHelper.DrawOutlinedText(chargeText, chargePos, 0xFFFFFFFF, 0xFF000000, drawList, 1);
                            }
                        }
                    }
                }

                if (IsGameDragging())
                {
                    var mp = ImGui.GetMousePos();
                    float hole = 32f;
                    var cursorRect = new ClipRect(mp - new Vector2(hole), mp + new Vector2(hole));
                    var barRect = new ClipRect(topLeft, topLeft + barSize);
                    var inverted = ClipRectsHelper.GetInvertedClipRects(cursorRect);
                    var drawList = ImGui.GetWindowDrawList();
                    for (int i = 0; i < inverted.Length; i++)
                    {
                        var inter = barRect.Intersect(inverted[i]);
                        if (!inter.HasValue) continue;
                        var r = inter.Value;
                        ImGui.PushClipRect(r.Min, r.Max, false);
                        DrawBarContent(drawList);
                        ImGui.PopClipRect();
                    }
                }
                else
                {
                    DrawHelper.DrawInWindow(ID + "_actionbars", topLeft, barSize, false, DrawBarContent);
                }
            });

            // Overlay at HIGHEST so it captures input above the bar; required for icon swap.
            // Draw as a child of HSUI_HUD so hovering does not create a separate toplevel window that hides other HSUI elements.
            AddDrawAction(StrataLevel.HIGHEST, () =>
            {
                var slots = ActionBarsManager.Instance.GetSlotData(Config.HotbarIndex, Config.SlotCount);
                var flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground
                    | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings
                    | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus
                    | ImGuiWindowFlags.NoNav;

                string overlayId = $"HSUI_Hotbar{Config.HotbarIndex}_input";
                ImGui.SetCursorScreenPos(topLeft);
                if (!ImGui.BeginChild(overlayId, barSize, false, flags))
                {
                    ImGui.EndChild();
                    return;
                }

                int frame = ImGui.GetFrameCount();
                if (frame != _lastOverlayFrame)
                {
                    _lastOverlayFrame = frame;
                    _anyHotbarAcceptedDrop = false;
                    // Process deferred release-outside clear from previous frame (must run before overlays)
                    if (_pendingReleaseOutsideSlotId >= 0 && GetHotbarsConfig()?.GeneralOptions?.EnableReleaseOutsideToClear != false)
                    {
                        var (bar, slot) = FromSlotId(_pendingReleaseOutsideSlotId);
                        if (bar >= 1 && slot >= 0)
                            ActionBarsManager.Instance?.ClearSlot(bar, slot);
                        _pendingReleaseOutsideSlotId = -1;
                    }
                }

                bool acceptedDrop = false;
                for (int i = 0; i < Config.SlotCount; i++)
                {
                    var slotOffset = GetSlotPosition(i, slotSize, pad);
                    var slotPos = topLeft + slotOffset;
                    ImGui.SetCursorScreenPos(slotPos);
                    bool clicked = ImGui.InvisibleButton($"##{overlayId}_slot_{i}", slotSize);
                    bool isHovered = ImGui.IsItemHovered();
                    bool isActive = ImGui.IsItemActive();

                    bool shiftHeld = ImGui.GetIO().KeyShift;

                    int targetSlotId = ToSlotId(Config.HotbarIndex, i);

                    if (GetHotbarsConfig()?.GeneralOptions?.EnableShiftDragToRearrange != false && ImGui.BeginDragDropTarget())
                    {
                        var payload = ImGui.AcceptDragDropPayload(SlotPayloadType);
                        if (payload != ImGuiPayloadPtr.Null && payload.IsDataType(SlotPayloadType) && TryGetSlotPayload(payload, out var src))
                        {
                            acceptedDrop = true;
                            _anyHotbarAcceptedDrop = true;
                            _imGuiDragSourceSlotId = -1;
                            _pendingReleaseOutsideSlotId = -1;
                            _lastLoggedPickupSlotId = -1;
                            if (src.SlotId != targetSlotId)
                            {
                                var (srcBar, srcSlot) = FromSlotId(src.SlotId);
                                if (srcBar >= 1 && srcSlot >= 0)
                                {
                                    ActionBarsManager.Instance?.SwapSlots(srcBar, srcSlot, Config.HotbarIndex, i,
                                        Config.DebugDragDrop ? s => Plugin.Logger.Information($"[HSUI DragDrop DBG] {s}") : null);
                                }
                            }
                        }
                        ImGui.EndDragDropTarget();
                    }

                    if (GetHotbarsConfig()?.GeneralOptions?.EnableShiftDragToRearrange != false && !IsGameDragging() && shiftHeld && i < slots.Count && !slots[i].IsEmpty && isActive && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
                    {
                        if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None))
                        {
                            _imGuiDragSourceSlotId = targetSlotId;
                            if (Config.DebugDragDrop && _lastLoggedPickupSlotId != targetSlotId)
                            {
                                _lastLoggedPickupSlotId = targetSlotId;
                                Plugin.Logger.Information($"[HSUI DragDrop DBG] Shift+drag PICKUP: bar={Config.HotbarIndex} slot={i} slotId={targetSlotId}");
                            }
                            SetSlotPayload(new SlotDragPayload { SlotId = targetSlotId });
                            var icon = TexturesHelper.GetTextureFromIconId(slots[i].IconId);
                            if (icon != null)
                                ImGui.Image(icon.Handle, new Vector2(32, 32));
                            ImGui.EndDragDropSource();
                        }
                    }

                    if (clicked && !acceptedDrop && !ImGui.IsMouseDragging(ImGuiMouseButton.Left) && i < slots.Count && !slots[i].IsEmpty)
                    {
                        if (_suppressSlotClicksAfterDragRelease || i == _lastFrameDroppedOnSlot)
                        {
                            if (Config.DebugDragDrop)
                                Plugin.Logger.Information($"[HSUI DragDrop DBG] Suppressed ExecuteSlot slot={i} suppressAfterDrag={_suppressSlotClicksAfterDragRelease} lastDroppedOn={_lastFrameDroppedOnSlot}");
                            _suppressSlotClicksAfterDragRelease = false;
                            _lastFrameDroppedOnSlot = -1;
                        }
                        else if (shiftHeld)
                        {
                            ActionBarsManager.Instance?.ClearSlot(Config.HotbarIndex, i);
                        }
                        else
                        {
                            if (Config.DebugDragDrop)
                                Plugin.Logger.Information($"[HSUI DragDrop DBG] ExecuteSlot hotbar={Config.HotbarIndex} slot={i}");
                            ActionBarsManager.Instance?.ExecuteSlot(Config.HotbarIndex, i);
                        }
                    }

                    if (Config.ShowTooltips && isHovered && i < slots.Count)
                    {
                        var slot = slots[i];
                        if (!slot.IsEmpty)
                        {
                            var (title, text) = GetSlotTooltip(slot);
                            if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text))
                            {
                                string body = string.IsNullOrEmpty(text) ? title : text;
                                var tooltipConfig = ConfigurationManager.Instance?.GetConfigObject<HSUI.Helpers.TooltipsConfig>();
                                var formatted = tooltipConfig != null ? TooltipsHelper.BuildFormattedActionTooltipBody(body, tooltipConfig) : null;
                                TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "", slot.IconId > 0 ? slot.IconId : null, Helpers.TooltipIdKind.Action, formatted);
                                if (IsTooltipDebugEnabled())
                                    Plugin.Logger.Information($"[HSUI Tooltip DBG] ActionBar tooltip (main overlay): slot={i} title='{title}'");
                            }
                        }
                    }
                }

                // Release-outside clear: defer to next frame so we run after all overlays have had a chance to accept
                if (GetHotbarsConfig()?.GeneralOptions?.EnableReleaseOutsideToClear != false && ImGui.GetIO().KeyShift && _imGuiDragSourceSlotId >= 0 &&
                    ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !_anyHotbarAcceptedDrop)
                {
                    if (Config.DebugDragDrop)
                    {
                        var (bar, slot) = FromSlotId(_imGuiDragSourceSlotId);
                        Plugin.Logger.Information($"[HSUI DragDrop DBG] Release outside: defer clear bar={bar} slot={slot}");
                    }
                    _pendingReleaseOutsideSlotId = _imGuiDragSourceSlotId;
                    _imGuiDragSourceSlotId = -1;
                    _lastLoggedPickupSlotId = -1;
                }

                ImGui.EndChild();
            });
        }

        /// <summary>Tooltip-only overlay when click overlay is skipped (proxy on / HUD not locked). Uses NoInputs so it doesn't capture clicks.</summary>
        private void DrawTooltipOnlyOverlay(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad)
        {
            if (!Config.ShowTooltips)
                return;
            var slots = ActionBarsManager.Instance?.GetSlotData(Config.HotbarIndex, Config.SlotCount);
            if (slots == null)
                return;
            var flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground
                | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings
                | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus
                | ImGuiWindowFlags.NoInputs;
            ImGuiHelpers.ForceNextWindowMainViewport();
            ImGui.SetNextWindowPos(topLeft);
            ImGui.SetNextWindowSize(barSize);
            if (!ImGui.Begin(ID + "_actionbars_tooltip_only", flags))
            {
                ImGui.End();
                return;
            }
            for (int i = 0; i < Config.SlotCount && i < slots.Count; i++)
            {
                var slotOffset = GetSlotPosition(i, slotSize, pad);
                var slotPos = topLeft + slotOffset;
                if (ImGui.IsMouseHoveringRect(slotPos, slotPos + slotSize))
                {
                    var slot = slots[i];
                    if (!slot.IsEmpty)
                    {
                        var (title, text) = GetSlotTooltip(slot);
                        if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text))
                        {
                            string body = string.IsNullOrEmpty(text) ? title : text;
                            var tooltipConfig = ConfigurationManager.Instance?.GetConfigObject<HSUI.Helpers.TooltipsConfig>();
                            var formatted = tooltipConfig != null ? TooltipsHelper.BuildFormattedActionTooltipBody(body, tooltipConfig) : null;
                            TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "", slot.IconId > 0 ? slot.IconId : null, Helpers.TooltipIdKind.Action, formatted);
                            if (IsTooltipDebugEnabled())
                                Plugin.Logger.Information($"[HSUI Tooltip DBG] ActionBar tooltip (overlay): slot={i} title='{title}'");
                        }
                    }
                    break;
                }
            }
            ImGui.End();
        }

        private static bool IsTooltipDebugEnabled()
        {
            try
            {
                return ConfigurationManager.Instance?.GetConfigObject<TooltipsConfig>()?.DebugTooltips == true;
            }
            catch { return false; }
        }

        private static (uint LastActionId, string? LastDesc, double LastTime) _tooltipDebugLogState;
        private static bool ShouldLogTooltipDebug(uint actionId, string name, string? desc)
        {
            double now = ImGui.GetTime();
            var (lastId, lastDesc, lastTime) = _tooltipDebugLogState;
            if (actionId == lastId && desc == lastDesc && now - lastTime < 1f)
                return false;
            _tooltipDebugLogState = (actionId, desc, now);
            return true;
        }

        private static bool TryGetDebug()
        {
            try
            {
                var configs = ConfigurationManager.Instance?.GetObjects<HotbarBarConfig>();
                return configs != null && configs.Exists(c => c.DebugDragDrop);
            }
            catch { return false; }
        }

        private static (int Int1, int Int2, double LastTime) _lastItemPayloadLog;
        private static bool ShouldLogItemPayload(int int1, int int2)
        {
            double now = ImGui.GetTime();
            var last = _lastItemPayloadLog;
            if (int1 == last.Int1 && int2 == last.Int2 && now - last.LastTime < 0.5)
                return false;
            _lastItemPayloadLog = (int1, int2, now);
            return true;
        }

        private static (int Int1, int Int2, double LastTime) _lastMacroPayloadLog;
        private static bool ShouldLogMacroPayload(int rawInt1, int rawInt2)
        {
            double now = ImGui.GetTime();
            var last = _lastMacroPayloadLog;
            if (rawInt1 == last.Int1 && rawInt2 == last.Int2 && now - last.LastTime < 2.0)
                return false;
            _lastMacroPayloadLog = (rawInt1, rawInt2, now);
            return true;
        }

        internal static HotbarsConfig? GetHotbarsConfig() =>
            ConfigurationManager.Instance?.GetConfigObject<HotbarsConfig>();

        private static (int Type, int Int1, int Int2, double LastTime) _lastDragNoPayloadLog;
        private static unsafe bool ShouldLogDragNoPayload(AtkDragDropManager* dm)
        {
            if (dm == null) return false;
            var dd = dm->DragDrop1;
            int type = dd != null ? (int)dd->DragDropType : 0;
            int i1 = dm->PayloadContainer.Int1;
            int i2 = dm->PayloadContainer.Int2;
            double now = ImGui.GetTime();
            var last = _lastDragNoPayloadLog;
            if (type == last.Type && i1 == last.Int1 && i2 == last.Int2 && now - last.LastTime < 0.5)
                return false;
            _lastDragNoPayloadLog = (type, i1, i2, now);
            return true;
        }

        internal static unsafe bool IsGameDragging()
        {
            var stage = AtkStage.Instance();
            if (stage == null) return false;
            var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
            return dm->IsDragging;
        }

        internal static unsafe bool TryGetSlotPayload(ImGuiPayloadPtr payload, out SlotDragPayload src)
        {
            if (payload.Data == null || payload.DataSize < sizeof(SlotDragPayload))
            {
                src = default;
                return false;
            }
            src = *(SlotDragPayload*)payload.Data;
            return true;
        }

        internal static void SetSlotPayload(SlotDragPayload pl)
        {
            var type = new ImU8String(SlotPayloadType);
            var data = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref pl, 1));
            ImGui.SetDragDropPayload(type, data, ImGuiCond.None);
            type.Recycle();
        }

        private static bool IsComboNextAction(ActionBarsManager.SlotInfo slot)
        {
            if (slot.IsEmpty || slot.ActionId == 0) return false;
            try
            {
                var slotType = slot.SlotType;
                var actionType = slotType switch
                {
                    RaptureHotbarModule.HotbarSlotType.Action => ActionType.Action,
                    RaptureHotbarModule.HotbarSlotType.GeneralAction => ActionType.GeneralAction,
                    RaptureHotbarModule.HotbarSlotType.CraftAction => ActionType.CraftAction,
                    RaptureHotbarModule.HotbarSlotType.PetAction => ActionType.PetAction,
                    _ => (ActionType?)null
                };
                if (!actionType.HasValue) return false;
                return SpellHelper.Instance.IsActionHighlighted(slot.ActionId, actionType.Value);
            }
            catch { return false; }
        }

        private unsafe void HandleDragDrop(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad)
        {
            bool debug = Config.DebugDragDrop;

            // NOTE: Release-outside clear is done in the overlay only. HandleDragDrop runs before overlay
            // processes AcceptDragDropPayload, so we must not clear here or we'd clear the source before
            // the drop is processed (breaking swap/move when dropping on a slot).

            if (_pendingSlotIconIndex >= 0 && _pendingSlotIconFramesLeft > 0)
            {
                _pendingSlotIconFramesLeft--;
                if (_pendingSlotIconFramesLeft <= 0)
                    _pendingSlotIconIndex = -1;
            }
            else if (_pendingSlotIconIndex >= 0)
            {
                _pendingSlotIconIndex = -1;
            }

            var stage = AtkStage.Instance();
            if (stage == null) return;
            var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
            bool dragging = dm->IsDragging;

            if (dragging)
            {
                if (GetHotbarsConfig()?.GeneralOptions?.EnableDragDropFromGame != false && TryGetDragPayload(out var slotType, out var id))
                {
                    _dragSlotType = slotType;
                    _dragId = id;
                    if (!_wasDragging && debug)
                        Plugin.Logger.Information($"[HSUI DragDrop] Game drag detected, payload: type={slotType} id={id}");
                    _wasDragging = true;

                    var mp = ImGui.GetMousePos();
                    int idx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, false);
                    if (idx >= 0)
                    {
                        InputsHelper.SuppressExecuteSlotByIdAfterDrop((uint)(Config.HotbarIndex - 1), (uint)idx, 500);
                    }

                    if (ImGui.IsMouseReleased(ImGuiMouseButton.Left))
                    {
                        int releaseIdx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, debug);
                        if (releaseIdx >= 0 && slotType != RaptureHotbarModule.HotbarSlotType.Empty && (id != 0 || slotType == RaptureHotbarModule.HotbarSlotType.Macro || slotType == RaptureHotbarModule.HotbarSlotType.GearSet))
                        {
                            if (!ImGui.GetIO().KeyShift)
                            {
                                try
                                {
                                    var module = RaptureHotbarModule.Instance();
                                    if (module != null && module->ModuleReady)
                                    {
                                        uint barId = (uint)(Config.HotbarIndex - 1);
                                        ref var displayBar = ref module->StandardHotbars[(int)barId];
                                        var displaySlot = displayBar.GetHotbarSlot((uint)releaseIdx);
                                        if (displaySlot != null)
                                        {
                                            displaySlot->Set(slotType, id);
                                            displaySlot->LoadIconId();
                                        }
                                        ActionBarsManager.SetAndSaveSlotInternal(module, barId, (uint)releaseIdx, slotType, id, displaySlot);
                                        uint iconId = GetIconIdForPayload(slotType, id);
                                        if (iconId == 0)
                                        {
                                            var bar = module->StandardHotbars[(int)barId];
                                            var writtenSlot = bar.GetHotbarSlot((uint)releaseIdx);
                                            if (writtenSlot != null)
                                                iconId = (uint)writtenSlot->IconId;
                                        }
                                        if (iconId != 0)
                                        {
                                            _pendingSlotIconIndex = releaseIdx;
                                            _pendingSlotIconId = iconId;
                                            _pendingSlotIconFramesLeft = 300;
                                        }
                                        _lastFrameDroppedOnSlot = releaseIdx;
                                        InputsHelper.SuppressExecuteSlotByIdAfterDrop(barId, (uint)releaseIdx, 300);
                                        if (slotType == RaptureHotbarModule.HotbarSlotType.Action ||
                                            slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction ||
                                            slotType == RaptureHotbarModule.HotbarSlotType.CraftAction ||
                                            slotType == RaptureHotbarModule.HotbarSlotType.PetAction)
                                            InputsHelper.SuppressUseActionAfterDrop(id, 300);
                                        if (debug)
                                            Plugin.Logger.Information($"[HSUI DragDrop] Placed {slotType} id={id} on slot {releaseIdx}");
                                    }
                                }
                                catch (Exception ex)
                                {
                                    Plugin.Logger.Warning($"[HSUI DragDrop] Drop failed: {ex.Message}");
                                }
                            }
                            try { dm->CancelDragDrop(true, true); }
                            catch { }
                        }
                        else
                        {
                            try { dm->CancelDragDrop(true, true); }
                            catch { }
                        }
                        _wasDragging = false;
                    }
                }
                else
                {
                    _dragSlotType = RaptureHotbarModule.HotbarSlotType.Empty;
                    _dragId = 0;
                    _wasDragging = true;
                    if (ImGui.IsMouseReleased(ImGuiMouseButton.Left))
                    {
                        try { dm->CancelDragDrop(true, true); }
                        catch { }
                        _wasDragging = false;
                    }
                    if (debug && ShouldLogDragNoPayload(dm))
                    {
                        try
                        {
                            var dd = dm->DragDrop1;
                            if (dd != null)
                                Plugin.Logger.Information($"[HSUI DragDrop DBG] Drag but no payload (clearing cache): DragDropType={dd->DragDropType} Int1={dm->PayloadContainer.Int1} Int2={dm->PayloadContainer.Int2} HoveredItem={Plugin.GameGui?.HoveredItem ?? 0}");
                        }
                        catch { }
                    }
                }
                return;
            }

            if (_wasDragging)
            {
                _suppressSlotClicksAfterDragRelease = true;
                var mp = ImGui.GetMousePos();
                int idx = GetSlotIndexAtPosition(topLeft, barSize, slotSize, pad, Config.SlotCount, Config.GetLayoutGrid().Cols, mp, debug);

                if (idx >= 0)
                {
                    var slotType = _dragSlotType;
                    var id = _dragId;
                    // Only use payload we captured during the drag. Do NOT fall back to TryGetDragPayload here:
                    // after we eat LBUTTONUP, the game drag is cancelled and HoveredAction/HoveredItem can be
                    // stale (e.g. from macro menu UI), causing wrong placements (macro drag -> GeneralAction placed).
                    // If we never got valid payload (e.g. Macro Int2=0), skip the place.
                    bool hasValidPayload = slotType != RaptureHotbarModule.HotbarSlotType.Empty &&
                        (id != 0 || slotType == RaptureHotbarModule.HotbarSlotType.Macro || slotType == RaptureHotbarModule.HotbarSlotType.GearSet);

                    if (hasValidPayload)
                    {
                        bool shiftHeld = ImGui.GetIO().KeyShift;
                        if (shiftHeld)
                        {
                            try { dm->CancelDragDrop(true, true); }
                            catch (Exception ex) { Plugin.Logger.Warning($"[HSUI DragDrop] CancelDragDrop: {ex.Message}"); }
                            ActionBarsManager.Instance?.ClearSlot(Config.HotbarIndex, idx);
                            if (debug)
                                Plugin.Logger.Information($"[HSUI DragDrop] Shift+release: cleared slot {idx}");
                            _lastFrameDroppedOnSlot = idx;
                            _wasDragging = false;
                            return;
                        }

                        try
                        {
                            var module = RaptureHotbarModule.Instance();
                            if (module != null && module->ModuleReady)
                            {
                                uint barId = (uint)(Config.HotbarIndex - 1);
                                ref var displayBar = ref module->StandardHotbars[(int)barId];
                                var displaySlot = displayBar.GetHotbarSlot((uint)idx);
                                if (displaySlot != null)
                                {
                                    displaySlot->Set(slotType, id);
                                    displaySlot->LoadIconId();
                                }
                                ActionBarsManager.SetAndSaveSlotInternal(module, barId, (uint)idx, slotType, id, displaySlot);
                                try { dm->CancelDragDrop(true, true); }
                                catch (Exception ex2) { Plugin.Logger.Warning($"[HSUI DragDrop] CancelDragDrop: {ex2.Message}"); }

                                uint iconId = GetIconIdForPayload(slotType, id);
                                if (iconId == 0)
                                {
                                    var bar = module->StandardHotbars[(int)barId];
                                    var writtenSlot = bar.GetHotbarSlot((uint)idx);
                                    if (writtenSlot != null)
                                        iconId = (uint)writtenSlot->IconId;
                                }
                                if (iconId != 0)
                                {
                                    _pendingSlotIconIndex = idx;
                                    _pendingSlotIconId = iconId;
                                    _pendingSlotIconFramesLeft = 300;
                                }
                                _lastFrameDroppedOnSlot = idx;
                                InputsHelper.SuppressExecuteSlotByIdAfterDrop(barId, (uint)idx, 300);
                                if (slotType == RaptureHotbarModule.HotbarSlotType.Action ||
                                    slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction ||
                                    slotType == RaptureHotbarModule.HotbarSlotType.CraftAction ||
                                    slotType == RaptureHotbarModule.HotbarSlotType.PetAction)
                                    InputsHelper.SuppressUseActionAfterDrop(id, 300);
                                if (debug)
                                    Plugin.Logger.Information($"[HSUI DragDrop] Placed {slotType} id={id} on slot {idx}");
                            }
                        }
                        catch (Exception ex)
                        {
                            Plugin.Logger.Warning($"[HSUI DragDrop] Drop processing failed: {ex.Message}\n{ex.StackTrace}");
                        }
                    }
                    else if (debug)
                    {
                        Plugin.Logger.Information($"[HSUI DragDrop] Release over slot {idx} but invalid payload: type={slotType} id={id}");
                    }
                }
                else if (debug)
                {
                    Plugin.Logger.Information($"[HSUI DragDrop] Release outside bar area (idx={idx})");
                }
                _wasDragging = false;
            }
            else if (!dragging)
            {
                _wasDragging = false;
            }
        }

        private const float DropMargin = 24f;

        private static int GetSlotIndexAtPosition(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad, int slotCount, int cols, Vector2 pos, bool debug = false)
        {
            // topLeft and InvisibleButtons use the same coordinate system as ImGui.SetCursorScreenPos/GetMousePos.
            // Use strict bar bounds (no extended reach) so only one hotbar matches when bars are stacked.
            var viewport = ImGui.GetMainViewport();
            Vector2 adjPos = pos - viewport.Pos;

            int TryHit(Vector2 p)
            {
                float barMinX = topLeft.X;
                float barMaxX = topLeft.X + barSize.X;
                float barMinY = topLeft.Y;
                float barMaxY = topLeft.Y + barSize.Y;
                if (p.X < barMinX || p.X >= barMaxX || p.Y < barMinY || p.Y >= barMaxY)
                    return -1;
                int best = -1;
                float bestDistSq = float.MaxValue;
                for (int i = 0; i < slotCount; i++)
                {
                    int col = i % cols;
                    int row = i / cols;
                    float cx = topLeft.X + col * (slotSize.X + pad) + slotSize.X * 0.5f;
                    float cy = topLeft.Y + row * (slotSize.Y + pad) + slotSize.Y * 0.5f;
                    float dx = p.X - cx, dy = p.Y - cy;
                    float distSq = dx * dx + dy * dy;
                    float slotHalfW = slotSize.X * 0.5f + DropMargin;
                    float slotHalfH = slotSize.Y * 0.5f + DropMargin;
                    bool inSlot = System.Math.Abs(dx) <= slotHalfW && System.Math.Abs(dy) <= slotHalfH;
                    if (inSlot && distSq < bestDistSq) { bestDistSq = distSq; best = i; }
                }
                return best;
            }

            int idx = TryHit(pos);
            if (idx < 0 && adjPos != pos)
                idx = TryHit(adjPos);

            if (debug)
                Plugin.Logger.Information($"[HSUI DragDrop DBG] HitTest: rawMouse=({pos.X:F1},{pos.Y:F1}) viewport.Pos=({viewport.Pos.X:F1},{viewport.Pos.Y:F1}) adjPos=({adjPos.X:F1},{adjPos.Y:F1}) slotIdx={idx}");
            return idx;
        }

        private static unsafe uint GetActiveHotbarClassJobId()
        {
            var module = RaptureHotbarModule.Instance();
            if (module != null)
                return module->ActiveHotbarClassJobId;
            var player = Plugin.ObjectTable?.LocalPlayer;
            return player != null ? (uint)player.ClassJob.RowId : 0;
        }

        /// <summary>
        /// Gets drag payload from game/Dalamud. When game reports Item drag, prioritize HoveredItem.
        /// Used by ActionBarsHud and CrossBarHud for drag-drop.
        /// </summary>
        internal static unsafe bool TryGetDragPayload(out RaptureHotbarModule.HotbarSlotType slotType, out uint id)
        {
            slotType = RaptureHotbarModule.HotbarSlotType.Empty;
            id = 0;
            DragDropType gameType = DragDropType.Nothing;
            try
            {
                var stage = AtkStage.Instance();
                if (stage != null)
                {
                    var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
                    var dd = dm->DragDrop1;
                    if (dd != null)
                        gameType = dd->DragDropType;
                }
            }
            catch { }

            bool isItemDrag = gameType is DragDropType.Item or DragDropType.Inventory_Item or DragDropType.RemoteInventory_Item
                or DragDropType.ActionBar_Item or DragDropType.LetterEditor_Item;

            bool isMacroDrag = gameType is DragDropType.Macro or DragDropType.ActionBar_Macro;
            if (isMacroDrag)
            {
                try
                {
                    var stage = AtkStage.Instance();
                    if (stage != null)
                    {
                        var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
                        int rawInt1 = dm->PayloadContainer.Int1;
                        int rawInt2 = dm->PayloadContainer.Int2;
                        // Int1=1 or 49 -> Individual (set 0), Int1=48 -> Shared (set 1). Int2 = 1-based macro index (1=first).
                        // CommandId: 1–100 Individual, 101–200 Shared. RaptureMacroModule set 0=Individual, set 1=Shared.
                        int set = (rawInt1 == 48) ? 1 : 0;   // 48=Shared, else (1,49,...)=Individual
                        id = (uint)((set * 100) + rawInt2);  // Int2 1-based: 1->id1, 2->id2, 3->id3
                        slotType = RaptureHotbarModule.HotbarSlotType.Macro;
                        if (TryGetDebug() && ShouldLogMacroPayload(rawInt1, rawInt2))
                            Plugin.Logger.Information($"[HSUI DragDrop DBG] Macro payload: Int1={rawInt1} Int2={rawInt2} -> set={set} idx={rawInt2} id={id}");
                        return id is >= 1 and <= 200;
                    }
                }
                catch { }
                return false;
            }

            if (isItemDrag)
            {
                var hi = Plugin.GameGui?.HoveredItem ?? 0;
                if (hi != 0)
                {
                    id = (uint)(hi & 0xFFFFFFFF);
                    slotType = RaptureHotbarModule.HotbarSlotType.Item;
                    return true;
                }
                // HoveredItem can be 0 when game clears it; fallback: resolve from PayloadContainer (Int1=container, Int2=slot)
                if (TryGetItemIdFromInventoryPayload(out id, TryGetDebug()))
                {
                    slotType = RaptureHotbarModule.HotbarSlotType.Item;
                    return true;
                }
                return false;
            }

            if (TryGetDragPayloadFromGame(out slotType, out id))
                return true;
            return TryGetDragPayloadFromDalamud(out slotType, out id);
        }

        /// <summary>Map game PayloadContainer Int1 (agent/UI container id) to InventoryType.</summary>
        private static InventoryType ContainerIdToInventoryType(int containerId)
        {
            return containerId switch
            {
                4 => InventoryType.EquippedItems,
                7 => InventoryType.KeyItems,
                48 => InventoryType.Inventory1,
                49 => InventoryType.Inventory2,
                50 => InventoryType.Inventory3,
                51 => InventoryType.Inventory4,
                52 => InventoryType.RetainerPage1,
                53 => InventoryType.RetainerPage2,
                54 => InventoryType.RetainerPage3,
                55 => InventoryType.RetainerPage4,
                56 => InventoryType.RetainerPage5,
                57 => InventoryType.ArmoryMainHand,
                58 => InventoryType.ArmoryHead,
                59 => InventoryType.ArmoryBody,
                60 => InventoryType.ArmoryHands,
                61 => InventoryType.ArmoryLegs,
                62 => InventoryType.ArmoryFeets,
                63 => InventoryType.ArmoryOffHand,
                64 => InventoryType.ArmoryEar,
                65 => InventoryType.ArmoryNeck,
                66 => InventoryType.ArmoryWrist,
                67 => InventoryType.ArmoryRings,
                68 => InventoryType.ArmorySoulCrystal,
                69 => InventoryType.SaddleBag1,
                70 => InventoryType.SaddleBag2,
                71 => InventoryType.PremiumSaddleBag1,
                72 => InventoryType.PremiumSaddleBag2,
                _ => InventoryType.Invalid
            };
        }

        /// <summary>Resolve visual (containerId, slotIndex) to real (invType, slot) for main inventory via ItemOrderModule.</summary>
        private static unsafe bool TryResolveToRealSlot(int containerId, int visualSlot, out InventoryType realInvType, out int realSlot)
        {
            realInvType = InventoryType.Invalid;
            realSlot = -1;
            try
            {
                var iom = ItemOrderModule.Instance();
                if (iom == null) return false;
                var sorter = iom->InventorySorter;
                if (sorter == null) return false;
                int itemsPerPage = sorter->ItemsPerPage > 0 ? sorter->ItemsPerPage : 35;
                int startIndex = containerId switch { 48 => 0, 49 => itemsPerPage, 50 => itemsPerPage * 2, 51 => itemsPerPage * 3, _ => -1 };
                if (startIndex < 0) return false;
                int displayIndex = startIndex + visualSlot;
                if (displayIndex < 0 || displayIndex >= sorter->Items.LongCount) return false;
                var entry = sorter->Items[displayIndex].Value;
                if (entry == null) return false;
                realInvType = InventoryType.Inventory1 + entry->Page;
                realSlot = entry->Slot;
                return true;
            }
            catch { return false; }
        }

        /// <summary>Resolve item ID from inventory when HoveredItem is 0. PayloadContainer Int1=container, Int2=slot.
        /// Resolves through ItemOrderModule for main inventory (sorted bags) so we get the correct item.</summary>
        private static unsafe bool TryGetItemIdFromInventoryPayload(out uint itemId, bool debug = false)
        {
            itemId = 0;
            try
            {
                var stage = AtkStage.Instance();
                if (stage == null) return false;
                var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
                int containerId = dm->PayloadContainer.Int1;
                int slotIndex = dm->PayloadContainer.Int2;
                InventoryType invType;
                int slot;
                if (containerId is 48 or 49 or 50 or 51 && TryResolveToRealSlot(containerId, slotIndex, out invType, out slot))
                {
                    if (debug && ShouldLogItemPayload(containerId, slotIndex))
                        Plugin.Logger.Information($"[HSUI DragDrop DBG] Item payload: Int1={containerId} Int2={slotIndex} -> resolved to {invType} slot={slot}");
                }
                else
                {
                    invType = ContainerIdToInventoryType(containerId);
                    slot = slotIndex;
                    if (invType == InventoryType.Invalid) return false;
                }
                var inv = InventoryManager.Instance();
                if (inv == null) return false;
                var item = inv->GetInventorySlot(invType, slot);
                if (item == null || item->IsEmpty()) return false;
                itemId = item->GetItemId();
                if (debug && itemId != 0 && ShouldLogItemPayload(containerId, slotIndex))
                    Plugin.Logger.Information($"[HSUI DragDrop DBG] Item from inventory: Int1={containerId} Int2={slotIndex} -> {invType} slot={slot} itemId={itemId}");
                return itemId != 0;
            }
            catch { return false; }
        }

        private static unsafe bool TryGetDragPayloadFromGame(out RaptureHotbarModule.HotbarSlotType slotType, out uint id)
        {
            slotType = RaptureHotbarModule.HotbarSlotType.Empty;
            id = 0;
            try
            {
                var stage = AtkStage.Instance();
                if (stage == null) return false;
                var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
                var dd = dm->DragDrop1;
                if (dd == null) return false;
                slotType = UIGlobals.GetHotbarSlotTypeFromDragDropType(dd->DragDropType);
                id = (uint)dm->PayloadContainer.Int2;
                return slotType != RaptureHotbarModule.HotbarSlotType.Empty;
            }
            catch { return false; }
        }

        private static bool TryGetDragPayloadFromDalamud(out RaptureHotbarModule.HotbarSlotType slotType, out uint id)
        {
            slotType = RaptureHotbarModule.HotbarSlotType.Empty;
            id = 0;
            var ha = Plugin.GameGui?.HoveredAction;
            if (ha != null && ha.ActionKind != HoverActionKind.None && ha.ActionID != 0)
            {
                slotType = GetHotbarSlotTypeFromHoverActionKind(ha.ActionKind);
                if (slotType != RaptureHotbarModule.HotbarSlotType.Empty)
                {
                    id = ha.ActionID;
                    return true;
                }
            }
            var hi = Plugin.GameGui?.HoveredItem ?? 0;
            if (hi != 0)
            {
                id = (uint)(hi & 0xFFFFFFFF);
                slotType = RaptureHotbarModule.HotbarSlotType.Item;
                return true;
            }
            return false;
        }

        /// <summary>Maps HoverActionKind to HotbarSlotType for tooltip lookups. Returns Empty if not supported.</summary>
        public static RaptureHotbarModule.HotbarSlotType GetHotbarSlotTypeForHover(HoverActionKind kind) =>
            GetHotbarSlotTypeFromHoverActionKind(kind);

        /// <summary>
        /// Gets tooltip (title, text, iconId) for an action when hovered in chat or elsewhere.
        /// Used by global tooltip to show action details when hovering over linked actions in chat.
        /// </summary>
        public static (string title, string text, uint iconId)? GetActionTooltipForHover(RaptureHotbarModule.HotbarSlotType slotType, uint actionId)
        {
            if (actionId == 0) return null;
            var slot = new ActionBarsManager.SlotInfo(0, false, true, 0, 0, actionId, slotType, "", 0, 0);
            var (title, text) = GetSlotTooltip(slot);
            if (string.IsNullOrEmpty(title) && string.IsNullOrEmpty(text)) return null;
            uint iconId = GetIconIdForPayload(slotType, actionId);
            return (title, string.IsNullOrEmpty(text) ? title : text, iconId);
        }

        private static RaptureHotbarModule.HotbarSlotType GetHotbarSlotTypeFromHoverActionKind(HoverActionKind kind)
        {
            return kind switch
            {
                HoverActionKind.Action => RaptureHotbarModule.HotbarSlotType.Action,
                HoverActionKind.CraftingAction => RaptureHotbarModule.HotbarSlotType.CraftAction,
                HoverActionKind.GeneralAction => RaptureHotbarModule.HotbarSlotType.GeneralAction,
                HoverActionKind.MainCommand => RaptureHotbarModule.HotbarSlotType.MainCommand,
                HoverActionKind.ExtraCommand => RaptureHotbarModule.HotbarSlotType.ExtraCommand,
                HoverActionKind.Companion => RaptureHotbarModule.HotbarSlotType.Companion,
                HoverActionKind.PetOrder => RaptureHotbarModule.HotbarSlotType.PetAction,
                HoverActionKind.BuddyAction => RaptureHotbarModule.HotbarSlotType.BuddyAction,
                HoverActionKind.Mount => RaptureHotbarModule.HotbarSlotType.Mount,
                HoverActionKind.BgcArmyAction => RaptureHotbarModule.HotbarSlotType.BgcArmyAction,
                HoverActionKind.Perform => RaptureHotbarModule.HotbarSlotType.PerformanceInstrument,
                HoverActionKind.Ornament => RaptureHotbarModule.HotbarSlotType.Ornament,
                HoverActionKind.Glasses => RaptureHotbarModule.HotbarSlotType.Glasses,
                HoverActionKind.MYCTemporaryItem => RaptureHotbarModule.HotbarSlotType.LostFindsItem,
                HoverActionKind.QuickChat => RaptureHotbarModule.HotbarSlotType.PvPQuickChat,
                HoverActionKind.ActionComboRoute => RaptureHotbarModule.HotbarSlotType.PvPCombo,
                _ => RaptureHotbarModule.HotbarSlotType.Empty
            };
        }

        internal static unsafe uint GetIconIdForPayload(RaptureHotbarModule.HotbarSlotType slotType, uint id)
        {
            // id 0 is valid for GearSet (first gearset in list) and Macro; don't short-circuit for those
            if (id == 0 && slotType != RaptureHotbarModule.HotbarSlotType.GearSet && slotType != RaptureHotbarModule.HotbarSlotType.Macro)
                return 0;
            if (slotType == RaptureHotbarModule.HotbarSlotType.Macro)
            {
                var macroModule = RaptureMacroModule.Instance();
                if (macroModule != null && id is >= 1 and <= 200)
                {
                    uint set = (id - 1) / 100;
                    uint index1Based = ((id - 1) % 100) + 1;  // GetMacro expects 1-based index
                    var macro = macroModule->GetMacro(set, index1Based);
                    if (macro != null)
                        return macro->IconId;
                }
                return 0;
            }
            var module = RaptureHotbarModule.Instance();
            if (module != null && module->ModuleReady)
            {
                var bar = module->StandardHotbars[0];
                var slot = bar.GetHotbarSlot(0);
                if (slot != null)
                {
                    int gameIcon = slot->GetIconIdForSlot(slotType, id);
                    if (gameIcon > 0) return (uint)gameIcon;
                }
            }
            try
            {
                switch (slotType)
                {
                    case RaptureHotbarModule.HotbarSlotType.GeneralAction:
                        var gaRow = Plugin.DataManager.GetExcelSheet<GeneralAction>()?.GetRow(id);
                        return gaRow.HasValue ? (uint)gaRow.Value.Icon : 0;
                    case RaptureHotbarModule.HotbarSlotType.Action:
                    case RaptureHotbarModule.HotbarSlotType.CraftAction:
                    case RaptureHotbarModule.HotbarSlotType.PetAction:
                        var action = Plugin.DataManager.GetExcelSheet<LuminaAction>()?.GetRow(id);
                        return action.HasValue ? (uint)action.Value.Icon : 0;
                    case RaptureHotbarModule.HotbarSlotType.Item:
                        uint baseItemId = id >= 1000000 ? id - 1000000 : id;
                        var item = Plugin.DataManager.GetExcelSheet<Item>()?.GetRow(baseItemId);
                        return item.HasValue ? (uint)item.Value.Icon : 0;
                    case RaptureHotbarModule.HotbarSlotType.Mount:
                        var mount = Plugin.DataManager.GetExcelSheet<Mount>()?.GetRow(id);
                        return mount.HasValue ? (uint)mount.Value.Icon : 0;
                    case RaptureHotbarModule.HotbarSlotType.Companion:
                        var companion = Plugin.DataManager.GetExcelSheet<Companion>()?.GetRow(id);
                        return companion.HasValue ? (uint)companion.Value.Icon : 0;
                    case RaptureHotbarModule.HotbarSlotType.Emote:
                        var emote = Plugin.DataManager.GetExcelSheet<Emote>()?.GetRow(id);
                        return emote.HasValue ? (uint)emote.Value.Icon : 0;
                    default:
                        return 0;
                }
            }
            catch { return 0; }
        }

        /// <summary>Public for CrossBarHud and other consumers. Returns (title, body) for a hotbar slot.</summary>
        public static (string title, string text) GetSlotTooltipPublic(ActionBarsManager.SlotInfo slot)
        {
            try
            {
                return GetSlotTooltip(slot);
            }
            catch
            {
                return ("", "");
            }
        }

        private static unsafe (string title, string text) GetSlotTooltip(ActionBarsManager.SlotInfo slot)
        {
            try
            {
                return GetSlotTooltipCore(slot);
            }
            catch
            {
                // Lumina GetRow can throw for invalid/transitional row IDs (e.g. during inventory drag-drop)
                return ("", "");
            }
        }

        private static unsafe (string title, string text) GetSlotTooltipCore(ActionBarsManager.SlotInfo slot)
        {
            // GeneralAction uses a different ID space: GeneralAction 7 = Teleport links to Action 5. Look up via GeneralAction sheet.
            if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.GeneralAction)
            {
                if (Plugin.DataManager.GetExcelSheet<GeneralAction>()?.TryGetRow(slot.ActionId, out var gaRow) == true)
                {
                    string name = gaRow.Name.ToString();
                    string desc = "";
                    try
                    {
                        string descRaw = gaRow.Description.ToDalamudString().ToString();
                        if (!string.IsNullOrEmpty(descRaw))
                        {
                            try
                            {
                                var evaluated = Plugin.SeStringEvaluator.Evaluate(gaRow.Description.AsSpan());
                                desc = evaluated.ExtractText();
                                if (string.IsNullOrEmpty(desc)) desc = descRaw;
                            }
                            catch { desc = descRaw; }
                            if (!string.IsNullOrEmpty(desc))
                                desc = EncryptedStringsHelper.GetString(desc);
                        }
                    }
                    catch { /* ignore */ }
                    string statsLine = "";
                    if (gaRow.Action.RowId != 0)
                    {
                        var actionRow = Plugin.DataManager.GetExcelSheet<LuminaAction>()?.GetRow(gaRow.Action.RowId);
                        if (actionRow.HasValue)
                            statsLine = TryGetActionStatsLine(actionRow.Value, includeRangeAndRadius: false);
                    }
                    if (!string.IsNullOrEmpty(statsLine))
                        desc = string.IsNullOrEmpty(desc) ? statsLine : statsLine + "\n\n" + desc;
                    return (name, desc ?? "");
                }
            }

            if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Action ||
                slot.SlotType == RaptureHotbarModule.HotbarSlotType.CraftAction ||
                slot.SlotType == RaptureHotbarModule.HotbarSlotType.PetAction)
            {
                uint actionIdForLookup = slot.ActionId;
                LuminaAction? actionRow = null;

                var actionSheet = Plugin.DataManager.GetExcelSheet<LuminaAction>();
                if (actionSheet != null)
                {
                    if (actionSheet.TryGetRow(slot.ActionId, out var actionRowOpt))
                    {
                        actionRow = actionRowOpt;
                    }
                    else if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.CraftAction)
                    {
                        // Crafting actions: hotbar may store CraftAction sheet row ID (1-based). Action sheet uses 100000+ range (100001 = Basic Synthesis).
                        const uint CraftActionToActionOffset = 100000;
                        uint mappedId = slot.ActionId + CraftActionToActionOffset;
                        if (actionSheet.TryGetRow(mappedId, out var mappedRow))
                        {
                            actionIdForLookup = mappedId;
                            actionRow = mappedRow;
                        }
                    }
                }

                // CraftAction fallback: when Action sheet lookup failed, try CraftAction sheet directly (hotbar stores CraftAction row ID).
                if (!actionRow.HasValue && slot.SlotType == RaptureHotbarModule.HotbarSlotType.CraftAction)
                {
                    var craftSheet = Plugin.DataManager.GetExcelSheet<Lumina.Excel.Sheets.CraftAction>();
                    if (craftSheet != null && craftSheet.TryGetRow(slot.ActionId, out var craftRow))
                    {
                        try
                        {
                            var nameSe = craftRow.Name;
                            string name = nameSe.ToString();
                            if (string.IsNullOrWhiteSpace(name))
                                name = nameSe.ToDalamudString().ToString();
                            if (!string.IsNullOrWhiteSpace(name))
                            {
                                // CraftAction sheet has its own Description column with the full text
                                string desc = "";
                                try
                                {
                                    var descSe = craftRow.Description;
                                    var descRaw = descSe.ToDalamudString().ToString();
                                    if (!string.IsNullOrEmpty(descRaw))
                                    {
                                        try
                                        {
                                            var evaluated = Plugin.SeStringEvaluator.Evaluate(descSe.AsSpan());
                                            desc = evaluated.ExtractText();
                                            if (string.IsNullOrEmpty(desc)) desc = descRaw;
                                        }
                                        catch { desc = descRaw; }
                                        if (!string.IsNullOrEmpty(desc))
                                            desc = EncryptedStringsHelper.GetString(desc);
                                    }
                                }
                                catch { /* ignore */ }
                                return (name, desc);
                            }
                        }
                        catch { /* ignore */ }
                    }
                }

                var row = actionRow;
                if (row.HasValue)
                {
                    string name = row.Value.Name.ToString();
                    string desc = "";
                    string descRaw = "";
                    // For CraftAction, use CraftAction.Description; ActionTransient returns the name instead of the full description
                    if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.CraftAction)
                    {
                        var craftSheet = Plugin.DataManager.GetExcelSheet<Lumina.Excel.Sheets.CraftAction>();
                        if (craftSheet != null && craftSheet.TryGetRow(actionIdForLookup, out var craftRow))
                        {
                            try
                            {
                                var descSe = craftRow.Description;
                                var craftDescRaw = descSe.ToDalamudString().ToString();
                                if (!string.IsNullOrEmpty(craftDescRaw))
                                {
                                    try
                                    {
                                        var evaluated = Plugin.SeStringEvaluator.Evaluate(descSe.AsSpan());
                                        desc = evaluated.ExtractText();
                                        if (string.IsNullOrEmpty(desc)) desc = craftDescRaw;
                                    }
                                    catch { desc = craftDescRaw; }
                                    if (!string.IsNullOrEmpty(desc))
                                        desc = EncryptedStringsHelper.GetString(desc);
                                }
                            }
                            catch { /* ignore */ }
                        }
                    }
                    else
                    {
                        var descSheet = Plugin.DataManager.GetExcelSheet<ActionTransient>();
                        if (descSheet != null && descSheet.TryGetRow(actionIdForLookup, out var descRow))
                        {
                            try
                            {
                                var descSeStr = descRow.Description;
                                descRaw = descRow.Description.ToDalamudString().ToString();
                                try
                                {
                                    var evaluated = Plugin.SeStringEvaluator.Evaluate(descSeStr.AsSpan());
                                    desc = evaluated.ExtractText();
                                    if (string.IsNullOrEmpty(desc)) desc = descRaw;
                                }
                                catch
                                {
                                    desc = descRaw;
                                }
                                if (!string.IsNullOrEmpty(desc))
                                    desc = EncryptedStringsHelper.GetString(desc);
                            }
                            catch { /* ignore */ }
                        }
                    }
                    bool isCombatAction = IsCombatAction(slot.SlotType, row.Value);
                    int? potencyValue = isCombatAction ? TryGetPotencyValue(row.Value) : null;
                    string potencyLine = potencyValue.HasValue ? $"Potency: {potencyValue.Value}" : "";
                    string statsLine = TryGetActionStatsLine(row.Value, includeRangeAndRadius: isCombatAction);
                    if (!string.IsNullOrEmpty(desc) && potencyValue.HasValue)
                    {
                        string before = desc;
                        desc = ReplacePotencyPlaceholderInDesc(desc, potencyValue.Value);
                        if (IsTooltipDebugEnabled() && before != desc)
                            Plugin.Logger.Information($"[HSUI Tooltip DBG] Replaced potency placeholder: actionId={slot.ActionId} value={potencyValue.Value}");
                    }
                    else if (IsTooltipDebugEnabled() && !string.IsNullOrEmpty(desc) && desc.Contains("potency of", StringComparison.OrdinalIgnoreCase))
                        Plugin.Logger.Information($"[HSUI Tooltip DBG] Potency placeholder NOT replaced: actionId={slot.ActionId} potencyValue={potencyValue?.ToString() ?? "null"} descSnippet='{(desc.Length > 60 ? desc[..60] + "..." : desc)}'");
                    var statsParts = new List<string>();
                    if (!string.IsNullOrEmpty(potencyLine)) statsParts.Add(potencyLine);
                    if (!string.IsNullOrEmpty(statsLine)) statsParts.Add(statsLine);
                    if (statsParts.Count > 0)
                        desc = string.IsNullOrEmpty(desc) ? string.Join("\n", statsParts) : string.Join("\n", statsParts) + "\n\n" + desc;

                    if (IsTooltipDebugEnabled() && ShouldLogTooltipDebug(slot.ActionId, name, desc))
                        Plugin.Logger.Information($"[HSUI Tooltip DBG] GetSlotTooltip actionId={slot.ActionId} name='{name}' descRawLen={descRaw?.Length ?? 0} descLen={desc?.Length ?? 0} descRawPreview='{(descRaw?.Length > 0 ? (descRaw.Length > 50 ? descRaw[..50] + "..." : descRaw) : "(empty)")}' fullBodyPreview='{(desc != null && desc.Length > 100 ? desc[..100] + "..." : desc ?? "")}'");
                    return (name, desc ?? "");
                }
            }

            if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Macro)
            {
                try
                {
                    var macroModule = RaptureMacroModule.Instance();
                    if (macroModule != null && slot.ActionId is >= 1 and <= 200)
                    {
                        uint set = (slot.ActionId - 1) / 100;
                        uint index1Based = ((slot.ActionId - 1) % 100) + 1;  // GetMacro expects 1-based index
                        var macro = macroModule->GetMacro(set, index1Based);
                        if (macro != null)
                        {
                            string name = macro->Name.ToString();
                            if (string.IsNullOrWhiteSpace(name))
                                name = (set == 0 ? "Individual" : "Shared") + " Macro " + index1Based;
                            var sb = new System.Text.StringBuilder();
                            var lineCount = (int)macroModule->GetLineCount(macro);
                            for (int i = 0; i < lineCount && i < 15; i++)
                            {
                                ref var line = ref macro->Lines[i];
                                if (!line.IsEmpty)
                                {
                                    if (sb.Length > 0) sb.Append('\n');
                                    sb.Append(line.ToString());
                                }
                            }
                            string desc = sb.ToString();
                            return (name, desc);
                        }
                    }
                }
                catch { }
                return ("Macro", "");
            }

            if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Mount)
            {
                var row = Plugin.DataManager.GetExcelSheet<Mount>()?.GetRow(slot.ActionId);
                if (row.HasValue)
                {
                    string name = row.Value.Singular.ToString();
                    string desc = "";
                    try
                    {
                        var transient = Plugin.DataManager.GetExcelSheet<MountTransient>()?.GetRow(slot.ActionId);
                        if (transient.HasValue)
                        {
                            string descRaw = transient.Value.Description.ToDalamudString().ToString();
                            if (!string.IsNullOrEmpty(descRaw))
                                desc = EncryptedStringsHelper.GetString(descRaw);
                        }
                    }
                    catch { /* ignore */ }
                    return (name, desc);
                }
            }

            if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Companion)
            {
                var row = Plugin.DataManager.GetExcelSheet<Companion>()?.GetRow(slot.ActionId);
                if (row.HasValue)
                {
                    string name = row.Value.Singular.ToString();
                    string desc = "";
                    try
                    {
                        var transient = Plugin.DataManager.GetExcelSheet<CompanionTransient>()?.GetRow(slot.ActionId);
                        if (transient.HasValue)
                        {
                            string descRaw = transient.Value.Description.ToDalamudString().ToString();
                            if (!string.IsNullOrEmpty(descRaw))
                                desc = EncryptedStringsHelper.GetString(descRaw);
                        }
                    }
                    catch { /* ignore */ }
                    return (name, desc);
                }
            }

            if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Emote)
            {
                var row = Plugin.DataManager.GetExcelSheet<Emote>()?.GetRow(slot.ActionId);
                if (row.HasValue)
                    return (row.Value.Name.ToString(), "");
            }

            if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.Item)
            {
                uint baseItemId = slot.ActionId >= 1000000 ? slot.ActionId - 1000000 : slot.ActionId;
                if (Plugin.DataManager.GetExcelSheet<Item>()?.TryGetRow(baseItemId, out var itemRow) == true)
                {
                    string name = itemRow.Name.ToString();
                    try
                    {
                        string desc = itemRow.Description.ToDalamudString().ToString();
                        if (!string.IsNullOrEmpty(desc))
                            desc = EncryptedStringsHelper.GetString(desc);
                        return (name, desc);
                    }
                    catch
                    {
                        return (name, "");
                    }
                }
            }

            if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.MainCommand ||
                slot.SlotType == RaptureHotbarModule.HotbarSlotType.ExtraCommand)
            {
                if (Plugin.DataManager.GetExcelSheet<MainCommand>()?.TryGetRow(slot.ActionId, out var mcRow) == true)
                {
                    string name = mcRow.Name.ToString();
                    string desc = "";
                    try
                    {
                        string descRaw = mcRow.Description.ToDalamudString().ToString();
                        if (!string.IsNullOrEmpty(descRaw))
                        {
                            try
                            {
                                var evaluated = Plugin.SeStringEvaluator.Evaluate(mcRow.Description.AsSpan());
                                desc = evaluated.ExtractText();
                                if (string.IsNullOrEmpty(desc)) desc = descRaw;
                            }
                            catch { desc = descRaw; }
                            if (!string.IsNullOrEmpty(desc))
                                desc = EncryptedStringsHelper.GetString(desc);
                        }
                    }
                    catch { /* ignore */ }
                    return (name, desc ?? "");
                }
            }

            return (slot.SlotType.ToString(), "");
        }

        private static int? TryGetPotencyValue(LuminaAction action)
        {
            try
            {
                var t = typeof(LuminaAction);
                foreach (var name in new[] { "AttackPotency", "Effect1", "Effect2", "Effect3", "PrimaryEffect", "SecondaryEffect" })
                {
                    var prop = t.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
                    if (prop == null) continue;
                    var v = prop.GetValue(action);
                    if (v is int i && i > 0 && i <= 5000) return i;
                    if (v is uint u && u > 0 && u <= 5000) return (int)u;
                }
                // Fallback: try Omen row - potency may be stored there for weaponskills
                try
                {
                    var omenRef = action.Omen;
                    uint omenRowId = omenRef.RowId;
                    if (omenRowId != 0)
                    {
                        var omenSheet = Plugin.DataManager.GetExcelSheet<Omen>();
                        if (omenSheet != null && omenSheet.GetRow(omenRowId) is { } omenRow)
                        {
                            var ot = typeof(Omen);
                            foreach (var pname in new[] { "Effect1", "Effect2", "Effect3", "AttackPotency" })
                            {
                                var op = ot.GetProperty(pname, BindingFlags.Public | BindingFlags.Instance);
                                if (op == null) continue;
                                var ov = op.GetValue(omenRow);
                                if (ov is int oi && oi >= 50 && oi <= 2000) return oi;
                                if (ov is uint ou && ou >= 50 && ou <= 2000) return (int)ou;
                            }
                        }
                    }
                }
                catch { /* ignore */ }
                // Last resort: scan Action int/uint properties
                foreach (var prop in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
                {
                    if (prop.PropertyType != typeof(int) && prop.PropertyType != typeof(uint)) continue;
                    var name = prop.Name;
                    if (name.Contains("Id") || name.Contains("Index") || name.Contains("Time") ||
                        name.Contains("Range") || name.Contains("Radius") || name.Contains("Cost") ||
                        name.Contains("Level") || name.Contains("Category") || name.Contains("Cast") ||
                        name.Contains("Recast") || name.Contains("Omen"))
                        continue;
                    var v = prop.GetValue(action);
                    if (v is int i && i >= 50 && i <= 2000) return i;
                    if (v is uint u && u >= 50 && u <= 2000) return (int)u;
                }
            }
            catch { /* ignore */ }
            return null;
        }

        private static string ReplacePotencyPlaceholderInDesc(string desc, int potencyValue)
        {
            if (string.IsNullOrEmpty(desc)) return desc;
            string replaced = desc;
            // Game uses placeholder that can render as ".", "?", etc. Try common patterns
            replaced = System.Text.RegularExpressions.Regex.Replace(replaced,
                @"(potency of\s+)[^0-9]{1,4}",
                $"$1{potencyValue}",
                System.Text.RegularExpressions.RegexOptions.IgnoreCase);
            if (replaced == desc && desc.Contains("potency of .", StringComparison.OrdinalIgnoreCase))
            {
                int idx = desc.IndexOf("potency of .", StringComparison.OrdinalIgnoreCase);
                if (idx >= 0)
                    replaced = desc[..(idx + 10)] + potencyValue + desc[(idx + 11)..];
            }
            return replaced;
        }

        /// <summary>Returns true if this action should show combat stats (potency, range, radius). False for Teleport, Return, Sprint, etc.</summary>
        private static bool IsCombatAction(RaptureHotbarModule.HotbarSlotType slotType, LuminaAction action)
        {
            // GeneralAction: Teleport, Return, Sprint, Limit Break - never show combat stats
            if (slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction)
                return false;
            // CraftAction: crafting actions have cast/recast but no combat range/radius
            if (slotType == RaptureHotbarModule.HotbarSlotType.CraftAction)
                return false;
            // PetAction: usually combat, but some pets have non-combat skills - keep combat stats for now
            // Action: check ActionCategory - category 4 (General) and similar are non-combat
            try
            {
                var catRef = action.ActionCategory;
                uint rowId = catRef.RowId;
                if (rowId is 4 or 5) return false;  // General, Other
            }
            catch { /* ignore */ }
            return true;
        }

        private static string TryGetActionStatsLine(LuminaAction action, bool includeRangeAndRadius = true)
        {
            try
            {
                var parts = new List<string>();
                float castSec = action.Cast100ms / 10f;  // Cast100ms/Recast100ms are in 100ms units
                float recastSec = action.Recast100ms / 10f;
                if (castSec > 0)
                    parts.Add($"Cast: {castSec:F1}s");
                else
                    parts.Add("Cast: Instant");
                if (recastSec > 0)
                    parts.Add($"Recast: {recastSec:F1}s");
                if (includeRangeAndRadius)
                {
                    int range = action.Range;
                    int effectRange = action.EffectRange;
                    if (range > 0)
                        parts.Add($"Range: {range}y");
                    if (effectRange > 0)
                        parts.Add($"Radius: {effectRange}y");
                }
                return parts.Count > 0 ? string.Join(" | ", parts) : "";
            }
            catch { return ""; }
        }
    }
}
