using LightlessSync.API.Data;
using LightlessSync.API.Data.Comparer;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Logging;

namespace LightlessSync.PlayerData.Pairs;

/// <summary>
///     pushes character data to visible pairs
/// </summary>
public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
{
    private readonly ApiController _apiController;
    private readonly DalamudUtilService _dalamudUtil;
    private readonly FileUploadManager _fileTransferManager;
    private readonly PairLedger _pairLedger;
    private CharacterData? _lastCreatedData;
    private CharacterData? _uploadingCharacterData = null;
    private readonly List<UserData> _previouslyVisiblePlayers = [];
    private Task<CharacterData>? _fileUploadTask = null;
    private readonly HashSet<UserData> _usersToPushDataTo = new(UserDataComparer.Instance);
    private readonly SemaphoreSlim _pushLock = new(1, 1);
    private readonly CancellationTokenSource _runtimeCts = new();

    public VisibleUserDataDistributor(ILogger<VisibleUserDataDistributor> logger, ApiController apiController, DalamudUtilService dalamudUtil,
        PairLedger pairLedger, LightlessMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator)
    {
        _apiController = apiController;
        _dalamudUtil = dalamudUtil;
        _pairLedger = pairLedger;
        _fileTransferManager = fileTransferManager;
        Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => FrameworkOnUpdate());
        Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
        {
            var newData = msg.CharacterData;
            if (_lastCreatedData == null || (!string.Equals(newData.DataHash.Value, _lastCreatedData.DataHash.Value, StringComparison.Ordinal)))
            {
                _lastCreatedData = newData;
                Logger.LogTrace("Storing new data hash {hash}", newData.DataHash.Value);
                PushToAllVisibleUsers(forced: true);
            }
            else
            {
                Logger.LogTrace("Data hash {hash} equal to stored data", newData.DataHash.Value);
            }
        });

        Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
        Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
        {
            _fileTransferManager.CancelUpload();
            _previouslyVisiblePlayers.Clear();
            _usersToPushDataTo.Clear();
            _uploadingCharacterData = null;
            _fileUploadTask = null;
        });
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _runtimeCts.Cancel();
            _runtimeCts.Dispose();
        }

        base.Dispose(disposing);
    }

    private void PushToAllVisibleUsers(bool forced = false)
    {
        foreach (var user in GetVisibleUsers())
        {
            _usersToPushDataTo.Add(user);
        }

        if (_usersToPushDataTo.Count > 0)
        {
            Logger.LogDebug("Pushing data {hash} for {count} visible players", _lastCreatedData?.DataHash.Value ?? "UNKNOWN", _usersToPushDataTo.Count);
            PushCharacterData(forced);
        }
    }

    private void FrameworkOnUpdate()
    {
        if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return;

        var allVisibleUsers = GetVisibleUsers();
        var newVisibleUsers = allVisibleUsers.Except(_previouslyVisiblePlayers, UserDataComparer.Instance).ToList();
        _previouslyVisiblePlayers.Clear();
        _previouslyVisiblePlayers.AddRange(allVisibleUsers);
        if (newVisibleUsers.Count == 0) return;

        Logger.LogDebug("Scheduling character data push of {data} to {users}",
            _lastCreatedData?.DataHash.Value ?? string.Empty,
            string.Join(", ", newVisibleUsers.Select(k => k.AliasOrUID)));
        foreach (var user in newVisibleUsers)
        {
            _usersToPushDataTo.Add(user);
        }
        PushCharacterData();
    }

    private void PushCharacterData(bool forced = false)
    {
        if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) return;
        _ = PushCharacterDataAsync(forced);
    }

    private async Task PushCharacterDataAsync(bool forced = false)
    {
        await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
        try
        {
            if (_lastCreatedData == null || _usersToPushDataTo.Count == 0)
                return;

            var hashChanged = _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
            forced |= hashChanged;

            if (_fileUploadTask == null || _fileUploadTask.IsCompleted || forced)
            {
                _uploadingCharacterData = _lastCreatedData.DeepClone();
                var uploadTargets = _usersToPushDataTo.ToList();
                Logger.LogDebug("Starting UploadTask for {hash}, Reason: TaskIsNull: {task}, TaskIsCompleted: {taskCpl}, Forced: {frc}",
                    _lastCreatedData.DataHash,
                    _fileUploadTask == null,
                    _fileUploadTask?.IsCompleted ?? false,
                    forced);

                _fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, uploadTargets);
            }

            var dataToSend = await _fileUploadTask.ConfigureAwait(false);

            var users = _usersToPushDataTo.ToList();
            if (users.Count == 0)
                return;

            Logger.LogDebug("Pushing {data} to {users}", dataToSend.DataHash, string.Join(", ", users.Select(k => k.AliasOrUID)));

            await _apiController.PushCharacterData(dataToSend, users).ConfigureAwait(false);
            _usersToPushDataTo.Clear();
        }
        finally
        {
            _pushLock.Release();
        }
    }

    private List<UserData> GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)];
}
