using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using Knot.Modules.TelegramImport.Application.Abstractions;
using Knot.Modules.TelegramImport.Application.TelegramImport.DTOs;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Knot.Modules.TelegramImport.Infrastructure.Parser;

public sealed class TelegramHtmlParser : ITelegramHtmlParser
{
    private readonly HtmlParser _parser;
    private readonly ILogger<TelegramHtmlParser> _logger;

    public TelegramHtmlParser(ILogger<TelegramHtmlParser> logger)
    {
        _parser = new HtmlParser();
        _logger = logger;
    }

    public async Task<List<string>> ExtractAllUserNamesAsync(Stream htmlStream, CancellationToken ct = default)
    {
        var doc = await _parser.ParseDocumentAsync(htmlStream, ct);
        var names = new HashSet<string>();
        
        var fromNameNodes = doc.QuerySelectorAll(".message .from_name");
        foreach (var node in fromNameNodes)
        {
            var text = CleanName(node);
            if (!string.IsNullOrWhiteSpace(text)) names.Add(text);
        }
        
        return names.ToList();
    }

    public async Task<List<TelegramMessage>> ParseMessagesAsync(Stream htmlStream, string baseDirInZip, CancellationToken ct = default)
    {
        var doc = await _parser.ParseDocumentAsync(htmlStream, ct);
        var messages = new List<TelegramMessage>();
        string? lastSenderName = null;
        DateTime? lastCreatedAt = null;
        
        var messageNodes = doc.QuerySelectorAll(".message");
        foreach (var node in messageNodes)
        {
            bool isJoined = node.ClassList.Contains("joined");
            var media = ExtractMedia(node, baseDirInZip);
            var textNode = node.QuerySelector(".text");
            var hasText = textNode != null && !string.IsNullOrWhiteSpace(textNode.TextContent);
            
            var dateNode = node.QuerySelector(".date[title]") ?? node.QuerySelector("[title]");
            var dateStr = dateNode?.GetAttribute("title") ?? "";
            DateTime? currentAt = ParseDate(dateStr);

            // REFINED ALBUM MERGING LOGIC:
            // 1. Current is .joined
            // 2. Current has media but NO text (important: if it has text, it's a new bubble group)
            // 3. Sender is the same as previous message
            // 4. Time difference is minimal (<= 2 seconds). Telegram albums usually share the exact same timestamp.
            if (isJoined && media.Count > 0 && !hasText && messages.Count > 0)
            {
                var prev = messages[messages.Count - 1];
                bool isSameSender = lastSenderName != null; // Since it's 'joined', it's the same visual group
                bool isCloseInTime = lastCreatedAt.HasValue && currentAt.HasValue && 
                                     Math.Abs((currentAt.Value - lastCreatedAt.Value).TotalSeconds) <= 2;

                if (isSameSender && isCloseInTime)
                {
                    var mergedMedia = (prev.Media ?? new List<TelegramMedia>()).Concat(media).ToList();
                    messages[messages.Count - 1] = prev with { Media = mergedMedia };
                    continue;
                }
            }

            var msg = ParseSingleMessage(node, baseDirInZip, ref lastSenderName);
            if (msg != null) 
            {
                messages.Add(msg);
                lastCreatedAt = currentAt;
            }
        }

        return messages;
    }

    public async Task<string?> ExtractGroupNameAsync(Stream htmlStream, CancellationToken ct = default)
    {
        var doc = await _parser.ParseDocumentAsync(htmlStream, ct);
        var header = doc.QuerySelector(".page_header .text");
        var name = header?.TextContent?.Trim();
        
        if (string.IsNullOrEmpty(name))
        {
            name = doc.Title?.Replace("Chat Export with ", "")?.Trim();
        }

        return name;
    }

    private TelegramMessage? ParseSingleMessage(IElement node, string baseDir, ref string? lastSenderName)
    {
        if (node.ClassList.Contains("service")) return null;

        try 
        {
            var idAttr = node.GetAttribute("id") ?? Guid.NewGuid().ToString();
            long numericId = 0;
            if (idAttr.StartsWith("message")) long.TryParse(idAttr.Replace("message", ""), out numericId);

            var fromNameNode = node.QuerySelector(".body > .from_name");
            if (fromNameNode != null && fromNameNode.Closest(".forwarded") != null) fromNameNode = null;

            var senderName = fromNameNode != null ? CleanName(fromNameNode) : lastSenderName;
            if (senderName != null && !node.ClassList.Contains("joined")) lastSenderName = senderName;

            var textNode = node.QuerySelector(".text");
            var content = textNode?.TextContent?.Trim() ?? "";
            
            var dateNode = node.QuerySelector(".date[title]") ?? node.QuerySelector("[title]");
            var dateStr = dateNode?.GetAttribute("title") ?? "";
            DateTime createdAt = ParseDate(dateStr) ?? DateTime.UtcNow;

            var mediaList = ExtractMedia(node, baseDir);

            string? replyToId = null;
            var replyNode = node.QuerySelector(".reply_to a[href^=\"#go_to_message\"]");
            if (replyNode != null)
            {
                var href = replyNode.GetAttribute("href");
                replyToId = href?.Replace("#go_to_message", "message");
            }

            string? forwardedFrom = null;
            var forwardNode = node.QuerySelector(".forwarded .from_name");
            if (forwardNode != null) forwardedFrom = CleanName(forwardNode);

            var reactions = new List<TelegramReaction>();
            var reactionNodes = node.QuerySelectorAll(".reactions .reaction");
            foreach (var r in reactionNodes)
            {
                var emoji = r.QuerySelector(".emoji")?.TextContent?.Trim() ?? "";
                var names = r.QuerySelectorAll(".userpics [title]")
                             .Select(x => x.GetAttribute("title") ?? "")
                             .Where(x => !string.IsNullOrEmpty(x))
                             .ToList();
                if (!string.IsNullOrEmpty(emoji)) reactions.Add(new TelegramReaction(emoji, names));
            }

            return new TelegramMessage(idAttr, senderName, createdAt, content, replyToId, forwardedFrom, mediaList.Any() ? mediaList : null, reactions.Any() ? reactions : null, numericId);
        }
        catch (Exception ex)
        { 
            _logger.LogError(ex, "Error parsing single Telegram message");
            return null; 
        }
    }

    private static DateTime? ParseDate(string? dateStr)
    {
        if (string.IsNullOrEmpty(dateStr)) return null;
        var cleanDateStr = dateStr.Trim();
        var formats = new[] { "dd.MM.yyyy HH:mm:ss 'UTC'zzz", "dd.MM.yyyy HH:mm:ss", "d.MM.yyyy HH:mm:ss", "dd.M.yyyy HH:mm:ss", "d.M.yyyy HH:mm:ss" };
        
        if (DateTimeOffset.TryParseExact(cleanDateStr, formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto)) return dto.UtcDateTime;
        if (DateTimeOffset.TryParse(cleanDateStr.Replace("UTC", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto2)) return dto2.UtcDateTime;
        return null;
    }

    private List<TelegramMedia> ExtractMedia(IElement node, string baseDir)
    {
        var mediaList = new List<TelegramMedia>();
        var allLinks = node.QuerySelectorAll("a[href]").ToList();
        
        foreach (var link in allLinks)
        {
            var href = NormalizeHref(link.GetAttribute("href"));
            if (href == null || !href.Contains("/")) continue;

            if (href.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) continue;

            var fullPath = BuildPath(baseDir, href);
            if (mediaList.Any(m => m.FilePath == fullPath)) continue;

            // Priority 1: Extract real filename from .title or .name or .description or specific body part
            string fileName = "";
            var titleNode = link.QuerySelector(".title") ?? link.QuerySelector(".name") ?? link.QuerySelector(".description");
            
            if (titleNode != null && !titleNode.TextContent.Contains(":") && titleNode.TextContent.Length < 100)
            {
                fileName = titleNode.TextContent.Trim();
            }
            else
            {
                // Try body but exclude status
                var bodyNode = link.QuerySelector(".body");
                if (bodyNode != null)
                {
                    var clone = (IElement)bodyNode.Clone();
                    foreach (var s in clone.QuerySelectorAll(".status, .details, .pull_right")) s.Remove();
                    var candidate = clone.TextContent.Trim();
                    if (candidate.Length > 0 && candidate.Length < 100 && !candidate.Contains(":")) fileName = candidate;
                }
                
                // Final desperate attempt: link's own content excluding status tags
                if (string.IsNullOrWhiteSpace(fileName))
                {
                    var clone = (IElement)link.Clone();
                    foreach (var s in clone.QuerySelectorAll(".status, .details, .pull_right, .details_icon")) s.Remove();
                    var candidate = clone.TextContent.Trim();
                    if (candidate.Length > 0 && candidate.Length < 100 && !candidate.Contains(":")) fileName = candidate;
                }
            }

            // Cleanup & Russian Support
            if (!string.IsNullOrWhiteSpace(fileName))
            {
                // Decrypt potential HTML entities
                fileName = System.Net.WebUtility.HtmlDecode(fileName);

                // Remove extra lines if any
                var lines = fileName.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
                fileName = lines.Length > 0 ? lines[0].Trim() : "";
                
                // If the "name" is just "Animation" or generic, we might want to fallback to path
                if (fileName.Equals("Animation", StringComparison.OrdinalIgnoreCase)) fileName = "";
            }

            // Priority 2: Fallback to Path filename (e.g. file_1.mp3)
            if (string.IsNullOrWhiteSpace(fileName) || fileName.Length < 2)
            {
                var rawName = Path.GetFileName(href);
                fileName = System.Net.WebUtility.UrlDecode(rawName);
            }

            var mimeType = GetMimeType(href);

            bool isVoice = link.ClassList.Contains("media_voice_message") || href.Contains("voice_messages");
            bool isAudio = link.ClassList.Contains("media_audio_file") || (mimeType.StartsWith("audio") && !isVoice);
            bool isVideo = link.ClassList.Contains("media_video") || mimeType.StartsWith("video");
            bool isGif = link.ClassList.Contains("animated_wrap") || link.TextContent.Contains("Animation", StringComparison.OrdinalIgnoreCase);

            if (isGif) mimeType = "image/gif"; 
            else if (isVoice) mimeType = "audio/ogg";
            else if (isAudio && mimeType == "application/octet-stream") mimeType = "audio/mpeg";

            // Extract Duration from .status
            string? duration = null;
            var statusNode = link.QuerySelector(".status") ?? link.QuerySelector(".details");
            if (statusNode != null)
            {
                var text = statusNode.TextContent.Trim();
                // Status is often: "01:23, 1.2 MB" or just "01:23"
                var parts = text.Split(',');
                var first = parts[0].Trim();
                if (first.Contains(":") && first.All(c => char.IsDigit(c) || c == ':'))
                {
                    duration = first;
                }
            }

            mediaList.Add(new TelegramMedia(fullPath, fileName, mimeType, duration));
        }

        foreach (var audioEl in node.QuerySelectorAll("audio[src]"))
        {
            var href = NormalizeHref(audioEl.GetAttribute("src"));
            if (href == null) continue;
            var fullPath = BuildPath(baseDir, href);
            if (!mediaList.Any(m => m.FilePath == fullPath))
                mediaList.Add(new TelegramMedia(fullPath, System.Net.WebUtility.UrlDecode(Path.GetFileName(href)), "audio/ogg"));
        }

        return mediaList;
    }

    private static string? NormalizeHref(string? href)
    {
        if (string.IsNullOrWhiteSpace(href)) return null;
        if (href.Contains('#')) href = href[..href.IndexOf('#')];
        if (href.Contains('?')) href = href[..href.IndexOf('?')];
        return string.IsNullOrWhiteSpace(href) ? null : href.Replace("\\", "/");
    }

    private static string BuildPath(string baseDir, string href)
    {
        var normalized = href.Replace("\\", "/");
        return normalized.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase)
            ? normalized
            : (baseDir.TrimEnd('/') + "/" + normalized.TrimStart('/')).Replace("//", "/");
    }

    private string CleanName(IElement node)
    {
        var clone = (IElement)node.Clone();
        foreach (var span in clone.QuerySelectorAll("span, a, div")) span.Remove();
        return clone.TextContent.Trim();
    }

    private string GetMimeType(string path)
    {
        var ext = Path.GetExtension(path).ToLowerInvariant();
        return ext switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".webp" => "image/webp",
            ".mp4" => "video/mp4",
            ".mov" => "video/quicktime",
            ".webm" => "video/webm",
            ".mp3" => "audio/mpeg",
            ".ogg" => "audio/ogg",
            ".wav" => "audio/wav",
            ".m4a" => "audio/mp4",
            ".aac" => "audio/aac",
            _ => "application/octet-stream"
        };
    }
}
