using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.ImGuiNotification;
using HellionChat.Code;
using HellionChat.GameFunctions.Types;
using HellionChat.Resources;
using HellionChat.Util;
using Microsoft.Extensions.Logging;

namespace HellionChat;

// Auto-Tell-Tabs: spawns session-only tabs per /tell partner.
// Subscribes to MessageManager.MessageProcessed and ClientState.Logout.
internal sealed class AutoTellTabsService : IDisposable
{
    private readonly Plugin _plugin;
    private readonly MessageManager _messageManager;
    private readonly MessageStore _store;
    private readonly ILogger<AutoTellTabsService> _logger;
    private readonly object _tempTabsLock = new();

    // Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
    // of usage. Separate pool from AutoTellTabsLimit (15) — pinned tabs live
    // in their own bucket. A configurable cap is a vault-backlog anchor for
    // a later cycle if tester feedback demands it.
    internal const int MaxPinnedTempTabs = 5;

    private bool _initialized;

    internal AutoTellTabsService(
        Plugin plugin,
        MessageManager messageManager,
        MessageStore store,
        ILogger<AutoTellTabsService> logger
    )
    {
        _plugin = plugin;
        _messageManager = messageManager;
        _store = store;
        _logger = logger;
    }

    // Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
    // mutate IsPinned or remove tabs — the count adapts automatically.
    // Replaces the F2.1 Interlocked counter because the new pin-state
    // transitions are cold-path and don't need lock-free reads.
    internal int ActiveTempTabCount =>
        Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);

    internal int PinnedTempTabCount => Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);

    internal void Initialize()
    {
        if (_initialized)
        {
            return;
        }

        // Pinned tabs come out of the JSON with TellTarget set but
        // CurrentChannel reset (NonSerialized). Without re-seeding, the chat
        // input has no tell-target on the active pinned tab, and the
        // game-side channel hook only repaints CurrentChannel once the user
        // triggers a /tell or channel switch.
        RehydratePinnedTabs();

        _messageManager.MessageProcessed += HandleTell;
        Plugin.ClientState.Logout += OnLogout;
        _initialized = true;
    }

    private void RehydratePinnedTabs()
    {
        var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
        _logger.LogDebug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found");

        foreach (var tab in Plugin.Config.Tabs)
        {
            if (!TabLifecycleHelpers.IsInPinnedPool(tab))
                continue;

            if (tab.TellTarget is null || !tab.TellTarget.IsSet())
            {
                _logger.LogWarning(
                    $"[Pin] Pinned tab '{tab.Name}' has no usable TellTarget "
                        + $"(Name={tab.TellTarget?.Name ?? "<null>"} World={tab.TellTarget?.World ?? 0}). "
                        + "Chat input on this tab will be empty until the partner sends a tell or you /tell manually."
                );
                continue;
            }

            tab.Channel ??= InputChannel.Tell;
            tab.CurrentChannel.Channel = InputChannel.Tell;
            tab.CurrentChannel.TellTarget = tab.TellTarget.Clone();

            // MessageList is NonSerialized so pinned tabs come back empty.
            // Preload the same history window the spawn path uses so the user
            // sees the recent conversation, not a blank tab.
            PreloadHistory(tab, tab.TellTarget.Name, tab.TellTarget.World, Guid.Empty);

            _logger.LogDebug(
                $"[Pin] Rehydrated '{tab.Name}' -> Tell target {tab.TellTarget.Name}@{tab.TellTarget.World}"
            );
        }
    }

    public void Dispose()
    {
        if (!_initialized)
        {
            return;
        }

        Plugin.ClientState.Logout -= OnLogout;
        _messageManager.MessageProcessed -= HandleTell;
        _initialized = false;
    }

    internal void HandleTell(Message message)
    {
        if (!Plugin.Config.EnableAutoTellTabs)
        {
            return;
        }

        if (
            message.Code.Type != ChatType.TellIncoming
            && message.Code.Type != ChatType.TellOutgoing
        )
        {
            return;
        }

        var partner = ExtractTellPartner(message);
        if (partner == null)
        {
            // Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
            _logger.LogWarning(
                $"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
                    + $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
                    + $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
                    + $"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}"
            );
            return;
        }

        lock (_tempTabsLock)
        {
            var existing = FindTempTab(partner.Value.Name, partner.Value.World);
            if (existing != null)
            {
                // Already routed via MessageManager pipeline. Repair the
                // tell-target if the fallback hit a pinned tab whose
                // TellTarget didn't survive a previous round-trip — keeps
                // FindTempTab fast on the next message.
                if (
                    existing.IsPinned
                    && (existing.TellTarget is null || !existing.TellTarget.IsSet())
                )
                {
                    existing.TellTarget = new TellTarget(
                        partner.Value.Name,
                        partner.Value.World,
                        0,
                        TellReason.Direct
                    );
                    _plugin.SaveConfig();
                }
                return;
            }

            if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
            {
                DropOldestTempTab();
            }

            SpawnTempTab(partner.Value, message);
        }
    }

    private (string Name, uint World)? ExtractTellPartner(Message message)
    {
        if (message.Code.Type == ChatType.TellIncoming)
        {
            // Sender is the partner; check chunks first, then raw SeString as fallback
            var fromSender =
                ChunkUtil.TryGetPlayerPayload(message.Sender)
                ?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
            if (fromSender != null)
            {
                return (fromSender.PlayerName, fromSender.World.RowId);
            }
            return null;
        }

        // Outgoing tell: check content first, then channels's TellTarget as fallback
        var fromContent =
            ChunkUtil.TryGetPlayerPayload(message.Content)
            ?? ChunkUtil.TryGetPlayerPayload(message.ContentSource)
            ?? ChunkUtil.TryGetPlayerPayload(message.Sender)
            ?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
        if (fromContent != null)
        {
            return (fromContent.PlayerName, fromContent.World.RowId);
        }

        var current =
            _plugin.CurrentTab.CurrentChannel.TellTarget
            ?? _plugin.CurrentTab.CurrentChannel.TempTellTarget;
        if (current != null && current.IsSet())
        {
            return (current.Name, current.World);
        }

        return null;
    }

    private static Tab? FindTempTab(string name, uint world)
    {
        var byTarget = Plugin.Config.Tabs.FirstOrDefault(t =>
            t.IsTempTab
            && t.TellTarget != null
            && string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
            && t.TellTarget.World == world
        );
        if (byTarget != null)
            return byTarget;

        // Fallback: match by tab name. Pinned tabs are named via
        // FormatTabName(player, world) at spawn time, so the name is a
        // stable secondary key when TellTarget didn't survive a save/load
        // (older configs from a renamed pin, malformed migrations, etc.).
        var expectedName = FormatTabName(name, world);
        return Plugin.Config.Tabs.FirstOrDefault(t =>
            t.IsTempTab && string.Equals(t.Name, expectedName, StringComparison.OrdinalIgnoreCase)
        );
    }

    internal void DropOldestTempTab()
    {
        // Pinned tabs live in their own bucket (MaxPinnedTempTabs) and are
        // never drop candidates. They leave the bucket only via Unpin or
        // PromoteToPermanent.
        var victim = Plugin
            .Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
            .Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t.Tab))
            .OrderByDescending(t => t.Tab.IsGreeted)
            .ThenBy(t => t.Tab.LastActivity)
            .FirstOrDefault();

        if (victim.Tab == null)
        {
            return;
        }

        // Clean up pop-out window if tab is popped out
        if (victim.Tab.PopOut)
        {
            var popout = _plugin.ChatLogWindow.ActivePopouts.FirstOrDefault(p =>
                p.TabIdentifier == victim.Tab.Identifier
            );
            if (popout != null)
            {
                popout.IsOpen = false;
            }
        }

        Plugin.Config.Tabs.RemoveAt(victim.Index);

        // Re-anchor active tab to avoid silent switch when tab is dropped
        if (victim.Index <= _plugin.LastTab)
        {
            _plugin.WantedTab = 0;
        }
    }

    private void SpawnTempTab((string Name, uint World) partner, Message currentMessage)
    {
        var tab = BuildTempTab(partner.Name, partner.World);

        // Preload history: chronological order with current message already persisted
        PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id);

        tab.AddMessage(currentMessage, unread: true);

        // Open as pop-out if configured (set before Tabs.Add for next render-tick)
        if (Plugin.Config.AutoTellTabsOpenAsPopout)
        {
            tab.PopOut = true;
        }

        Plugin.Config.Tabs.Add(tab);
    }

    private static Tab BuildTempTab(string playerName, uint worldRowId)
    {
        return new Tab
        {
            Name = FormatTabName(playerName, worldRowId),
            IsTempTab = true,
            AllSenderMessages = true,
            TellTarget = new TellTarget(playerName, worldRowId, 0, TellReason.Direct),
            Channel = InputChannel.Tell,
            DisplayTimestamp = true,
            UnreadMode = UnreadMode.Unseen,
            HideWhenInactive = false,
            SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
            {
                [ChatType.TellIncoming] = (ChatSourceExt.All, ChatSourceExt.All),
                [ChatType.TellOutgoing] = (ChatSourceExt.All, ChatSourceExt.All),
            },
        };
    }

    private static string FormatTabName(string playerName, uint worldRowId)
    {
        if (Sheets.WorldSheet.TryGetRow(worldRowId, out var worldRow))
        {
            return $"{playerName}@{worldRow.Name}";
        }
        // Fallback if world lookup misses (rare; only for unseen worlds)
        return $"{playerName}@World{worldRowId}";
    }

    private void PreloadHistory(Tab tab, string senderName, uint senderWorld, Guid currentMessageId)
    {
        var preloadCount = Plugin.Config.AutoTellTabsHistoryPreload;
        if (preloadCount <= 0)
        {
            return;
        }

        try
        {
            // Pull one extra row: current message is already in store and would eat a preload slot
            var history = _store.GetTellHistoryWithSender(
                _messageManager.CurrentContentId,
                senderName,
                senderWorld,
                preloadCount + 1
            );

            var historicMessages = history
                .Where(m => m.Id != currentMessageId)
                .Take(preloadCount)
                .ToList();

            if (historicMessages.Count == 0)
            {
                // No prior tells; leave tab empty to avoid orphaned "history loaded" marker
                return;
            }

            // History is oldest-first; add in order for chronological display
            foreach (var message in historicMessages)
            {
                tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
            }

            // Separator between history and live tell (sorts after history but before current)
            tab.Messages.AddPrune(
                MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator),
                MessageManager.MessageDisplayLimit
            );
        }
        catch (Exception ex)
        {
            // Non-fatal: tab still spawns with visible error notice instead of silent history loss
            _logger.LogError(ex, "[AutoTellTabs] History preload failed");
            tab.Messages.AddPrune(
                MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
                MessageManager.MessageDisplayLimit
            );
        }
    }

    private static Message MakeSystemMarker(string text)
    {
        var seString = new SeStringBuilder().AddText(text).Build();
        var chunks = ChunkUtil.ToChunks(seString, ChunkSource.Content, ChatType.System).ToList();
        var code = new ChatCode((XivChatType)ChatType.System, 0, 0);
        return Message.FakeMessage(chunks, code);
    }

    internal void MarkGreeted(Tab tab)
    {
        SetGreeted(tab, true);
    }

    internal void UnmarkGreeted(Tab tab)
    {
        SetGreeted(tab, false);
    }

    internal bool IsGreeted(Tab tab)
    {
        return tab.IsGreeted;
    }

    private void SetGreeted(Tab tab, bool greeted)
    {
        if (tab == null)
        {
            return;
        }

        lock (_tempTabsLock)
        {
            // Guard against frame-race: sidebar might render a tab already removed by LRU or logout
            if (!Plugin.Config.Tabs.Contains(tab))
            {
                return;
            }

            tab.IsGreeted = greeted;
        }
    }

    private void OnLogout(int type, int code)
    {
        lock (_tempTabsLock)
        {
            // Pinned TempTabs must survive char-switch — that's the whole point
            // of pinning. Only unpinned ones get stripped.
            var lastIndex = _plugin.LastTab;
            var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
            var currentWasUnpinnedTempTab =
                lastIndexValid
                && TabLifecycleHelpers.IsInUnpinnedPool(Plugin.Config.Tabs[lastIndex]);

            var poppedTempTabIds = Plugin
                .Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
                .Select(t => t.Identifier)
                .ToList();
            if (poppedTempTabIds.Count > 0)
            {
                var poppedSet = poppedTempTabIds.ToHashSet();
                foreach (
                    var popout in _plugin
                        .ChatLogWindow.ActivePopouts.Where(p => poppedSet.Contains(p.TabIdentifier))
                        .ToList()
                )
                {
                    popout.IsOpen = false;
                }
            }

            Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);

            // Force switch to tab 0 if active tab was an unpinned temp tab or
            // index is now out of range. Pinned tabs survive — no switch needed.
            var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
            if (currentWasUnpinnedTempTab || !stillValid)
            {
                _plugin.WantedTab = 0;
            }
        }
    }

    internal bool TryPin(Tab tab)
    {
        if (!tab.IsTempTab || tab.IsPinned)
        {
            _logger.LogDebug(
                $"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}"
            );
            return false;
        }

        if (PinnedTempTabCount >= MaxPinnedTempTabs)
        {
            WrapperUtil.AddNotification(
                string.Format(HellionStrings.PinTab_LimitReached, MaxPinnedTempTabs),
                NotificationType.Warning
            );
            return false;
        }

        tab.IsPinned = true;
        _logger.LogDebug(
            $"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}"
        );
        _plugin.SaveConfig();
        return true;
    }

    internal void Unpin(Tab tab)
    {
        if (!tab.IsPinned)
        {
            return;
        }

        // If the unpinned pool is already full, dropping the oldest before
        // flipping the flag avoids counting the just-unpinned tab as a drop
        // candidate.
        if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
        {
            DropOldestTempTab();
        }

        tab.IsPinned = false;
        _logger.LogDebug("[Pin] Unpinned tab '{TabName}'", tab.Name);
        _plugin.SaveConfig();
    }

    internal void PromoteToPermanent(Tab tab)
    {
        if (!tab.IsTempTab)
        {
            return;
        }

        tab.IsTempTab = false;
        tab.IsPinned = false;
        tab.TellTarget = TellTarget.Empty();
        _logger.LogDebug($"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)");
        _plugin.SaveConfig();
    }
}
