using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using HellionChat.Code;
using HellionChat.Util;

namespace HellionChat;

public partial class Message
{
    public Guid Id { get; } = Guid.NewGuid();
    public ulong Receiver { get; }
    public ulong ContentId { get; set; }
    public ulong AccountId { get; set; } // 0 if not set

    public DateTimeOffset Date { get; }
    public ChatCode Code { get; }
    public List<Chunk> Sender { get; }
    public List<Chunk> Content { get; private set; }

    public SeString SenderSource { get; }
    public SeString ContentSource { get; }

    public int SortCodeV2 { get; }
    public Guid ExtraChatChannel { get; }

    // Not stored in the database:
    public int Hash { get; }
    public Dictionary<Guid, float?> Height { get; } = new();
    public Dictionary<Guid, bool> IsVisible { get; } = new();

    public Message(
        ulong receiver,
        ulong contentId,
        ulong accountId,
        ChatCode code,
        List<Chunk> sender,
        List<Chunk> content,
        SeString senderSource,
        SeString contentSource
    )
    {
        var extraChatChannel = ExtractExtraChatChannel(contentSource);
        Receiver = receiver;
        ContentId = contentId;
        AccountId = accountId;
        Date = DateTimeOffset.UtcNow;
        Code = code;
        Sender = sender;
        Content = CheckMessageContent(content, extraChatChannel);
        SenderSource = senderSource;
        ContentSource = contentSource;
        SortCodeV2 = Code.ToSortCodeV2();
        ExtraChatChannel = extraChatChannel;
        Hash = GenerateHash();

        // Iterate the processed Content list (returned by CheckMessageContent)
        // rather than the raw constructor parameter — chunks added or replaced
        // by CheckMessageContent must also have their back-reference set.
        foreach (var chunk in Sender.Concat(Content))
            chunk.Message = this;
    }

    public Message(
        Guid id,
        ulong receiver,
        ulong contentId,
        DateTimeOffset date,
        ChatCode code,
        List<Chunk> sender,
        List<Chunk> content,
        SeString senderSource,
        SeString contentSource,
        Guid extraChatChannel
    )
    {
        Id = id;
        Receiver = receiver;
        ContentId = contentId;
        Date = date;
        Code = code;
        Sender = sender;
        // Don't call ReplaceContentURLs here since we're loading the message
        // from the database, and it should already have parsed URL data.
        Content = content;
        SenderSource = senderSource;
        ContentSource = contentSource;
        SortCodeV2 = code.ToSortCodeV2();
        ExtraChatChannel = extraChatChannel;
        Hash = GenerateHash();

        foreach (var chunk in sender.Concat(content))
            chunk.Message = this;
    }

    public static Message FakeMessage(List<Chunk> content, ChatCode code)
    {
        return new Message(0, 0, 0, code, [], content, new SeString(), new SeString());
    }

    public bool Matches(
        Dictionary<ChatType, (ChatSource Source, ChatSource Target)> channels,
        bool allExtraChatChannels,
        HashSet<Guid> extraChatChannels
    )
    {
        if (ExtraChatChannel != Guid.Empty)
            return allExtraChatChannels || extraChatChannels.Contains(ExtraChatChannel);

        var source = (ChatSource)(1 << (int)Code.Source);
        var target = (ChatSource)(1 << (int)Code.Target);
        return Code.Type.IsGm()
            || channels.TryGetValue(Code.Type, out var sources)
                && (
                    Code.Source is 0
                    || sources.Source.HasFlag(source)
                    || sources.Target.HasFlag(target)
                );
    }

    private int GenerateHash()
    {
        var hash =
            SortCodeV2.GetHashCode()
            ^ ExtraChatChannel.GetHashCode()
            ^ string.Join("", Sender.Select(c => c.StringValue())).GetHashCode()
            ^ string.Join("", Content.Select(c => c.StringValue())).GetHashCode();

        if (Plugin.Config.CollapseKeepUniqueLinks)
        {
            // Hash the link too for something like DeathRecap where the message is the same
            // but the link is different
            hash ^= string.Join("", Content.Select(c => c.Link?.GetHashCode())).GetHashCode();
        }

        return hash;
    }

    private static Guid ExtractExtraChatChannel(SeString contentSource)
    {
        if (contentSource.Payloads.Count > 0 && contentSource.Payloads[0] is RawPayload raw)
        {
            // this does an encode and clone every time it's accessed, so cache
            var data = raw.Data;
            try
            {
                if (data[1] == 0x27 && data[2] == 18 && data[3] == 0x20)
                    return new Guid(data[4..^1]);
            }
            catch (ArgumentException ex)
            {
                Plugin.LogProxy.Error(ex, "Failed to parse extra chat channel GUID");
                Plugin.LogProxy.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
                return Guid.Empty;
            }
        }

        return Guid.Empty;
    }

    private List<Chunk> CheckMessageContent(List<Chunk> oldChunks, Guid extraChatChannel)
    {
        var newChunks = new List<Chunk>();
        void AddChunkWithMessage(TextChunk chunk)
        {
            if (string.IsNullOrEmpty(chunk.Content))
                return;

            chunk.Message = this;
            newChunks.Add(chunk);
        }

        var nextIsAutoTranslate = false;
        var checkForEmotes =
            (Code.IsPlayerMessage() || extraChatChannel != Guid.Empty) && Plugin.Config.ShowEmotes;
        foreach (var chunk in oldChunks)
        {
            // Use as is if it's not a text chunk, it already has a payload, or is auto translate
            if (chunk is not TextChunk text || chunk.Link != null || nextIsAutoTranslate)
            {
                nextIsAutoTranslate =
                    chunk is IconChunk { Icon: BitmapFontIcon.AutoTranslateBegin };

                // No need to call AddChunkWithMessage here since the chunk
                // already has the Message field set.
                newChunks.Add(chunk);
                continue;
            }

            var wordBuilder = new StringBuilder();
            var sentenceBuilder = new StringBuilder();
            foreach (var token in Tokenizer.PrecedenceBasedRegexTokenizer.Tokenize(text.Content))
            {
                if (token.TokenType == Tokenizer.TokenType.StringValue)
                {
                    wordBuilder.Append(token.Value);
                    continue;
                }

                var word = wordBuilder.ToString();
                wordBuilder.Clear();

                var wordUsed = false;
                var tokenUsed = false;

                if (
                    checkForEmotes
                    && EmoteCache.Exists(word)
                    && !Plugin.Config.BlockedEmotes.Contains(word)
                )
                {
                    // Add the previous sentence before adding the emote
                    AddChunkWithMessage(text.NewWithStyle(chunk, sentenceBuilder.ToString()));
                    AddChunkWithMessage(
                        new TextChunk(chunk.Source, EmotePayload.ResolveEmote(word), word)
                        {
                            FallbackColour = text.FallbackColour,
                        }
                    );

                    wordUsed = true;
                    sentenceBuilder.Clear();
                }

                if (token.TokenType == Tokenizer.TokenType.UrlString)
                {
                    // Add the previous sentence before adding the url
                    AddChunkWithMessage(
                        text.NewWithStyle(
                            chunk.Source,
                            chunk.Link,
                            sentenceBuilder.Append(!wordUsed ? word : "").ToString()
                        )
                    );
                    try
                    {
                        AddChunkWithMessage(
                            text.NewWithStyle(
                                chunk.Source,
                                UriPayload.ResolveUri(token.Value),
                                token.Value
                            )
                        );
                    }
                    catch (UriFormatException)
                    {
                        AddChunkWithMessage(
                            text.NewWithStyle(chunk.Source, chunk.Link, token.Value)
                        );
                        Plugin.LogProxy.Debug(
                            $"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'"
                        );
                    }

                    wordUsed = true;
                    tokenUsed = true;
                    sentenceBuilder.Clear();
                }

                // Append match if we haven't reached end of string yet
                if (token.TokenType != Tokenizer.TokenType.SequenceTerminator)
                {
                    sentenceBuilder.Append(!wordUsed ? word : "");
                    sentenceBuilder.Append(!tokenUsed ? token.Value : "");
                    continue;
                }

                // End of string reached, we add our leftover
                AddChunkWithMessage(
                    text.NewWithStyle(
                        chunk,
                        sentenceBuilder.Append(!wordUsed ? word : "").ToString()
                    )
                );
            }
        }

        return newChunks;
    }

    public unsafe void DecodeTextParam()
    {
        var newChunks = new List<Chunk>();
        void AddChunkWithMessage(TextChunk chunk)
        {
            if (string.IsNullOrEmpty(chunk.Content))
                return;

            chunk.Message = this;
            newChunks.Add(chunk);
        }

        foreach (var chunk in Content)
        {
            // Use as is if it's not a text chunk or it already has a payload.
            if (chunk is not TextChunk text || chunk.Link != null)
            {
                // No need to call AddChunkWithMessage here since the chunk
                // already has the Message field set.
                newChunks.Add(chunk);
                continue;
            }

            var splits = TextParamRegex().Split(text.Content);
            if (splits.Length == 1)
            {
                newChunks.Add(chunk);
                continue;
            }

            var nextIsMatch = false;
            foreach (var split in splits)
            {
                if (split == "" || !nextIsMatch)
                {
                    nextIsMatch = true;
                    AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
                    continue;
                }

                nextIsMatch = false;
                try
                {
                    if (split == "<item>")
                    {
                        var agentChat = AgentChatLog.Instance();
                        var item = agentChat->LinkedItem;

                        if (item.ItemId == 0)
                        {
                            AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
                            continue;
                        }

                        var kind = item.ItemId switch
                        {
                            < 500_000 => ItemKind.Normal,
                            < 1_000_000 => ItemKind.Collectible,
                            < 2_000_000 => ItemKind.Hq,
                            _ => ItemKind.EventItem,
                        };

                        var name =
                            kind != ItemKind.EventItem
                                ? Sheets.ItemSheet.GetRow(item.ItemId).Name.ToString()
                                : Sheets.EventItemSheet.GetRow(item.ItemId).Name.ToString();

                        var link = new ItemPayload(
                            item.ItemId,
                            kind,
                            $"{SeIconChar.LinkMarker.ToIconChar()}{name}"
                        );
                        AddChunkWithMessage(
                            text.NewWithStyle(chunk.Source, link, link.DisplayName ?? "Unknown")
                        );
                    }
                    else if (split == "<status>")
                    {
                        var statusId = AgentChatLog.Instance()->ContextStatusId;
                        if (
                            statusId == 0
                            || !Sheets.StatusSheet.TryGetRow(statusId, out var statusRow)
                        )
                        {
                            AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
                            continue;
                        }

                        var nameValue = statusRow.Name.ToString();
                        var content = statusRow.StatusCategory switch
                        {
                            1 => $"{SeIconChar.Buff.ToIconString()}{nameValue}",
                            2 => $"{SeIconChar.Debuff.ToIconString()}{nameValue}",
                            _ => nameValue,
                        };

                        var link = new StatusPayload(statusId);
                        AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, content));
                    }
                    else if (split == "<flag>")
                    {
                        var agentMap = AgentMap.Instance();
                        if (agentMap->FlagMarkerCount == 0)
                        {
                            AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
                            continue;
                        }

                        var mapCoords = agentMap->FlagMapMarkers[0];
                        var rawX = (int)(
                            MathF.Round(mapCoords.XFloat, 3, MidpointRounding.AwayFromZero) * 1000
                        );
                        var rawY = (int)(
                            MathF.Round(mapCoords.YFloat, 3, MidpointRounding.AwayFromZero) * 1000
                        );

                        var link = new MapLinkPayload(
                            mapCoords.TerritoryId,
                            mapCoords.MapId,
                            rawX,
                            rawY
                        );
                        AddChunkWithMessage(
                            text.NewWithStyle(
                                chunk.Source,
                                link,
                                $"{SeIconChar.LinkMarker.ToIconChar()}{link.PlaceName} {link.CoordinateString}"
                            )
                        );
                    }
                }
                catch (Exception)
                {
                    AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
                    Plugin.LogProxy.Debug($"Failed to parse the text param: '{split}'");
                }
            }
        }

        Content = newChunks;
    }

    [GeneratedRegex("(<item>|<flag>|<status>)")]
    private static partial Regex TextParamRegex();
}
