using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using Dalamud.Game.Chat;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using HellionChat._Helpers;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Util;
using Lumina.Text.Expressions;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
using Microsoft.Extensions.Logging;

namespace HellionChat;

internal class MessageManager : IAsyncDisposable
{
    internal const int MessageDisplayLimit = 10_000;

    private Plugin Plugin { get; }
    private readonly ILogger<MessageManager> _logger;
    internal MessageStore Store { get; }

    private Dictionary<ChatType, NameFormatting> Formats { get; } = [];
    private ulong LastContentId { get; set; }

    // PendingSync (main thread) → PendingAsync (worker thread); LinkedList for O(1) Last access
    private LinkedList<PendingMessage> PendingSync { get; } = [];
    private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
    private readonly Thread PendingMessageThread;
    private readonly CancellationTokenSource PendingThreadCancellationToken = new();

    private Hook<RaptureLogModule.Delegates.AddMsgSourceEntry>? ContentIdResolverHook { get; init; }

    internal ulong CurrentContentId
    {
        get
        {
            var contentId = Plugin.PlayerState.ContentId;
            return contentId == 0 ? LastContentId : contentId;
        }
    }

    // Auto-Tell-Tabs hook: fires after a message is processed and stored, allowing
    // AutoTellTabsService to spawn or refresh temp tabs without coupling.
    public event Action<Message>? MessageProcessed;

    internal unsafe MessageManager(
        Plugin plugin,
        ILogger<MessageManager> logger,
        ILoggerFactory loggerFactory
    )
    {
        Plugin = plugin;
        _logger = logger;

        Store = new MessageStore(
            DatabasePath(),
            Plugin.PlatformUtil,
            loggerFactory.CreateLogger<MessageStore>(),
            loggerFactory
        );

        PendingMessageThread = new Thread(() =>
            ProcessPendingMessages(PendingThreadCancellationToken.Token)
        )
        {
            IsBackground = true,
        };
        PendingMessageThread.Start();

        ContentIdResolverHook =
            Plugin.GameInteropProvider.HookFromAddress<RaptureLogModule.Delegates.AddMsgSourceEntry>(
                RaptureLogModule.MemberFunctionPointers.AddMsgSourceEntry,
                ContentIdResolver
            );
        ContentIdResolverHook.Enable();

        Plugin.ChatGui.ChatMessageUnhandled += ChatMessage;
        Plugin.Framework.Update += OnFrameworkUpdate;
        Plugin.ClientState.Logout += Logout;
    }

    public async ValueTask DisposeAsync()
    {
        ContentIdResolverHook?.Dispose();
        Plugin.ClientState.Logout -= Logout;
        Plugin.Framework.Update -= OnFrameworkUpdate;
        Plugin.ChatGui.ChatMessageUnhandled -= ChatMessage;

        await PendingThreadCancellationToken.CancelAsync();

        // 10s cooperative window; Thread.Abort is gone since .NET 5, so a
        // stuck worker has to ride out the next AppDomain unload.
        var deadline = TimeSpan.FromSeconds(10);
        var stopwatch = Stopwatch.StartNew();
        while (stopwatch.Elapsed < deadline && PendingMessageThread.IsAlive)
            await Task.Delay(100);

        if (PendingMessageThread.IsAlive)
            _logger.LogWarning(
                "PendingMessageThread did not observe cancellation within 10s. "
                    + "Worker remains on background thread; next plugin reload releases it."
            );

        PendingThreadCancellationToken.Dispose();

        Store.Dispose();
    }

    internal static string DatabasePath()
    {
        return Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-sqlite.db");
    }

    private void Logout(int _, int __)
    {
        LastContentId = 0;
    }

    private void OnFrameworkUpdate(IFramework framework)
    {
        var contentId = Plugin.PlayerState.ContentId;
        if (contentId != 0)
            LastContentId = contentId;

        // Drain the PendingSync queue into the PendingAsync queue.
        while (PendingSync.First is { } first)
        {
            PendingSync.RemoveFirst();
            PendingAsync.Enqueue(first.Value);
        }
    }

    private void ProcessPendingMessages(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            if (PendingAsync.TryDequeue(out var pendingMessage))
            {
                try
                {
                    ProcessMessage(pendingMessage);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error processing pending message");
                }
            }
            else
            {
                Thread.Sleep(1);
            }
        }
    }

    internal void ClearAllTabs()
    {
        // TempTabs are session-only (not persisted); exclude them to preserve Tell history
        foreach (var tab in Plugin.Config.Tabs.Where(t => !t.IsTempTab))
            tab.Clear();
    }

    internal void FilterAllTabs()
    {
        DateTimeOffset? since = null;
        if (!Plugin.Config.FilterIncludePreviousSessions)
            since = Plugin.GameStarted;

        using var messages = Store.GetMostRecentMessages(CurrentContentId, since);

        // TempTabs are excluded; they maintain live state from AutoTellTabsService
        var pendingTabs = Plugin
            .Config.Tabs.Where(t => !t.IsTempTab)
            .Select(tab => (tab, new List<Message>()))
            .ToList();
        foreach (var message in messages)
        foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
            pendingMessages.Add(message);

        // Apply messages to chat log all at once.
        foreach (var (tab, pendingMessages) in pendingTabs)
            tab.Messages.AddSortPrune(pendingMessages, MessageDisplayLimit);

        if (!messages.DidError)
            return;

        WrapperUtil.AddNotification(Language.LoadMessages_Error, NotificationType.Error);

        // Mark failed messages as deleted to prevent retry attempts
        var failedIds = messages.FailedMessageIds();
        _logger.LogInformation(
            $"Marking {failedIds.Count} messages as deleted due to parse failures"
        );
        foreach (var msgId in messages.FailedMessageIds())
        {
            _logger.LogDebug($"Marking message '{msgId}' as deleted due to parse failure");
            Store.DeleteMessage(msgId);
        }
    }

    internal void FilterAllTabsAsync()
    {
        Task.Run(() =>
        {
            var stopwatch = Stopwatch.StartNew();
            try
            {
                FilterAllTabs();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in FilterAllTabs");
            }

            // v1.4.9 R3 profiling: Information so the xllog tail surfaces this
            // without a Debug filter. Belt-and-suspenders for future plugin-load
            // regressions; remains in place after Sub-Task 3.4 Befund.
            _logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
        });
    }

    public (SeString? Sender, SeString? Message) LastMessage = (null, null);

    private void ChatMessage(IChatMessage message)
    {
        LastMessage = (message.Sender, message.Message);

        var pendingMessage = new PendingMessage
        {
            ContentId = 0,
            AccountId = 0,
            LogKind = message.LogKind,
            SourceKind = message.SourceKind,
            TargetKind = message.TargetKind,
            Sender = message.Sender,
            Content = message.Message,
        };

        // Update colour codes.
        GlobalParametersCache.Refresh();

        // Delay to next tick to get content ID from ContentIdResolver hook
        PendingSync.AddLast(pendingMessage);
    }

    private unsafe void ContentIdResolver(
        RaptureLogModule* agent,
        ulong contentId,
        ulong accountId,
        int messageIndex,
        ushort worldId,
        ushort chatType
    )
    {
        try
        {
            ContentIdResolverHook?.Original(
                agent,
                contentId,
                accountId,
                messageIndex,
                worldId,
                chatType
            );
            if (PendingSync.Last is not { } last)
                return;

            last.Value.ContentId = contentId;
            last.Value.AccountId = accountId;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in ContentIdResolver");
        }
    }

    private void ProcessMessage(PendingMessage pendingMessage)
    {
        var chatCode = new ChatCode(
            pendingMessage.LogKind,
            pendingMessage.SourceKind,
            pendingMessage.TargetKind
        );

        NameFormatting? formatting = null;
        if (pendingMessage.Sender.Payloads.Count > 0)
            formatting = FormatFor(chatCode.Type);

        var senderChunks = new List<Chunk>();
        if (formatting is { IsPresent: true })
        {
            senderChunks.Add(
                new TextChunk(ChunkSource.None, null, formatting.Before)
                {
                    FallbackColour = chatCode.Type,
                }
            );
            senderChunks.AddRange(
                ChunkUtil.ToChunks(pendingMessage.Sender, ChunkSource.Sender, chatCode.Type)
            );
            senderChunks.Add(
                new TextChunk(ChunkSource.None, null, formatting.After)
                {
                    FallbackColour = chatCode.Type,
                }
            );
        }

        var contentChunks = ChunkUtil
            .ToChunks(pendingMessage.Content, ChunkSource.Content, chatCode.Type)
            .ToList();
        var message = new Message(
            CurrentContentId,
            pendingMessage.ContentId,
            pendingMessage.AccountId,
            chatCode,
            senderChunks,
            contentChunks,
            pendingMessage.Sender,
            pendingMessage.Content
        );

        if (Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle())
            Store.UpsertMessage(message);

        var currentMatches = Plugin.CurrentTab.Matches(message);
        uint? notificationSound = null;
        foreach (var tab in Plugin.Config.Tabs)
        {
            var unread = !(
                tab.UnreadMode == UnreadMode.Unseen && Plugin.CurrentTab != tab && currentMatches
            );

            if (tab.Matches(message))
            {
                tab.AddMessage(message, unread);

                // Per-tab notification sound. Fire once for the first inactive
                // tab that wants it, keeping a message matching several
                // background tabs from stacking sounds.
                // TEST-MIRROR: ../_Helpers/TabSoundDecision.cs
                if (
                    notificationSound is null
                    && TabSoundDecision.ShouldPlay(
                        Plugin.CurrentTab == tab,
                        tab.EnableNotificationSound,
                        Plugin.Config.PlaySounds
                    )
                )
                {
                    notificationSound = tab.NotificationSoundId;
                }
            }
        }

        if (notificationSound is { } soundId)
        {
            if (soundId is >= 1 and <= 16)
            {
                // ProcessMessage runs on the PendingMessageThread worker; the native
                // UIGlobals.PlaySoundEffect must be marshalled onto the framework
                // thread (reference_dalamud_framework_thread).
                Plugin.Framework.RunOnFrameworkThread(() =>
                {
                    unsafe
                    {
                        UIGlobals.PlaySoundEffect(soundId);
                    }
                });
            }
            else if (soundId >= 17)
            {
                // Custom bundled sounds (ids 17-19) go through NAudio WaveOutEvent.
                // NAudio manages its own playback thread, so no framework marshalling needed.
                Plugin.CustomAudioPlayer.Play((int)soundId - 16, Plugin.Config.CustomSoundVolume);
            }
            // soundId == 0 (hand-edited config) falls through: plays nothing.
        }

        MessageProcessed?.Invoke(message);
    }

    internal class NameFormatting
    {
        internal string Before { get; private set; } = string.Empty;
        internal string After { get; private set; } = string.Empty;
        internal bool IsPresent { get; private set; } = true;

        internal static NameFormatting Empty()
        {
            return new NameFormatting { IsPresent = false };
        }

        internal static NameFormatting Of(string before, string after)
        {
            return new NameFormatting { Before = before, After = after };
        }
    }

    private NameFormatting FormatFor(ChatType type)
    {
        if (Formats.TryGetValue(type, out var cached))
            return cached;

        var formats = Sheets.LogKindSheet.GetRow((uint)type).Format.ToList();
        static bool IsStringParam(ReadOnlySePayload payload, byte num)
        {
            if (payload.MacroCode != MacroCode.String)
                return false;

            return payload.TryGetExpression(out var expr1)
                && expr1.TryGetParameterExpression(out var expressionType, out var operand)
                && expressionType == (byte)ExpressionType.LocalString
                && operand.TryGetInt(out var lstrIndex)
                && lstrIndex == num;
        }

        var firstStringParam = formats.FindIndex(payload => IsStringParam(payload, 1));
        var secondStringParam = formats.FindIndex(payload => IsStringParam(payload, 2));

        if (firstStringParam == -1 || secondStringParam == -1)
            return NameFormatting.Empty();

        var before = formats
            .GetRange(0, firstStringParam)
            .Where(payload => payload.Type == ReadOnlySePayloadType.Text)
            .Select(text => Encoding.UTF8.GetString(text.Body.Span));
        var after = formats
            .GetRange(firstStringParam + 1, secondStringParam - firstStringParam)
            .Where(payload => payload.Type == ReadOnlySePayloadType.Text)
            .Select(text => Encoding.UTF8.GetString(text.Body.Span));

        var nameFormatting = NameFormatting.Of(string.Join("", before), string.Join("", after));
        Formats[type] = nameFormatting;

        return nameFormatting;
    }

    private class PendingMessage
    {
        public ulong ContentId; // 0 if unknown
        public ulong AccountId; // 0 if unknown
        public XivChatType LogKind;
        public XivChatRelationKind SourceKind;
        public XivChatRelationKind TargetKind;
        public required SeString Sender;
        public required SeString Content;
    }
}
