using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Interface.Colors;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.PairProcessing;
using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Numerics;

namespace LightlessSync.UI;

public class DownloadUi : WindowMediatorSubscriberBase
{
    private readonly LightlessConfigService _configService;
    private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
    private readonly DalamudUtilService _dalamudUtilService;
    private readonly FileUploadManager _fileTransferManager;
    private readonly UiSharedService _uiShared;
    private readonly PairProcessingLimiter _pairProcessingLimiter;
    private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
    private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
    private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = [];

    private byte _transferBoxTransparency = 100;
    private bool _notificationDismissed = true;
    private int _lastDownloadStateHash = 0;

    public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
        PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, 
        PerformanceCollectorService performanceCollectorService)
        : base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
    {
        _dalamudUtilService = dalamudUtilService;
        _configService = configService;
        _pairProcessingLimiter = pairProcessingLimiter;
        _fileTransferManager = fileTransferManager;
        _uiShared = uiShared;

        SizeConstraints = new WindowSizeConstraints()
        {
            MaximumSize = new Vector2(500, 90),
            MinimumSize = new Vector2(500, 90),
        };

        Flags |= ImGuiWindowFlags.NoMove;
        Flags |= ImGuiWindowFlags.NoBackground;
        Flags |= ImGuiWindowFlags.NoInputs;
        Flags |= ImGuiWindowFlags.NoNavFocus;
        Flags |= ImGuiWindowFlags.NoResize;
        Flags |= ImGuiWindowFlags.NoScrollbar;
        Flags |= ImGuiWindowFlags.NoTitleBar;
        Flags |= ImGuiWindowFlags.NoDecoration;
        Flags |= ImGuiWindowFlags.NoFocusOnAppearing;

        DisableWindowSounds = true;

        ForceMainWindow = true;

        IsOpen = true;

        Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => 
        {
            _currentDownloads[msg.DownloadId] = msg.DownloadStatus;
            _notificationDismissed = false;
        });
        Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => 
        {
            _currentDownloads.TryRemove(msg.DownloadId, out _);
            
            // Dismiss notification if all downloads are complete
            if (!_currentDownloads.Any() && !_notificationDismissed)
            {
                Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
                _notificationDismissed = true;
                _lastDownloadStateHash = 0;
            }
        });
        Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
        Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
        Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
        {
            if (msg.IsUploading)
            {
                _uploadingPlayers[msg.Handler] = true;
            }
            else
            {
                _uploadingPlayers.TryRemove(msg.Handler, out _);
            }
        });
    }

    protected override void DrawInternal()
    {
        if (_configService.Current.ShowTransferWindow)
        {
            var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();

            // Check if download notifications are enabled (not set to TextOverlay)
            var useNotifications =
                _configService.Current.UseLightlessNotifications &&
                _configService.Current.LightlessDownloadNotification == NotificationLocation.LightlessUi;

            if (useNotifications)
            {
                if (!_currentDownloads.IsEmpty)
                {
                    UpdateDownloadNotificationIfChanged(limiterSnapshot);
                    _notificationDismissed = false;
                }
                else if (!_notificationDismissed)
                {
                    Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
                    _notificationDismissed = true;
                    _lastDownloadStateHash = 0;
                }
            }
            
            DrawDownloadSummaryBox();

            if (_configService.Current.ShowUploading)
            {
                const int transparency = 100;
                foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList())
                {
                    var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject());
                    if (screenPos == Vector2.Zero) continue;

                    try
                    {
                        using var _ = _uiShared.UidFont.Push();
                        var uploadText = "Uploading";
                        var textSize = ImGui.CalcTextSize(uploadText);

                        var drawList = ImGui.GetBackgroundDrawList();
                        UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
                            UiSharedService.Color(255, 255, 0, transparency),
                            UiSharedService.Color(0, 0, 0, transparency),
                            2
                        );
                    }
                    catch
                    {
                        _logger.LogDebug("Error drawing upload progress");
                    }
                }
            }
        }

        if (_configService.Current.ShowTransferBars)
        {
            DrawTransferBar();
        }
    }

    private void DrawTransferBar()
    {
        const int dlBarBorder = 3;
        const float rounding = 6f;
        var shadowOffset = new Vector2(2, 2);

        List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers;
        try
        {
            transfers = _currentDownloads?.ToList() ?? [];
        }
        catch (ArgumentException)
        {
            return;
        }

        foreach (var transfer in transfers)
        {
            var transferKey = transfer.Key;
            if (transferKey is null)
                continue;

            var statusDict = transfer.Value;
            if (statusDict is null)
                continue;

            var gameObj = transferKey.GetGameObject();
            if (gameObj is null)
            {
                _smoothed.Remove(transferKey);
                continue;
            }

            var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());

            // If RawPos is zero, remove it from smoothed dictionary
            if (rawPos == Vector2.Zero)
            {
                _smoothed.Remove(transferKey);
                continue;
            }

            // Smoothing out the movement and fix jitter around the position.
            Vector2 screenPos = _smoothed.TryGetValue(transferKey, out var lastPos)
                ? (rawPos - lastPos).Length() < 4f ? lastPos : rawPos
                : rawPos;
            _smoothed[transferKey] = screenPos;

            var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes);
            var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes);

            // Per-player state counts
            var dlSlot = 0;
            var dlQueue = 0;
            var dlProg = 0;
            var dlDecomp = 0;

            try
            {
                foreach (var entry in statusDict)
                {
                    var fileStatus = entry.Value;
                    if (fileStatus is null) continue;

                    totalBytes += fileStatus.TotalBytes;
                    transferredBytes += fileStatus.TransferredBytes;

                    switch (fileStatus.DownloadStatus)
                    {
                        case DownloadStatus.WaitingForSlot: dlSlot++; break;
                        case DownloadStatus.WaitingForQueue: dlQueue++; break;
                        case DownloadStatus.Downloading: dlProg++; break;
                        case DownloadStatus.Decompressing: dlDecomp++; break;
                    }
                }
            }
            catch (InvalidOperationException)
            {
                continue;
            }

            string statusText;
            if (dlProg > 0)
            {
                statusText = "Downloading";
            }
            else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes))
            {
                statusText = "Decompressing";
            }
            else if (dlQueue > 0)
            {
                statusText = "Waiting for queue";
            }
            else if (dlSlot > 0)
            {
                statusText = "Waiting for slot";
            }
            else
            {
                statusText = "Waiting";
            }

            var hasValidSize = totalBytes > 0;

            var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
            var textSize = _configService.Current.TransferBarsShowText
                ? ImGui.CalcTextSize(maxDlText)
                : new Vector2(10, 10);

            int dlBarHeight = _configService.Current.TransferBarsHeight > ((int)textSize.Y + 5)
                ? _configService.Current.TransferBarsHeight
                : (int)textSize.Y + 5;
            int dlBarWidth = _configService.Current.TransferBarsWidth > ((int)textSize.X + 10)
                ? _configService.Current.TransferBarsWidth
                : (int)textSize.X + 10;

            var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f);
            var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f);

            // Precompute rects
            var outerStart = new Vector2(dlBarStart.X - dlBarBorder - 1, dlBarStart.Y - dlBarBorder - 1);
            var outerEnd = new Vector2(dlBarEnd.X + dlBarBorder + 1, dlBarEnd.Y + dlBarBorder + 1);
            var borderStart = new Vector2(dlBarStart.X - dlBarBorder, dlBarStart.Y - dlBarBorder);
            var borderEnd = new Vector2(dlBarEnd.X + dlBarBorder, dlBarEnd.Y + dlBarBorder);

            var drawList = ImGui.GetBackgroundDrawList();

            drawList.AddRectFilled(
                outerStart + shadowOffset,
                outerEnd + shadowOffset,
                UiSharedService.Color(0, 0, 0, 100 / 2),
                rounding + 2
            );
            drawList.AddRectFilled(
                outerStart,
                outerEnd,
                UiSharedService.Color(0, 0, 0, 100),
                rounding + 2
            );
            drawList.AddRectFilled(
                borderStart,
                borderEnd,
                UiSharedService.Color(ImGuiColors.DalamudGrey),
                rounding
            );
            drawList.AddRectFilled(
                dlBarStart,
                dlBarEnd,
                UiSharedService.Color(0, 0, 0, 100),
                rounding
            );

            bool showFill = false;
            double fillPercent = 0.0;

            if (hasValidSize)
            {
                if (dlProg > 0)
                {
                    fillPercent = transferredBytes / (double)totalBytes;
                    showFill = true;
                }
                else if (dlDecomp > 0 || transferredBytes >= totalBytes)
                {
                    fillPercent = 1.0;
                    showFill = true;
                }
            }

            if (showFill)
            {
                if (fillPercent < 0) fillPercent = 0;
                if (fillPercent > 1) fillPercent = 1;

                var progressEndX = dlBarStart.X + (float)(fillPercent * dlBarWidth);
                var progressEnd = new Vector2(progressEndX, dlBarEnd.Y);

                drawList.AddRectFilled(
                    dlBarStart,
                    progressEnd,
                    UiSharedService.Color(UIColors.Get("LightlessPurple")),
                    rounding
                );
            }

            if (_configService.Current.TransferBarsShowText)
            {
                string downloadText;

                if (dlProg > 0 && hasValidSize)
                {
                    downloadText =
                        $"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
                }
                else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize)
                {
                    downloadText = "Decompressing";
                }
                else
                {
                    // Waiting states
                    downloadText = statusText;
                }

                var actualTextSize = ImGui.CalcTextSize(downloadText);

                UiSharedService.DrawOutlinedFont(
                    drawList,
                    downloadText,
                    screenPos with
                    {
                        X = screenPos.X - actualTextSize.X / 2f - 1,
                        Y = screenPos.Y - actualTextSize.Y / 2f - 1
                    },
                    UiSharedService.Color(ImGuiColors.DalamudGrey),
                    UiSharedService.Color(0, 0, 0, 100),
                    1
                );
            }
        }

        if (_configService.Current.ShowUploading)
        {
            foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList())
            {
                var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject());
                if (screenPos == Vector2.Zero) continue;

                try
                {
                    using var _ = _uiShared.UidFont.Push();
                    var uploadText = "Uploading";

                    var textSize = ImGui.CalcTextSize(uploadText);

                    var drawList = ImGui.GetBackgroundDrawList();
                    UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
                        UiSharedService.Color(ImGuiColors.DalamudYellow),
                        UiSharedService.Color(0, 0, 0, 100),
                        2
                    );
                }
                catch
                {
                    _logger.LogDebug("Error drawing upload progress");
                }
            }
        }
    }

    private void DrawDownloadSummaryBox()
    {
        if (_currentDownloads.IsEmpty)
            return;

        const float padding = 6f;
        const float spacingY = 2f;
        const float minBoxWidth = 320f;

        var now = ImGui.GetTime();

        int totalFiles = 0;
        int transferredFiles = 0;
        long totalBytes = 0;
        long transferredBytes = 0;

        var totalDlSlot = 0;
        var totalDlQueue = 0;
        var totalDlProg = 0;
        var totalDlDecomp = 0;

        var perPlayer = new List<(
            string Name,
            int TransferredFiles,
            int TotalFiles,
            long TransferredBytes,
            long TotalBytes,
            double SpeedBytesPerSecond,
            int DlSlot,
            int DlQueue,
            int DlProg,
            int DlDecomp)>();

        foreach (var transfer in _currentDownloads)
        {
            var handler = transfer.Key;
            var statuses = transfer.Value.Values;

            var playerTotalFiles = statuses.Sum(s => s.TotalFiles);
            var playerTransferredFiles = statuses.Sum(s => s.TransferredFiles);
            var playerTotalBytes = statuses.Sum(s => s.TotalBytes);
            var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);

            totalFiles += playerTotalFiles;
            transferredFiles += playerTransferredFiles;
            totalBytes += playerTotalBytes;
            transferredBytes += playerTransferredBytes;

            // per-player W/Q/P/D
            var playerDlSlot = 0;
            var playerDlQueue = 0;
            var playerDlProg = 0;
            var playerDlDecomp = 0;

            foreach (var entry in transfer.Value)
            {
                var fileStatus = entry.Value;
                switch (fileStatus.DownloadStatus)
                {
                    case DownloadStatus.WaitingForSlot:
                        playerDlSlot++;
                        totalDlSlot++;
                        break;
                    case DownloadStatus.WaitingForQueue:
                        playerDlQueue++;
                        totalDlQueue++;
                        break;
                    case DownloadStatus.Downloading:
                        playerDlProg++;
                        totalDlProg++;
                        break;
                    case DownloadStatus.Decompressing:
                        playerDlDecomp++;
                        totalDlDecomp++;
                        break;
                }
            }

            double speed = 0;
            if (playerTotalBytes > 0)
            {
                if (!_downloadSpeeds.TryGetValue(handler, out var tracker))
                {
                    tracker = new DownloadSpeedTracker(windowSeconds: 3.0);
                    _downloadSpeeds[handler] = tracker;
                }

                speed = tracker.Update(now, playerTransferredBytes);
            }

            perPlayer.Add((
                handler.Name,
                playerTransferredFiles,
                playerTotalFiles,
                playerTransferredBytes,
                playerTotalBytes,
                speed,
                playerDlSlot,
                playerDlQueue,
                playerDlProg,
                playerDlDecomp
            ));
        }

        // Clean speed trackers for players with no active downloads
        foreach (var handler in _downloadSpeeds.Keys.ToList())
        {
            if (!_currentDownloads.ContainsKey(handler))
                _downloadSpeeds.Remove(handler);
        }

        if (totalFiles == 0 || totalBytes == 0)
            return;

        // max speed for per-player bar scale (clamped)
        double maxSpeed = perPlayer.Count > 0 ? perPlayer.Max(p => p.SpeedBytesPerSecond) : 0;
        if (maxSpeed <= 0)
            maxSpeed = 1;

        var drawList = ImGui.GetBackgroundDrawList();
        var windowPos = ImGui.GetWindowPos();

        // Overall texts
        var headerText =
            $"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}]";

        var bytesText =
            $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";

        var totalSpeed = perPlayer.Sum(p => p.SpeedBytesPerSecond);
        var speedText = totalSpeed > 0
            ? $"{UiSharedService.ByteToString((long)totalSpeed)}/s"
            : "Calculating in lightspeed...";

        var headerSize = ImGui.CalcTextSize(headerText);
        var bytesSize = ImGui.CalcTextSize(bytesText);
        var totalSpeedSize = ImGui.CalcTextSize(speedText);

        float contentWidth = headerSize.X;
        if (bytesSize.X > contentWidth) contentWidth = bytesSize.X;
        if (totalSpeedSize.X > contentWidth) contentWidth = totalSpeedSize.X;

        if (_configService.Current.ShowPlayerLinesTransferWindow)
        {
            foreach (var p in perPlayer)
            {
                var line =
                    $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";

                var lineSize = ImGui.CalcTextSize(line);
                if (lineSize.X > contentWidth)
                    contentWidth = lineSize.X;
            }

        }

        var lineHeight = ImGui.GetTextLineHeight();
        var globalBarHeight = lineHeight * 0.8f;
        var perPlayerBarHeight = lineHeight * 0.4f;

        // Box width
        float boxWidth = contentWidth + padding * 2;
        if (boxWidth < minBoxWidth)
            boxWidth = minBoxWidth;

        // Box height
        float boxHeight = 0;
        boxHeight += padding;
        boxHeight += globalBarHeight;
        boxHeight += padding;

        boxHeight += lineHeight + spacingY;
        boxHeight += lineHeight + spacingY;
        boxHeight += lineHeight * 1.4f + spacingY; 

        if (_configService.Current.ShowPlayerLinesTransferWindow)
        {
            foreach (var p in perPlayer)
            {
                boxHeight += lineHeight + spacingY;

                var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow
                              && p.TransferredBytes > 0;

                if (showBar)
                {
                    boxHeight += perPlayerBarHeight + spacingY;
                }
            }
        }

        boxHeight += padding;

        var boxMin = windowPos;
        var boxMax = new Vector2(windowPos.X + boxWidth, windowPos.Y + boxHeight);

        // Background + border
        drawList.AddRectFilled(boxMin, boxMax, UiSharedService.Color(0, 0, 0, _transferBoxTransparency), 5f);
        drawList.AddRect(boxMin, boxMax, UiSharedService.Color(ImGuiColors.DalamudGrey), 5f);

        var cursor = boxMin + new Vector2(padding, padding);

        var barMin = cursor;
        var barMax = new Vector2(boxMin.X + boxWidth - padding, cursor.Y + globalBarHeight);

        var progress = (float)transferredBytes / totalBytes;
        if (progress < 0f) progress = 0f;
        if (progress > 1f) progress = 1f;

        drawList.AddRectFilled(barMin, barMax, UiSharedService.Color(40, 40, 40, _transferBoxTransparency), 3f);
        drawList.AddRectFilled(
            barMin,
            new Vector2(barMin.X + (barMax.X - barMin.X) * progress, barMax.Y),
            UiSharedService.Color(UIColors.Get("LightlessPurple")),
            3f
        );

        cursor.Y = barMax.Y + padding;

        // Header
        UiSharedService.DrawOutlinedFont(
            drawList,
            headerText,
            cursor,
            UiSharedService.Color(ImGuiColors.DalamudWhite),
            UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
            1
        );
        cursor.Y += lineHeight + spacingY;

        // Bytes
        UiSharedService.DrawOutlinedFont(
            drawList,
            bytesText,
            cursor,
            UiSharedService.Color(ImGuiColors.DalamudWhite),
            UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
            1
        );
        cursor.Y += lineHeight + spacingY;

        // Total speed
        UiSharedService.DrawOutlinedFont(
            drawList,
            speedText,
            cursor,
            UiSharedService.Color(UIColors.Get("LightlessPurple")),
            UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
            1
        );
        cursor.Y += lineHeight * 1.4f + spacingY;

        var orderedPlayers = perPlayer.OrderByDescending(p => p.TotalBytes).ToList();

        foreach (var p in orderedPlayers)
        {
            var hasSpeed = p.SpeedBytesPerSecond > 0;
            var playerSpeedText = hasSpeed
                ? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s"
                : "-";

            var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow
                          && p.TransferredBytes > 0; 

            var labelLine =
                $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";

            if (!showBar)
            {
                UiSharedService.DrawOutlinedFont(
                    drawList,
                    labelLine,
                    cursor,
                    UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
                    UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
                    1
                );

                cursor.Y += lineHeight + spacingY;
                continue;
            }

            UiSharedService.DrawOutlinedFont(
                drawList,
                labelLine,
                cursor,
                UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
                UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
                1
            );
            cursor.Y += lineHeight + spacingY;

            // Bar background
            var barBgMin = new Vector2(boxMin.X + padding, cursor.Y);
            var barBgMax = new Vector2(boxMax.X - padding, cursor.Y + perPlayerBarHeight);

            drawList.AddRectFilled(
                barBgMin,
                barBgMax,
                UiSharedService.Color(40, 40, 40, _transferBoxTransparency),
                3f
            );

            // Fill based on Progress of download
            float ratio = 0f;
            if (p.TotalBytes > 0)
                ratio = (float)p.TransferredBytes / p.TotalBytes;

            if (ratio < 0f) ratio = 0f;
            if (ratio > 1f) ratio = 1f;

            var fillX = barBgMin.X + (barBgMax.X - barBgMin.X) * ratio;
            var barFillMax = new Vector2(fillX, barBgMax.Y);

            drawList.AddRectFilled(
                barBgMin,
                barFillMax,
                UiSharedService.Color(UIColors.Get("LightlessPurple")),
                3f
            );

            // Text inside bar: downloading vs decompressing
            string barText;

            var isDecompressing = p.DlDecomp > 0 && p.TransferredBytes >= p.TotalBytes && p.TotalBytes > 0;

            if (isDecompressing)
            {
                // Keep bar full, static text showing decompressing
                barText = "Decompressing...";
            }
            else
            {
                var bytesInside =
                    $"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}";

                barText = hasSpeed
                    ? $"{bytesInside} @ {playerSpeedText}"
                    : bytesInside;
            }

            if (!string.IsNullOrEmpty(barText))
            {
                var barTextSize = ImGui.CalcTextSize(barText);

                var barTextPos = new Vector2(
                    barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1,
                    barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1
                );

                UiSharedService.DrawOutlinedFont(
                    drawList,
                    barText,
                    barTextPos,
                    UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
                    UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
                    1
                );
            }

            cursor.Y += perPlayerBarHeight + spacingY;
        }
    }

    public override bool DrawConditions()
    {
        if (_uiShared.EditTrackerPosition) return true;
        if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
        if (_currentDownloads.IsEmpty && !_fileTransferManager.IsUploading && _uploadingPlayers.IsEmpty) return false;
        if (!IsOpen) return false;
        return true;
    }

    public override void PreDraw()
    {
        base.PreDraw();

        if (_uiShared.EditTrackerPosition)
        {
            Flags &= ~ImGuiWindowFlags.NoMove;
            Flags &= ~ImGuiWindowFlags.NoBackground;
            Flags &= ~ImGuiWindowFlags.NoInputs;
            Flags &= ~ImGuiWindowFlags.NoResize;
        }
        else
        {
            Flags |= ImGuiWindowFlags.NoMove;
            Flags |= ImGuiWindowFlags.NoBackground;
            Flags |= ImGuiWindowFlags.NoInputs;
            Flags |= ImGuiWindowFlags.NoResize;
        }

        var maxHeight = ImGui.GetTextLineHeight() * (_configService.Current.ParallelDownloads + 3);
        SizeConstraints = new()
        {
            MinimumSize = new Vector2(300, maxHeight),
            MaximumSize = new Vector2(300, maxHeight),
        };
    }

    private void UpdateDownloadNotificationIfChanged(PairProcessingLimiterSnapshot limiterSnapshot)
    {
        var downloadStatus = new List<(string playerName, float progress, string status)>(_currentDownloads.Count);
        var hashCode = new HashCode();

        foreach (var item in _currentDownloads)
        {
            var dlSlot = 0;
            var dlQueue = 0;
            var dlProg = 0;
            var dlDecomp = 0;
            long totalBytes = 0;
            long transferredBytes = 0;

            // Single pass through the dictionary to count everything - avoid multiple LINQ iterations
            foreach (var entry in item.Value)
            {
                var fileStatus = entry.Value;
                switch (fileStatus.DownloadStatus)
                {
                    case DownloadStatus.WaitingForSlot: dlSlot++; break;
                    case DownloadStatus.WaitingForQueue: dlQueue++; break;
                    case DownloadStatus.Downloading: dlProg++; break;
                    case DownloadStatus.Decompressing: dlDecomp++; break;
                }
                totalBytes += fileStatus.TotalBytes;
                transferredBytes += fileStatus.TransferredBytes;
            }

            var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
            
            string status;
            if (dlDecomp > 0) status = "decompressing";
            else if (dlProg > 0) status = "downloading";
            else if (dlQueue > 0) status = "queued";
            else if (dlSlot > 0) status = "waiting";
            else status = "completed";

            downloadStatus.Add((item.Key.Name, progress, status));
            
            // Build hash from meaningful state
            hashCode.Add(item.Key.Name);
            hashCode.Add(transferredBytes);
            hashCode.Add(totalBytes);
            hashCode.Add(status);
        }

        var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
        hashCode.Add(queueWaiting);
        
        var currentHash = hashCode.ToHashCode();
        
        // Only update notification if state has actually changed
        if (currentHash != _lastDownloadStateHash)
        {
            _lastDownloadStateHash = currentHash;
            if (downloadStatus.Count > 0 || queueWaiting > 0)
            {
                Mediator.Publish(new PairDownloadStatusMessage(downloadStatus, queueWaiting));
            }
        }
    }

    private sealed class DownloadSpeedTracker
    {
        private readonly Queue<(double Time, long Bytes)> _samples = new();
        private readonly double _windowSeconds;

        public double SpeedBytesPerSecond { get; private set; }

        public DownloadSpeedTracker(double windowSeconds = 3.0)
        {
            _windowSeconds = windowSeconds;
        }

        public double Update(double now, long totalBytes)
        {
            if (_samples.Count > 0 && totalBytes < _samples.Last().Bytes)
            {
                _samples.Clear();
            }

            _samples.Enqueue((now, totalBytes));

            while (_samples.Count > 0 && now - _samples.Peek().Time > _windowSeconds)
                _samples.Dequeue();

            if (_samples.Count < 2)
            {
                SpeedBytesPerSecond = 0;
                return SpeedBytesPerSecond;
            }

            var oldest = _samples.Peek();
            var newest = _samples.Last();

            var dt = newest.Time - oldest.Time;
            if (dt <= 0.0001)
            {
                SpeedBytesPerSecond = 0;
                return SpeedBytesPerSecond;
            }

            var dBytes = newest.Bytes - oldest.Bytes;
            if (dBytes <= 0)
            {
                SpeedBytesPerSecond = 0;
                return SpeedBytesPerSecond;
            }

            
            const long minBytesForSpeed = 32 * 1024;
            if (dBytes < minBytesForSpeed)
            {
                
                return SpeedBytesPerSecond;
            }

            var avg = dBytes / dt;

            const double alpha = 0.3;
            SpeedBytesPerSecond = SpeedBytesPerSecond <= 0
                ? avg
                : SpeedBytesPerSecond * (1 - alpha) + avg * alpha;

            return SpeedBytesPerSecond;
        }
    }
}