using Dalamud.Game.ClientState.Objects.Types;
using K4os.Compression.LZ4.Legacy;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Factories;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.CharaData.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Text;

namespace MareSynchronos.Services;

public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
{
    private readonly ApiController _apiController;
    private readonly CharaDataConfigService _configService;
    private readonly DalamudUtilService _dalamudUtilService;
    private readonly CharaDataFileHandler _fileHandler;
    private readonly IpcManager _ipcManager;
    private readonly ConcurrentDictionary<string, CharaDataMetaInfoExtendedDto?> _metaInfoCache = [];
    private readonly List<CharaDataMetaInfoExtendedDto> _nearbyData = [];
    private readonly CharaDataNearbyManager _nearbyManager;
    private readonly CharaDataCharacterHandler _characterHandler;
    private readonly PairManager _pairManager;
    private readonly Dictionary<string, CharaDataFullExtendedDto> _ownCharaData = [];
    private readonly Dictionary<string, Task> _sharedMetaInfoTimeoutTasks = [];
    private readonly Dictionary<UserData, List<CharaDataMetaInfoExtendedDto>> _sharedWithYouData = [];
    private readonly Dictionary<string, CharaDataExtendedUpdateDto> _updateDtos = [];
    private CancellationTokenSource _applicationCts = new();
    private CancellationTokenSource _charaDataCreateCts = new();
    private CancellationTokenSource _connectCts = new();
    private CancellationTokenSource _getAllDataCts = new();
    private CancellationTokenSource _getSharedDataCts = new();
    private CancellationTokenSource _uploadCts = new();

    public CharaDataManager(ILogger<CharaDataManager> logger, ApiController apiController,
        CharaDataFileHandler charaDataFileHandler,
        MareMediator mareMediator, IpcManager ipcManager, DalamudUtilService dalamudUtilService,
        FileDownloadManagerFactory fileDownloadManagerFactory,
        CharaDataConfigService charaDataConfigService, CharaDataNearbyManager charaDataNearbyManager,
        CharaDataCharacterHandler charaDataCharacterHandler, PairManager pairManager) : base(logger, mareMediator)
    {
        _apiController = apiController;
        _fileHandler = charaDataFileHandler;
        _ipcManager = ipcManager;
        _dalamudUtilService = dalamudUtilService;
        _configService = charaDataConfigService;
        _nearbyManager = charaDataNearbyManager;
        _characterHandler = charaDataCharacterHandler;
        _pairManager = pairManager;
        mareMediator.Subscribe<ConnectedMessage>(this, (msg) =>
        {
            _connectCts?.Cancel();
            _connectCts?.Dispose();
            _connectCts = new();
            _ownCharaData.Clear();
            _metaInfoCache.Clear();
            _sharedWithYouData.Clear();
            _updateDtos.Clear();
            Initialized = false;
            MaxCreatableCharaData = msg.Connection.ServerInfo.MaxCharaData;
            if (_configService.Current.DownloadMcdDataOnConnection)
            {
                var token = _connectCts.Token;
                _ = GetAllData(token);
                _ = GetAllSharedData(token);
            }
        });
        mareMediator.Subscribe<DisconnectedMessage>(this, (msg) =>
        {
            _ownCharaData.Clear();
            _metaInfoCache.Clear();
            _sharedWithYouData.Clear();
            _updateDtos.Clear();
            Initialized = false;
        });
    }

    public Task? AttachingPoseTask { get; private set; }
    public Task? CharaUpdateTask { get; set; }
    public string DataApplicationProgress { get; private set; } = string.Empty;
    public Task? DataApplicationTask { get; private set; }
    public Task<(string Output, bool Success)>? DataCreationTask { get; private set; }
    public Task? DataGetTimeoutTask { get; private set; }
    public Task<(string Result, bool Success)>? DownloadMetaInfoTask { get; private set; }
    public Task<List<CharaDataFullExtendedDto>>? GetAllDataTask { get; private set; }
    public Task<List<CharaDataMetaInfoDto>>? GetSharedWithYouTask { get; private set; }
    public Task? GetSharedWithYouTimeoutTask { get; private set; }
    public IReadOnlyDictionary<string, HandledCharaDataEntry> HandledCharaData => _characterHandler.HandledCharaData;
    public bool Initialized { get; private set; }
    public CharaDataMetaInfoExtendedDto? LastDownloadedMetaInfo { get; private set; }
    public Task<(MareCharaFileHeader LoadedFile, long ExpectedLength)>? LoadedMcdfHeader { get; private set; }
    public int MaxCreatableCharaData { get; private set; }
    public Task? McdfApplicationTask { get; private set; }
    public List<CharaDataMetaInfoExtendedDto> NearbyData => _nearbyData;
    public IDictionary<string, CharaDataFullExtendedDto> OwnCharaData => _ownCharaData;
    public IDictionary<UserData, List<CharaDataMetaInfoExtendedDto>> SharedWithYouData => _sharedWithYouData;
    public Task? UiBlockingComputation { get; private set; }
    public ValueProgress<string>? UploadProgress { get; private set; }
    public Task<(string Output, bool Success)>? UploadTask { get; set; }
    public bool BrioAvailable => _ipcManager.Brio.APIAvailable;

    public Task ApplyCharaData(CharaDataDownloadDto dataDownloadDto, string charaName)
    {
        return UiBlockingComputation = DataApplicationTask = Task.Run(async () =>
        {
            if (string.IsNullOrEmpty(charaName)) return;

            CharaDataMetaInfoDto metaInfo = new(dataDownloadDto.Id, dataDownloadDto.Uploader)
            {
                CanBeDownloaded = true,
                Description = $"Data from {dataDownloadDto.Uploader.AliasOrUID} for {dataDownloadDto.Id}",
                UpdatedDate = dataDownloadDto.UpdatedDate,
            };

            await DownloadAndAplyDataAsync(charaName, dataDownloadDto, metaInfo, false).ConfigureAwait(false);
        });
    }

    public Task ApplyCharaData(CharaDataMetaInfoDto dataMetaInfoDto, string charaName)
    {
        return UiBlockingComputation = DataApplicationTask = Task.Run(async () =>
        {
            if (string.IsNullOrEmpty(charaName)) return;

            var download = await _apiController.CharaDataDownload(dataMetaInfoDto.Uploader.UID + ":" + dataMetaInfoDto.Id).ConfigureAwait(false);
            if (download == null)
            {
                DataApplicationTask = null;
                return;
            }

            await DownloadAndAplyDataAsync(charaName, download, dataMetaInfoDto, false).ConfigureAwait(false);
        });
    }

    public Task ApplyCharaDataToGposeTarget(CharaDataMetaInfoDto dataMetaInfoDto)
    {
        return UiBlockingComputation = DataApplicationTask = Task.Run(async () =>
        {
            var obj = await _dalamudUtilService.GetGposeTargetGameObjectAsync().ConfigureAwait(false);
            var charaName = obj?.Name.TextValue ?? string.Empty;
            if (string.IsNullOrEmpty(charaName)) return;

            await ApplyCharaData(dataMetaInfoDto, charaName).ConfigureAwait(false);
        });
    }

    public async Task ApplyOwnDataToGposeTarget(CharaDataFullExtendedDto dataDto)
    {
        var chara = await _dalamudUtilService.GetGposeTargetGameObjectAsync().ConfigureAwait(false);
        var charaName = chara?.Name.TextValue ?? string.Empty;
        CharaDataDownloadDto downloadDto = new(dataDto.Id, dataDto.Uploader)
        {
            CustomizeData = dataDto.CustomizeData,
            Description = dataDto.Description,
            FileGamePaths = dataDto.FileGamePaths,
            GlamourerData = dataDto.GlamourerData,
            FileSwaps = dataDto.FileSwaps,
            ManipulationData = dataDto.ManipulationData,
            UpdatedDate = dataDto.UpdatedDate
        };

        CharaDataMetaInfoDto metaInfoDto = new(dataDto.Id, dataDto.Uploader)
        {
            CanBeDownloaded = true,
            Description = dataDto.Description,
            PoseData = dataDto.PoseData,
            UpdatedDate = dataDto.UpdatedDate,
        };

        UiBlockingComputation = DataApplicationTask = DownloadAndAplyDataAsync(charaName, downloadDto, metaInfoDto, false);
    }

    public Task ApplyPoseData(PoseEntry pose, string targetName)
    {
        return UiBlockingComputation = Task.Run(async () =>
        {
            if (string.IsNullOrEmpty(pose.PoseData) || !(await CanApplyInGpose().ConfigureAwait(false)).CanApply) return;
            var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(targetName, true).ConfigureAwait(false);
            if (gposeChara == null) return;

            var poseJson = Encoding.UTF8.GetString(LZ4Wrapper.Unwrap(Convert.FromBase64String(pose.PoseData)));
            if (string.IsNullOrEmpty(poseJson)) return;

            await _ipcManager.Brio.SetPoseAsync(gposeChara.Address, poseJson).ConfigureAwait(false);
        });
    }

    public Task ApplyPoseDataToGPoseTarget(PoseEntry pose)
    {
        return UiBlockingComputation = Task.Run(async () =>
        {
            var apply = await CanApplyInGpose().ConfigureAwait(false);

            if (apply.CanApply)
            {
                await ApplyPoseData(pose, apply.TargetName).ConfigureAwait(false);
            }
        });
    }

    public Task ApplyWorldDataToTarget(PoseEntry pose, string targetName)
    {
        return UiBlockingComputation = Task.Run(async () =>
        {
            var apply = await CanApplyInGpose().ConfigureAwait(false);
            if (pose.WorldData == default || !(await CanApplyInGpose().ConfigureAwait(false)).CanApply) return;
            var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(targetName, true).ConfigureAwait(false);
            if (gposeChara == null) return;

            if (pose.WorldData == null || pose.WorldData == default) return;

            Logger.LogDebug("Applying World data {data}", pose.WorldData);

            await _ipcManager.Brio.ApplyTransformAsync(gposeChara.Address, pose.WorldData.Value).ConfigureAwait(false);
        });
    }

    public Task ApplyWorldDataToGPoseTarget(PoseEntry pose)
    {
        return UiBlockingComputation = Task.Run(async () =>
        {
            var apply = await CanApplyInGpose().ConfigureAwait(false);
            if (apply.CanApply)
            {
                await ApplyPoseData(pose, apply.TargetName).ConfigureAwait(false);
            }
        });
    }

    public void AttachWorldData(PoseEntry pose, CharaDataExtendedUpdateDto updateDto)
    {
        AttachingPoseTask = Task.Run(async () =>
        {
            ICharacter? playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
            if (playerChar == null) return;
            if (_dalamudUtilService.IsInGpose)
            {
                playerChar = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(playerChar.Name.TextValue, true).ConfigureAwait(false);
            }
            if (playerChar == null) return;
            var worldData = await _ipcManager.Brio.GetTransformAsync(playerChar.Address).ConfigureAwait(false);
            if (worldData == default) return;

            Logger.LogTrace("Attaching World data {data}", worldData);

            worldData.LocationInfo = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);

            Logger.LogTrace("World data serialized: {data}", worldData);

            pose.WorldData = worldData;

            updateDto.UpdatePoseList();
        });
    }

    public async Task<(bool CanApply, string TargetName)> CanApplyInGpose()
    {
        var obj = await _dalamudUtilService.GetGposeTargetGameObjectAsync().ConfigureAwait(false);
        string targetName = string.Empty;
        bool canApply = _dalamudUtilService.IsInGpose && obj != null
            && obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player;
        if (canApply)
        {
            targetName = obj!.Name.TextValue;
        }
        else
        {
            targetName = "Invalid Target";
        }
        return (canApply, targetName);
    }

    public void CancelDataApplication()
    {
        _applicationCts.Cancel();
    }

    public void CancelUpload()
    {
        _uploadCts.Cancel();
    }

    public void CreateCharaDataEntry(CancellationToken cancelToken)
    {
        UiBlockingComputation = DataCreationTask = Task.Run(async () =>
        {
            var result = await _apiController.CharaDataCreate().ConfigureAwait(false);
            _ = Task.Run(async () =>
            {
                _charaDataCreateCts = _charaDataCreateCts.CancelRecreate();
                using var ct = CancellationTokenSource.CreateLinkedTokenSource(_charaDataCreateCts.Token, cancelToken);
                await Task.Delay(TimeSpan.FromSeconds(10), ct.Token).ConfigureAwait(false);
                DataCreationTask = null;
            });


            if (result == null)
                return ("Failed to create character data, see log for more information", false);

            await AddOrUpdateDto(result).ConfigureAwait(false);

            return ("Created Character Data", true);
        });
    }

    public async Task DeleteCharaData(CharaDataFullExtendedDto dto)
    {
        var ret = await _apiController.CharaDataDelete(dto.Id).ConfigureAwait(false);
        if (ret)
        {
            _ownCharaData.Remove(dto.Id);
            _metaInfoCache.Remove(dto.FullId, out _);
        }
        DistributeMetaInfo();
    }

    public void DownloadMetaInfo(string importCode, bool store = true)
    {
        DownloadMetaInfoTask = Task.Run(async () =>
        {
            try
            {
                if (store)
                {
                    LastDownloadedMetaInfo = null;
                }
                var metaInfo = await _apiController.CharaDataGetMetainfo(importCode).ConfigureAwait(false);
                _sharedMetaInfoTimeoutTasks[importCode] = Task.Delay(TimeSpan.FromSeconds(10));
                if (metaInfo == null)
                {
                    _metaInfoCache[importCode] = null;
                    return ("Failed to download meta info for this code. Check if the code is valid and you have rights to access it.", false);
                }
                await CacheData(metaInfo).ConfigureAwait(false);
                if (store)
                {
                    LastDownloadedMetaInfo = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService).ConfigureAwait(false);
                }
                return ("Ok", true);
            }
            finally
            {
                if (!store)
                    DownloadMetaInfoTask = null;
            }
        });
    }

    public async Task GetAllData(CancellationToken cancelToken)
    {
        foreach (var data in _ownCharaData)
        {
            _metaInfoCache.Remove(data.Key, out _);
        }
        _ownCharaData.Clear();
        UiBlockingComputation = GetAllDataTask = Task.Run(async () =>
        {
            _getAllDataCts = _getAllDataCts.CancelRecreate();
            var result = await _apiController.CharaDataGetOwn().ConfigureAwait(false);

            Initialized = true;

            if (result.Any())
            {
                DataGetTimeoutTask = Task.Run(async () =>
                {
                    using var ct = CancellationTokenSource.CreateLinkedTokenSource(_getAllDataCts.Token, cancelToken);
#if !DEBUG
                    await Task.Delay(TimeSpan.FromMinutes(1), ct.Token).ConfigureAwait(false);
#else
                    await Task.Delay(TimeSpan.FromSeconds(5), ct.Token).ConfigureAwait(false);
#endif
                });
            }

            return result.OrderBy(u => u.CreatedDate).Select(k => new CharaDataFullExtendedDto(k)).ToList();
        });

        var result = await GetAllDataTask.ConfigureAwait(false);
        foreach (var item in result)
        {
            await AddOrUpdateDto(item).ConfigureAwait(false);
        }

        foreach (var id in _updateDtos.Keys.Where(r => !result.Exists(res => string.Equals(res.Id, r, StringComparison.Ordinal))).ToList())
        {
            _updateDtos.Remove(id);
        }
        GetAllDataTask = null;
    }

    public async Task GetAllSharedData(CancellationToken token)
    {
        Logger.LogDebug("Getting Shared with You Data");

        UiBlockingComputation = GetSharedWithYouTask = _apiController.CharaDataGetShared();
        _sharedWithYouData.Clear();

        GetSharedWithYouTimeoutTask = Task.Run(async () =>
        {
            _getSharedDataCts = _getSharedDataCts.CancelRecreate();
            using var ct = CancellationTokenSource.CreateLinkedTokenSource(_getSharedDataCts.Token, token);
#if !DEBUG
            await Task.Delay(TimeSpan.FromMinutes(1), ct.Token).ConfigureAwait(false);
#else
            await Task.Delay(TimeSpan.FromSeconds(5), ct.Token).ConfigureAwait(false);
#endif
            GetSharedWithYouTimeoutTask = null;
            Logger.LogDebug("Finished Shared with You Data Timeout");
        });

        var result = await GetSharedWithYouTask.ConfigureAwait(false);
        foreach (var grouping in result.GroupBy(r => r.Uploader))
        {
            var pair = _pairManager.GetPairByUID(grouping.Key.UID);
            if (pair?.IsPaused ?? false) continue;
            List<CharaDataMetaInfoExtendedDto> newList = new();
            foreach (var item in grouping)
            {
                var extended = await CharaDataMetaInfoExtendedDto.Create(item, _dalamudUtilService).ConfigureAwait(false);
                newList.Add(extended);
                CacheData(extended);
            }
            _sharedWithYouData[grouping.Key] = newList;
        }

        DistributeMetaInfo();

        Logger.LogDebug("Finished getting Shared with You Data");
        GetSharedWithYouTask = null;
    }

    public CharaDataExtendedUpdateDto? GetUpdateDto(string id)
    {
        if (_updateDtos.TryGetValue(id, out var dto))
            return dto;
        return null;
    }

    public bool IsInTimeout(string key)
    {
        if (!_sharedMetaInfoTimeoutTasks.TryGetValue(key, out var task)) return false;
        return !task?.IsCompleted ?? false;
    }

    public void LoadMcdf(string filePath)
    {
        LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
    }

    public void McdfApplyToTarget(string charaName)
    {
        if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;

        List<string> actuallyExtractedFiles = [];

        UiBlockingComputation = McdfApplicationTask = Task.Run(async () =>
        {
            Guid applicationId = Guid.NewGuid();
            try
            {
                using GameObjectHandler? tempHandler = await _characterHandler.TryCreateGameObjectHandler(charaName, true).ConfigureAwait(false);
                if (tempHandler == null) return;
                var playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
                bool isSelf = playerChar != null && string.Equals(playerChar.Name.TextValue, tempHandler.Name, StringComparison.Ordinal);

                long expectedExtractedSize = LoadedMcdfHeader.Result.ExpectedLength;
                var charaFile = LoadedMcdfHeader.Result.LoadedFile;
                DataApplicationProgress = "Extracting MCDF data";

                var extractedFiles = _fileHandler.McdfExtractFiles(charaFile, expectedExtractedSize, actuallyExtractedFiles);

                foreach (var entry in charaFile.CharaFileData.FileSwaps.SelectMany(k => k.GamePaths, (k, p) => new KeyValuePair<string, string>(p, k.FileSwapPath)))
                {
                    extractedFiles[entry.Key] = entry.Value;
                }

                DataApplicationProgress = "Applying MCDF data";

                var extended = await CharaDataMetaInfoExtendedDto.Create(new(charaFile.FilePath, new UserData(string.Empty)), _dalamudUtilService)
                    .ConfigureAwait(false);
                await ApplyDataAsync(applicationId, tempHandler, isSelf, autoRevert: false, extended,
                    extractedFiles, charaFile.CharaFileData.ManipulationData, charaFile.CharaFileData.GlamourerData,
                    charaFile.CharaFileData.CustomizePlusData, CancellationToken.None).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Logger.LogWarning(ex, "Failed to extract MCDF");
                throw;
            }
            finally
            {
                // delete extracted files
                foreach (var file in actuallyExtractedFiles)
                {
                    File.Delete(file);
                }
            }
        });
    }

    public async Task McdfApplyToGposeTarget()
    {
        var apply = await CanApplyInGpose().ConfigureAwait(false);
        if (apply.CanApply)
        {
            McdfApplyToTarget(apply.TargetName);
        }
    }

    public void SaveMareCharaFile(string description, string filePath)
    {
        UiBlockingComputation = Task.Run(async () => await _fileHandler.SaveCharaFileAsync(description, filePath).ConfigureAwait(false));
    }

    public void SetAppearanceData(string dtoId)
    {
        var hasDto = _ownCharaData.TryGetValue(dtoId, out var dto);
        if (!hasDto || dto == null) return;

        var hasUpdateDto = _updateDtos.TryGetValue(dtoId, out var updateDto);
        if (!hasUpdateDto || updateDto == null) return;

        UiBlockingComputation = Task.Run(async () =>
        {
            await _fileHandler.UpdateCharaDataAsync(updateDto).ConfigureAwait(false);
        });
    }

    public Task<HandledCharaDataEntry?> SpawnAndApplyData(CharaDataDownloadDto charaDataDownloadDto)
    {
        var task = Task.Run(async () =>
        {
            var newActor = await _ipcManager.Brio.SpawnActorAsync().ConfigureAwait(false);
            if (newActor == null) return null;
            await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);

            await ApplyCharaData(charaDataDownloadDto, newActor.Name.TextValue).ConfigureAwait(false);

            return _characterHandler.HandledCharaData.GetValueOrDefault(newActor.Name.TextValue);
        });
        UiBlockingComputation = task;
        return task;
    }

    public Task<HandledCharaDataEntry?> SpawnAndApplyData(CharaDataMetaInfoDto charaDataMetaInfoDto)
    {
        var task = Task.Run(async () =>
        {
            var newActor = await _ipcManager.Brio.SpawnActorAsync().ConfigureAwait(false);
            if (newActor == null) return null;
            await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);

            await ApplyCharaData(charaDataMetaInfoDto, newActor.Name.TextValue).ConfigureAwait(false);

            return _characterHandler.HandledCharaData.GetValueOrDefault(newActor.Name.TextValue);
        });
        UiBlockingComputation = task;
        return task;
    }

    private async Task<CharaDataMetaInfoExtendedDto> CacheData(CharaDataFullExtendedDto ownCharaData)
    {
        var metaInfo = new CharaDataMetaInfoDto(ownCharaData.Id, ownCharaData.Uploader)
        {
            Description = ownCharaData.Description,
            UpdatedDate = ownCharaData.UpdatedDate,
            CanBeDownloaded = !string.IsNullOrEmpty(ownCharaData.GlamourerData) && (ownCharaData.OriginalFiles.Count == ownCharaData.FileGamePaths.Count),
            PoseData = ownCharaData.PoseData,
        };

        var extended = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService, isOwnData: true).ConfigureAwait(false);
        _metaInfoCache[extended.FullId] = extended;
        DistributeMetaInfo();

        return extended;
    }

    private async Task<CharaDataMetaInfoExtendedDto> CacheData(CharaDataMetaInfoDto metaInfo, bool isOwnData = false)
    {
        var extended = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService, isOwnData).ConfigureAwait(false);
        _metaInfoCache[extended.FullId] = extended;
        DistributeMetaInfo();

        return extended;
    }

    private readonly SemaphoreSlim _distributionSemaphore = new(1, 1);

    private void DistributeMetaInfo()
    {
        _distributionSemaphore.Wait();
        _nearbyManager.UpdateSharedData(_metaInfoCache.ToDictionary());
        _characterHandler.UpdateHandledData(_metaInfoCache.ToDictionary());
        _distributionSemaphore.Release();
    }

    private void CacheData(CharaDataMetaInfoExtendedDto charaData)
    {
        _metaInfoCache[charaData.FullId] = charaData;
    }

    public bool TryGetMetaInfo(string key, out CharaDataMetaInfoExtendedDto? metaInfo)
    {
        return _metaInfoCache.TryGetValue(key, out metaInfo);
    }

    public void UploadCharaData(string id)
    {
        var hasUpdateDto = _updateDtos.TryGetValue(id, out var updateDto);
        if (!hasUpdateDto || updateDto == null) return;

        UiBlockingComputation = CharaUpdateTask = CharaUpdateAsync(updateDto);
    }

    public void UploadMissingFiles(string id)
    {
        var hasDto = _ownCharaData.TryGetValue(id, out var dto);
        if (!hasDto || dto == null) return;

        UiBlockingComputation = UploadTask = RestoreThenUpload(dto);
    }

    private async Task<(string Output, bool Success)> RestoreThenUpload(CharaDataFullExtendedDto dto)
    {
        var newDto = await _apiController.CharaDataAttemptRestore(dto.Id).ConfigureAwait(false);
        if (newDto == null)
        {
            _ownCharaData.Remove(dto.Id);
            _metaInfoCache.Remove(dto.FullId, out _);
            UiBlockingComputation = null;
            return ("No such DTO found", false);
        }

        await AddOrUpdateDto(newDto).ConfigureAwait(false);
        _ = _ownCharaData.TryGetValue(dto.Id, out var extendedDto);

        if (!extendedDto!.HasMissingFiles)
        {
            UiBlockingComputation = null;
            return ("Restored successfully", true);
        }

        var missingFileList = extendedDto!.MissingFiles.ToList();
        var result = await UploadFiles(missingFileList, async () =>
        {
            var newFilePaths = dto.FileGamePaths;
            foreach (var missing in missingFileList)
            {
                newFilePaths.Add(missing);
            }
            CharaDataUpdateDto updateDto = new(dto.Id)
            {
                FileGamePaths = newFilePaths
            };
            var res = await _apiController.CharaDataUpdate(updateDto).ConfigureAwait(false);
            await AddOrUpdateDto(res).ConfigureAwait(false);
        }).ConfigureAwait(false);

        UiBlockingComputation = null;
        return result;
    }

    internal void ApplyDataToSelf(CharaDataFullExtendedDto dataDto)
    {
        var chara = _dalamudUtilService.GetPlayerName();
        CharaDataDownloadDto downloadDto = new(dataDto.Id, dataDto.Uploader)
        {
            CustomizeData = dataDto.CustomizeData,
            Description = dataDto.Description,
            FileGamePaths = dataDto.FileGamePaths,
            GlamourerData = dataDto.GlamourerData,
            FileSwaps = dataDto.FileSwaps,
            ManipulationData = dataDto.ManipulationData,
            UpdatedDate = dataDto.UpdatedDate
        };

        CharaDataMetaInfoDto metaInfoDto = new(dataDto.Id, dataDto.Uploader)
        {
            CanBeDownloaded = true,
            Description = dataDto.Description,
            PoseData = dataDto.PoseData,
            UpdatedDate = dataDto.UpdatedDate,
        };

        UiBlockingComputation = DataApplicationTask = DownloadAndAplyDataAsync(chara, downloadDto, metaInfoDto);
    }

    internal void AttachPoseData(PoseEntry pose, CharaDataExtendedUpdateDto updateDto)
    {
        AttachingPoseTask = Task.Run(async () =>
        {
            ICharacter? playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
            if (playerChar == null) return;
            if (_dalamudUtilService.IsInGpose)
            {
                playerChar = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(playerChar.Name.TextValue, true).ConfigureAwait(false);
            }
            if (playerChar == null) return;
            var poseData = await _ipcManager.Brio.GetPoseAsync(playerChar.Address).ConfigureAwait(false);
            if (poseData == null) return;

            var compressedByteData = LZ4Wrapper.WrapHC(Encoding.UTF8.GetBytes(poseData));
            pose.PoseData = Convert.ToBase64String(compressedByteData);
            updateDto.UpdatePoseList();
        });
    }

    internal void McdfSpawnApplyToGposeTarget()
    {
        UiBlockingComputation = Task.Run(async () =>
        {
            var newActor = await _ipcManager.Brio.SpawnActorAsync().ConfigureAwait(false);
            if (newActor == null) return;
            await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
            unsafe
            {
                _dalamudUtilService.GposeTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)newActor.Address;
            }

            await McdfApplyToGposeTarget().ConfigureAwait(false);
        });
    }

    internal void ApplyFullPoseDataToTarget(PoseEntry value, string targetName)
    {
        UiBlockingComputation = Task.Run(async () =>
        {
            await ApplyPoseData(value, targetName).ConfigureAwait(false);
            await ApplyWorldDataToTarget(value, targetName).ConfigureAwait(false);
        });
    }

    internal void ApplyFullPoseDataToGposeTarget(PoseEntry value)
    {
        UiBlockingComputation = Task.Run(async () =>
        {
            var apply = await CanApplyInGpose().ConfigureAwait(false);
            if (apply.CanApply)
            {
                await ApplyPoseData(value, apply.TargetName).ConfigureAwait(false);
                await ApplyWorldDataToTarget(value, apply.TargetName).ConfigureAwait(false);
            }
        });
    }

    internal void SpawnAndApplyWorldTransform(CharaDataMetaInfoDto metaInfo, PoseEntry value)
    {
        UiBlockingComputation = Task.Run(async () =>
        {
            var actor = await SpawnAndApplyData(metaInfo).ConfigureAwait(false);
            if (actor == null) return;
            await ApplyPoseData(value, actor.Name).ConfigureAwait(false);
            await ApplyWorldDataToTarget(value, actor.Name).ConfigureAwait(false);
        });
    }

    internal unsafe void TargetGposeActor(HandledCharaDataEntry actor)
    {
        var gposeActor = _dalamudUtilService.GetGposeCharacterFromObjectTableByName(actor.Name, true);
        if (gposeActor != null)
        {
            _dalamudUtilService.GposeTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gposeActor.Address;
        }
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (disposing)
        {
            _getAllDataCts?.Cancel();
            _getAllDataCts?.Dispose();
            _getSharedDataCts?.Cancel();
            _getSharedDataCts?.Dispose();
            _charaDataCreateCts?.Cancel();
            _charaDataCreateCts?.Dispose();
            _uploadCts?.Cancel();
            _uploadCts?.Dispose();
            _applicationCts.Cancel();
            _applicationCts.Dispose();
            _connectCts?.Cancel();
            _connectCts?.Dispose();
        }
    }

    private async Task AddOrUpdateDto(CharaDataFullDto? dto)
    {
        if (dto == null) return;

        _ownCharaData[dto.Id] = new(dto);
        _updateDtos[dto.Id] = new(new(dto.Id), _ownCharaData[dto.Id]);

        await CacheData(_ownCharaData[dto.Id]).ConfigureAwait(false);
    }

    private async Task ApplyDataAsync(Guid applicationId, GameObjectHandler tempHandler, bool isSelf, bool autoRevert,
        CharaDataMetaInfoExtendedDto metaInfo, Dictionary<string, string> modPaths, string? manipData, string? glamourerData, string? customizeData, CancellationToken token)
    {
        Guid? cPlusId = null;
        Guid penumbraCollection;
        try
        {
            DataApplicationProgress = "Reverting previous Application";

            Logger.LogTrace("[{appId}] Reverting chara {chara}", applicationId, tempHandler.Name);
            bool reverted = await _characterHandler.RevertHandledChara(tempHandler.Name).ConfigureAwait(false);
            if (reverted)
                await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);

            Logger.LogTrace("[{appId}] Applying data in Penumbra", applicationId);

            DataApplicationProgress = "Applying Penumbra information";
            penumbraCollection = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, metaInfo.Uploader.UID + metaInfo.Id).ConfigureAwait(false);
            var idx = await _dalamudUtilService.RunOnFrameworkThread(() => tempHandler.GetGameObject()?.ObjectIndex).ConfigureAwait(false) ?? 0;
            await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, idx).ConfigureAwait(false);
            await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, applicationId, penumbraCollection, modPaths).ConfigureAwait(false);
            await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, applicationId, penumbraCollection, manipData ?? string.Empty).ConfigureAwait(false);

            Logger.LogTrace("[{appId}] Applying Glamourer data and Redrawing", applicationId);
            DataApplicationProgress = "Applying Glamourer and redrawing Character";
            await _ipcManager.Glamourer.ApplyAllAsync(Logger, tempHandler, glamourerData, applicationId, token).ConfigureAwait(false);
            await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, token).ConfigureAwait(false);
            await _dalamudUtilService.WaitWhileCharacterIsDrawing(Logger, tempHandler, applicationId, ct: token).ConfigureAwait(false);
            Logger.LogTrace("[{appId}] Removing collection", applicationId);
            await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, penumbraCollection).ConfigureAwait(false);

            DataApplicationProgress = "Applying Customize+ data";
            Logger.LogTrace("[{appId}] Appplying C+ data", applicationId);

            if (!string.IsNullOrEmpty(customizeData))
            {
                cPlusId = await _ipcManager.CustomizePlus.SetBodyScaleAsync(tempHandler.Address, customizeData).ConfigureAwait(false);
            }
            else
            {
                cPlusId = await _ipcManager.CustomizePlus.SetBodyScaleAsync(tempHandler.Address, Convert.ToBase64String(Encoding.UTF8.GetBytes("{}"))).ConfigureAwait(false);
            }

            if (autoRevert)
            {
                Logger.LogTrace("[{appId}] Starting wait for auto revert", applicationId);

                int i = 15;
                while (i > 0)
                {
                    DataApplicationProgress = $"All data applied. Reverting automatically in {i} seconds.";
                    await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
                    i--;
                }
            }
            else
            {
                Logger.LogTrace("[{appId}] Adding {name} to handled objects", applicationId, tempHandler.Name);

                _characterHandler.AddHandledChara(new HandledCharaDataEntry(tempHandler.Name, isSelf, cPlusId, metaInfo));
            }
        }
        finally
        {
            if (token.IsCancellationRequested)
                DataApplicationProgress = "Application aborted. Reverting Character...";
            else if (autoRevert)
                DataApplicationProgress = "Application finished. Reverting Character...";
            if (autoRevert)
            {
                await _characterHandler.RevertChara(tempHandler.Name, cPlusId).ConfigureAwait(false);
            }

            if (!_dalamudUtilService.IsInGpose)
                Mediator.Publish(new HaltCharaDataCreation(Resume: true));

            if (metaInfo != null && _configService.Current.FavoriteCodes.TryGetValue(metaInfo.Uploader.UID + ":" + metaInfo.Id, out var favorite) && favorite != null)
            {
                favorite.LastDownloaded = DateTime.UtcNow;
                _configService.Save();
            }

            DataApplicationTask = null;
            DataApplicationProgress = string.Empty;
        }
    }

    private async Task CharaUpdateAsync(CharaDataExtendedUpdateDto updateDto)
    {
        Logger.LogDebug("Uploading Chara Data to Server");
        var baseUpdateDto = updateDto.BaseDto;
        if (baseUpdateDto.FileGamePaths != null)
        {
            Logger.LogDebug("Detected file path changes, starting file upload");

            UploadTask = UploadFiles(baseUpdateDto.FileGamePaths);
            var result = await UploadTask.ConfigureAwait(false);
            if (!result.Success)
            {
                return;
            }
        }

        Logger.LogDebug("Pushing update dto to server: {data}", baseUpdateDto);

        var res = await _apiController.CharaDataUpdate(baseUpdateDto).ConfigureAwait(false);
        await AddOrUpdateDto(res).ConfigureAwait(false);
        CharaUpdateTask = null;
    }

    private async Task DownloadAndAplyDataAsync(string charaName, CharaDataDownloadDto charaDataDownloadDto, CharaDataMetaInfoDto metaInfo, bool autoRevert = true)
    {
        _applicationCts = _applicationCts.CancelRecreate();
        var token = _applicationCts.Token;
        ICharacter? chara = (await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(charaName, _dalamudUtilService.IsInGpose).ConfigureAwait(false));

        if (chara == null)
            return;

        var applicationId = Guid.NewGuid();

        var playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
        bool isSelf = playerChar != null && string.Equals(playerChar.Name.TextValue, chara.Name.TextValue, StringComparison.Ordinal);

        DataApplicationProgress = "Checking local files";

        Logger.LogTrace("[{appId}] Computing local missing files", applicationId);

        Dictionary<string, string> modPaths;
        List<FileReplacementData> missingFiles;
        _fileHandler.ComputeMissingFiles(charaDataDownloadDto, out modPaths, out missingFiles);

        Logger.LogTrace("[{appId}] Computing local missing files", applicationId);

        using GameObjectHandler? tempHandler = await _characterHandler.TryCreateGameObjectHandler(chara.ObjectIndex).ConfigureAwait(false);
        if (tempHandler == null) return;

        if (missingFiles.Any())
        {
            try
            {
                DataApplicationProgress = "Downloading Missing Files. Please be patient.";
                await _fileHandler.DownloadFilesAsync(tempHandler, missingFiles, modPaths, token).ConfigureAwait(false);
            }
            catch (FileNotFoundException)
            {
                DataApplicationProgress = "Failed to download one or more files. Aborting.";
                DataApplicationTask = null;
                return;
            }
            catch (OperationCanceledException)
            {
                DataApplicationProgress = "Application aborted.";
                DataApplicationTask = null;
                return;
            }
        }

        if (!_dalamudUtilService.IsInGpose)
            Mediator.Publish(new HaltCharaDataCreation());

        var extendedMetaInfo = await CacheData(metaInfo).ConfigureAwait(false);

        await ApplyDataAsync(applicationId, tempHandler, isSelf, autoRevert, extendedMetaInfo, modPaths, charaDataDownloadDto.ManipulationData, charaDataDownloadDto.GlamourerData,
            charaDataDownloadDto.CustomizeData, token).ConfigureAwait(false);
    }

    public async Task<(string Result, bool Success)> UploadFiles(List<GamePathEntry> missingFileList, Func<Task>? postUpload = null)
    {
        UploadProgress = new ValueProgress<string>();
        try
        {
            _uploadCts = _uploadCts.CancelRecreate();
            var missingFiles = await _fileHandler.UploadFiles([.. missingFileList.Select(k => k.HashOrFileSwap)], UploadProgress, _uploadCts.Token).ConfigureAwait(false);
            if (missingFiles.Any())
            {
                Logger.LogInformation("Failed to upload {files}", string.Join(", ", missingFiles));
                return ($"Upload failed: {missingFiles.Count} missing or forbidden to upload local files.", false);
            }

            if (postUpload != null)
                await postUpload.Invoke().ConfigureAwait(false);

            return ("Upload sucessful", true);
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "Error during upload");
            if (ex is OperationCanceledException)
            {
                return ("Upload Cancelled", false);
            }
            return ("Error in upload, see log for more details", false);
        }
        finally
        {
            UiBlockingComputation = null;
        }
    }

    public void RevertChara(HandledCharaDataEntry? handled)
    {
        UiBlockingComputation = _characterHandler.RevertHandledChara(handled);
    }

    internal void RemoveChara(string handledActor)
    {
        if (string.IsNullOrEmpty(handledActor)) return;
        UiBlockingComputation = Task.Run(async () =>
        {
            await _characterHandler.RevertHandledChara(handledActor).ConfigureAwait(false);
            var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(handledActor, true).ConfigureAwait(false);
            if (gposeChara != null)
                await _ipcManager.Brio.DespawnActorAsync(gposeChara.Address).ConfigureAwait(false);
        });
    }
}
