using AtkValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace HSUI.Helpers
{
    public readonly struct MainMenuEntry
    {
        public uint CommandId { get; }
        public uint IconId { get; }
        public string Name { get; }

        public MainMenuEntry(uint commandId, uint iconId, string name)
        {
            CommandId = commandId;
            IconId = iconId;
            Name = name ?? "";
        }
    }

    public static class MainMenuHelper
    {
        /// <summary>Names of the 7 top-level Main Menu categories. Clicking these opens the context menu, not a direct action.</summary>
        private static readonly string[] MainMenuCategoryNames =
        {
            "Character", "Duty", "Logs", "Travel", "Party", "Social", "System"
        };

        /// <summary>Fixed icon IDs for categories (from sheet). Used for display; command row is still resolved by name.</summary>
        private static readonly IReadOnlyDictionary<string, uint> FixedCategoryIconIds = new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase)
        {
            { "Duty", 5 },
            { "Logs", 21 },
            { "Travel", 7 },
            { "Social", 20 }
        };

        /// <summary>Categories that must use the category header row (smallest RowId in MainCommandCategory) so the context menu matches the game bar.</summary>
        private static readonly HashSet<string> UseCategoryHeaderForMenu = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "Logs",
            "Travel"
        };

        /// <summary>Submenu item names that uniquely identify the Logs category (from default game context menu).</summary>
        private static readonly string[] LogsCategoryAnchorNames = { "Hunting Log", "Sightseeing Log", "Crafting Log", "Gathering Log", "Fishing Log", "Fish Guide", "Orchestrion List", "Challenge Log" };

        /// <summary>Submenu item names that uniquely identify the Travel category (from default game context menu).</summary>
        private static readonly string[] TravelCategoryAnchorNames = { "Aether Currents", "Mount Speed", "Shared FATE", "Map", "Teleport", "Return" };

        /// <summary>Get the 7 top-level Main Menu categories (Character, Duty, Logs, Travel, Party, Social, System) that are enabled. Clicking these opens the context menu.</summary>
        public static List<MainMenuEntry> GetEnabledMainCommands()
        {
            var list = new List<MainMenuEntry>();
            try
            {
                var sheet = Plugin.DataManager.GetExcelSheet<MainCommand>();
                if (sheet == null) return list;

                return GetEnabledMainCommandsUnsafe(list, sheet);
            }
            catch (Exception ex)
            {
                Plugin.Logger.Warning($"[HSUI] MainMenuHelper.GetEnabledMainCommands: {ex.Message}");
            }

            return list;
        }

        /// <summary>True if the sheet row name matches the main menu category (exact or flexible).</summary>
        private static bool MatchesCategoryName(string rowName, string categoryName)
        {
            if (string.IsNullOrEmpty(rowName)) return false;
            string r = rowName.Trim();
            if (string.Equals(r, categoryName, StringComparison.OrdinalIgnoreCase))
                return true;
            if (string.Equals(categoryName, "Duty", StringComparison.OrdinalIgnoreCase))
                return r.IndexOf("Duty", StringComparison.OrdinalIgnoreCase) >= 0;
            if (string.Equals(categoryName, "Travel", StringComparison.OrdinalIgnoreCase))
                return r.IndexOf("Travel", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Teleport", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Return", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Aetheryte", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Port", StringComparison.OrdinalIgnoreCase) >= 0;
            if (string.Equals(categoryName, "Party", StringComparison.OrdinalIgnoreCase))
                return r.IndexOf("Party", StringComparison.OrdinalIgnoreCase) >= 0;
            if (string.Equals(categoryName, "Logs", StringComparison.OrdinalIgnoreCase))
                return r.IndexOf("Log", StringComparison.OrdinalIgnoreCase) >= 0
                    || string.Equals(r, "Journal", StringComparison.OrdinalIgnoreCase)
                    || r.IndexOf("Quest", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Completion", StringComparison.OrdinalIgnoreCase) >= 0;
            if (string.Equals(categoryName, "Social", StringComparison.OrdinalIgnoreCase))
                return r.IndexOf("Social", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Friend", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Contact", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Linkshell", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Free Company", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Fellowship", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Group", StringComparison.OrdinalIgnoreCase) >= 0;
            if (string.Equals(categoryName, "System", StringComparison.OrdinalIgnoreCase))
                return r.IndexOf("System", StringComparison.OrdinalIgnoreCase) >= 0
                    || r.IndexOf("Config", StringComparison.OrdinalIgnoreCase) >= 0;
            return false;
        }

        /// <summary>Get the category header row ID (smallest RowId in the same MainCommandCategory). For Logs/Travel we identify the category by submenu item names from the default game menu.</summary>
        private static unsafe uint GetCategoryHeaderCommandId(string categoryName, Lumina.Excel.ExcelSheet<MainCommand> sheet, AgentHUD* agentHud)
        {
            uint categoryGroupId = 0;
            string[]? anchorNames = null;
            if (string.Equals(categoryName, "Logs", StringComparison.OrdinalIgnoreCase))
                anchorNames = LogsCategoryAnchorNames;
            else if (string.Equals(categoryName, "Travel", StringComparison.OrdinalIgnoreCase))
                anchorNames = TravelCategoryAnchorNames;

            if (anchorNames != null)
            {
                // Find category by a row that matches one of the known submenu names (e.g. "Hunting Log", "Aether Currents").
                foreach (var row in sheet)
                {
                    uint id = row.RowId;
                    if (id == 0) continue;
                    if (!agentHud->IsMainCommandEnabled(id)) continue;
                    string name = "";
                    try { name = row.Name.ToString(); }
                    catch { }
                    name = name?.Trim() ?? "";
                    foreach (string anchor in anchorNames)
                    {
                        if (name.IndexOf(anchor, StringComparison.OrdinalIgnoreCase) >= 0)
                        {
                            try { categoryGroupId = row.MainCommandCategory.RowId; break; }
                            catch { }
                            break;
                        }
                    }
                    if (categoryGroupId != 0) break;
                }
            }

            if (categoryGroupId == 0)
            {
                // Fallback: exact "Logs"/"Travel" or flexible category name match
                bool exactOnly = string.Equals(categoryName, "Logs", StringComparison.OrdinalIgnoreCase)
                    || string.Equals(categoryName, "Travel", StringComparison.OrdinalIgnoreCase);
                foreach (var row in sheet)
                {
                    uint id = row.RowId;
                    if (id == 0) continue;
                    string name = "";
                    try { name = row.Name.ToString(); }
                    catch { }
                    if (exactOnly && !string.Equals(name.Trim(), categoryName, StringComparison.OrdinalIgnoreCase))
                        continue;
                    if (!exactOnly && !MatchesCategoryName(name, categoryName))
                        continue;
                    if (!agentHud->IsMainCommandEnabled(id))
                        continue;
                    try { categoryGroupId = row.MainCommandCategory.RowId; break; }
                    catch { }
                }
            }
            if (categoryGroupId == 0)
            {
                foreach (var row in sheet)
                {
                    uint id = row.RowId;
                    if (id == 0) continue;
                    string name = "";
                    try { name = row.Name.ToString(); }
                    catch { }
                    if (!MatchesCategoryName(name, categoryName) || !agentHud->IsMainCommandEnabled(id))
                        continue;
                    try { categoryGroupId = row.MainCommandCategory.RowId; break; }
                    catch { }
                }
            }

            if (categoryGroupId == 0) return 0;
            uint headerId = 0;
            foreach (var row in sheet)
            {
                if (row.RowId == 0) continue;
                try { if (row.MainCommandCategory.RowId != categoryGroupId) continue; }
                catch { continue; }
                if (!agentHud->IsMainCommandEnabled(row.RowId)) continue;
                if (headerId == 0 || row.RowId < headerId)
                    headerId = row.RowId;
            }
            return headerId;
        }

        private static unsafe List<MainMenuEntry> GetEnabledMainCommandsUnsafe(List<MainMenuEntry> list, Lumina.Excel.ExcelSheet<MainCommand> sheet)
        {
            var agentHud = AgentHUD.Instance();
            if (agentHud == null) return list;

            foreach (string categoryName in MainMenuCategoryNames)
            {
                // Logs and Travel: use category header row so context menu matches game bar.
                if (UseCategoryHeaderForMenu.Contains(categoryName))
                {
                    uint headerId = GetCategoryHeaderCommandId(categoryName, sheet, agentHud);
                    if (headerId != 0)
                    {
                        uint iconId = 0;
                        if (FixedCategoryIconIds.TryGetValue(categoryName, out uint fixedIconId))
                            iconId = fixedIconId;
                        if (iconId == 0)
                            iconId = GetFallbackIconIdForCategory(categoryName);
                        list.Add(new MainMenuEntry(headerId, iconId, categoryName));
                        continue;
                    }
                }

                foreach (var row in sheet)
                {
                    uint id = row.RowId;
                    if (id == 0) continue;

                    string name = "";
                    try { name = row.Name.ToString(); }
                    catch { }

                    if (!MatchesCategoryName(name, categoryName))
                        continue;
                    if (!agentHud->IsMainCommandEnabled(id))
                        continue;

                    // Icon: use fixed icon ID for Duty/Logs/Travel/Social (from sheet); others use sheet → game → fallback.
                    uint iconId = 0;
                    if (FixedCategoryIconIds.TryGetValue(categoryName, out uint fixedIconId))
                        iconId = fixedIconId;
                    if (iconId == 0)
                    {
                        try { iconId = (uint)row.Icon; }
                        catch { }
                    }
                    if (iconId == 0)
                        iconId = GetIconIdForMainCommand(id);
                    if (iconId == 0)
                        iconId = GetFallbackIconIdForCategory(categoryName);

                    list.Add(new MainMenuEntry(id, iconId, categoryName));
                    break;
                }
            }

            return list;
        }

        /// <summary>Resolve icon for a Main Command. Uses RaptureHotbarModule when sheet row.Icon is 0.</summary>
        public static unsafe uint GetIconIdForMainCommand(uint commandId)
        {
            try
            {
                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(RaptureHotbarModule.HotbarSlotType.MainCommand, commandId);
                        if (gameIcon > 0) return (uint)gameIcon;
                    }
                }
            }
            catch (Exception ex)
            {
                Plugin.Logger.Warning($"[HSUI] MainMenuHelper.GetIconIdForMainCommand({commandId}): {ex.Message}");
            }
            return 0;
        }

        /// <summary>Fallback icon when game and sheet both return 0. Duty = 61002 (exclamation) to match default bar.</summary>
        private static uint GetFallbackIconIdForCategory(string categoryName)
        {
            return (categoryName?.ToLowerInvariant()) switch
            {
                "character" => 61001,
                "duty" => 61002,
                "logs" => 61003,
                "travel" => 61004,
                "party" => 61005,
                "social" => 61006,
                "system" => 61007,
                _ => 0
            };
        }

        /// <summary>Execute a main command by ID (e.g. Character, Social, Collection).</summary>
        public static unsafe void ExecuteMainCommand(uint commandId)
        {
            try
            {
                var uiModule = Framework.Instance()->GetUIModule();
                if (uiModule == null) return;

                uiModule->ExecuteMainCommand(commandId);
            }
            catch (Exception ex)
            {
                Plugin.Logger.Warning($"[HSUI] MainMenuHelper.ExecuteMainCommand({commandId}): {ex.Message}");
            }
        }

        /// <summary>Open the context submenu for a category, or execute directly if no submenu. When sub-commands exist, the HUD shows a custom context menu; otherwise executes the command.</summary>
        public static unsafe void OpenSubmenuOrExecute(uint categoryCommandId)
        {
            var subIds = GetSubCommandIds(categoryCommandId);
            if (subIds != null && subIds.Count > 0)
                return; // Caller should use GetSubCommandEntries and show custom menu; we don't call OpenSystemMenu (crashes)
            ExecuteMainCommand(categoryCommandId);
        }

        /// <summary>Get sub-command entries (id + display name) for the category. Same options as the default main menu bar context menu. Returns empty if category has no sub-commands.</summary>
        public static List<(uint CommandId, string Name)> GetSubCommandEntries(uint categoryCommandId)
        {
            var list = new List<(uint, string)>();
            try
            {
                var subIds = GetSubCommandIds(categoryCommandId);
                if (subIds == null || subIds.Count == 0) return list;

                var sheet = Plugin.DataManager.GetExcelSheet<MainCommand>();
                if (sheet == null) return list;

                foreach (uint id in subIds)
                {
                    string name = "";
                    if (sheet.TryGetRow(id, out var row))
                    {
                        try { name = row.Name.ToString(); }
                        catch { }
                    }
                    if (string.IsNullOrEmpty(name)) name = $"#{id}";
                    list.Add((id, name));
                }
            }
            catch (Exception ex)
            {
                Plugin.Logger.Warning($"[HSUI] MainMenuHelper.GetSubCommandEntries({categoryCommandId}): {ex.Message}");
            }
            return list;
        }

        /// <summary>Get MainCommand IDs that belong to the same category as the given command (for submenu).</summary>
        private static unsafe List<uint> GetSubCommandIds(uint categoryCommandId)
        {
            var list = new List<uint>();
            try
            {
                var sheet = Plugin.DataManager.GetExcelSheet<MainCommand>();
                if (sheet == null) return list;

                var agentHud = AgentHUD.Instance();
                if (agentHud == null) return list;

                if (!sheet.TryGetRow(categoryCommandId, out var categoryRow)) return list;

                uint categoryGroupId;
                try
                {
                    categoryGroupId = categoryRow.MainCommandCategory.RowId;
                }
                catch { return list; }

                foreach (var row in sheet)
                {
                    if (row.RowId == 0) continue;
                    try
                    {
                        if (row.MainCommandCategory.RowId != categoryGroupId) continue;
                    }
                    catch { continue; }
                    if (!agentHud->IsMainCommandEnabled(row.RowId)) continue;
                    list.Add(row.RowId);
                }
            }
            catch (Exception ex)
            {
                Plugin.Logger.Warning($"[HSUI] MainMenuHelper.GetSubCommandIds({categoryCommandId}): {ex.Message}");
            }
            return list;
        }

        /// <summary>Open the game's system menu with the given MainCommand IDs (context submenu).</summary>
        private static unsafe void OpenSystemMenuWithCommands(List<uint> commandIds)
        {
            if (commandIds == null || commandIds.Count == 0) return;
            if (commandIds.Count > 17) return; // game limit

            var agentHud = AgentHUD.Instance();
            if (agentHud == null) return;

            int count = commandIds.Count;
            int allocSize = 5 + 17 + 18; // atkValueArgs layout: [4]=size, [5..21]=commands, [23..]=optional name strings
            var ptr = (AtkValue*)Marshal.AllocHGlobal(allocSize * sizeof(AtkValue));
            try
            {
                // Zero entire buffer so the game never reads garbage as string pointers (prevents crash in Utf8String.SetString)
                for (int i = 0; i < allocSize; i++)
                {
                    ptr[i].Type = AtkValueType.Null;
                    ptr[i].Int = 0;
                }

                ptr[4].ChangeType(AtkValueType.UInt);
                ptr[4].UInt = (uint)count;

                for (int i = 0; i < count; i++)
                {
                    ptr[5 + i].ChangeType(AtkValueType.Int);
                    ptr[5 + i].Int = (int)commandIds[i];
                }

                agentHud->OpenSystemMenu(ptr, (uint)count);
            }
            finally
            {
                Marshal.FreeHGlobal(new IntPtr(ptr));
            }
        }
    }
}
