using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Mapster;
using PhoenixLib.Caching;
using PhoenixLib.Logging;
using WingsAPI.Communication.Families;
using WingsAPI.Data.Families;
using WingsAPI.Game.Extensions.Families;
using WingsAPI.Packets.Enums.Families;
using WingsEmu.Core.Extensions;
using WingsEmu.DTOs.Items;

namespace FamilyServer.Managers
{
    public class FamilyWarehouseManager : IFamilyWarehouseManager
    {
        private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.FamilyServerSaveIntervalMinutes) ?? "5") * 3);

        private readonly ILongKeyCachedRepository<Dictionary<short, FamilyWarehouseItemDto>> _cachedFamilyItems;
        private readonly SemaphoreSlim _familyLock = new(1, 1);
        private readonly IFamilyService _familyService;

        private readonly IFamilyWarehouseItemDao _familyWarehouseItemDao;
        private readonly FamilyWarehouseLogManager _familyWarehouseLogManager;

        private readonly Dictionary<long, Dictionary<short, (FamilyWarehouseItemDto dto, bool remove)>> _itemChanges = new();
        private readonly SemaphoreSlim _itemChangesSemaphore = new(1, 1);

        public FamilyWarehouseManager(IFamilyWarehouseItemDao familyWarehouseItemDao, ILongKeyCachedRepository<Dictionary<short, FamilyWarehouseItemDto>> cachedFamilyItems,
            IFamilyService familyService,
            FamilyWarehouseLogManager familyWarehouseLogManager)
        {
            _familyWarehouseItemDao = familyWarehouseItemDao;
            _cachedFamilyItems = cachedFamilyItems;
            _familyService = familyService;
            _familyWarehouseLogManager = familyWarehouseLogManager;
        }

        public async Task<IList<FamilyWarehouseLogEntryDto>> GetWarehouseLogs(long familyId, long? characterId = null)
        {
            if (!await CheckLogHistoryPermission(familyId, characterId))
            {
                return null;
            }

            return await _familyWarehouseLogManager.GetLogs(familyId);
        }

        public async Task<IEnumerable<FamilyWarehouseItemDto>> GetWarehouse(long familyId, long? characterId = null)
        {
            if (!await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put))
            {
                return null;
            }

            await _familyLock.WaitAsync();
            try
            {
                return (await GetFamilyWarehouse(familyId))?.Values;
            }
            finally
            {
                _familyLock.Release();
            }
        }

        public async Task<FamilyWarehouseItemDto> GetWarehouseItem(long familyId, short slot, long? characterId = null)
        {
            if (!await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put))
            {
                return null;
            }

            await _familyLock.WaitAsync();
            try
            {
                return (await GetFamilyWarehouse(familyId))?.GetValueOrDefault(slot);
            }
            finally
            {
                _familyLock.Release();
            }
        }

        public async Task<AddWarehouseItemResult> AddWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToAdd, long? characterId = null, string characterName = null)
        {
            long familyId = warehouseItemDtoToAdd.FamilyId;

            if (warehouseItemDtoToAdd.ItemInstance.Amount is < 1 or > 999 || !await CheckSlot(familyId, warehouseItemDtoToAdd.Slot) ||
                !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put))
            {
                return new AddWarehouseItemResult
                {
                    Success = false
                };
            }

            await _familyLock.WaitAsync();
            try
            {
                Dictionary<short, FamilyWarehouseItemDto> familyWarehouse = await GetFamilyWarehouse(familyId);
                if (familyWarehouse == null)
                {
                    return new AddWarehouseItemResult
                    {
                        Success = false
                    };
                }

                FamilyWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToAdd.Slot);

                if (alreadyExistentItem == null)
                {
                    familyWarehouse[warehouseItemDtoToAdd.Slot] = warehouseItemDtoToAdd;
                    await SetItemChangeWithLock(warehouseItemDtoToAdd, false);
                    await AddLog(familyId, warehouseItemDtoToAdd.ItemInstance, FamilyWarehouseLogEntryType.List, characterId, characterName);
                    return new AddWarehouseItemResult
                    {
                        Success = true,
                        UpdatedItem = warehouseItemDtoToAdd
                    };
                }

                if (warehouseItemDtoToAdd.ItemInstance.ItemVNum != alreadyExistentItem.ItemInstance.ItemVNum)
                {
                    return new AddWarehouseItemResult
                    {
                        Success = false
                    };
                }

                if (warehouseItemDtoToAdd.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || alreadyExistentItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM
                    || warehouseItemDtoToAdd.ItemInstance.Amount + alreadyExistentItem.ItemInstance.Amount > 999)
                {
                    return new AddWarehouseItemResult
                    {
                        Success = false
                    };
                }

                alreadyExistentItem.ItemInstance.Amount += warehouseItemDtoToAdd.ItemInstance.Amount;

                await SetItemChangeWithLock(alreadyExistentItem, false);
                await AddLog(familyId, warehouseItemDtoToAdd.ItemInstance, FamilyWarehouseLogEntryType.List, characterId, characterName);
                return new AddWarehouseItemResult
                {
                    Success = true,
                    UpdatedItem = alreadyExistentItem
                };
            }
            finally
            {
                _familyLock.Release();
            }
        }

        public async Task<WithdrawWarehouseItemResult> WithdrawWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToWithdraw, int amount, long? characterId = null, string characterName = null)
        {
            long familyId = warehouseItemDtoToWithdraw.FamilyId;

            if (amount is < 1 or > 999 || !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.PutAndWithdraw))
            {
                return new WithdrawWarehouseItemResult
                {
                    Success = false
                };
            }

            await _familyLock.WaitAsync();
            try
            {
                Dictionary<short, FamilyWarehouseItemDto> familyWarehouse = await GetFamilyWarehouse(familyId);

                if (familyWarehouse == null)
                {
                    return new WithdrawWarehouseItemResult
                    {
                        Success = false
                    };
                }

                FamilyWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToWithdraw.Slot);
                if (alreadyExistentItem == null || alreadyExistentItem.ItemInstance.Amount < amount)
                {
                    return new WithdrawWarehouseItemResult
                    {
                        Success = false
                    };
                }

                alreadyExistentItem.ItemInstance.Amount -= amount;

                bool toRemove = alreadyExistentItem.ItemInstance.Amount == 0;
                if (toRemove)
                {
                    familyWarehouse.Remove(warehouseItemDtoToWithdraw.Slot);
                }

                await SetItemChangeWithLock(alreadyExistentItem, toRemove);

                ItemInstanceDTO newItemInstance = alreadyExistentItem.ItemInstance.Adapt<ItemInstanceDTO>();
                newItemInstance.Amount = amount;

                await AddLog(familyId, newItemInstance, FamilyWarehouseLogEntryType.Withdraw, characterId, characterName);

                return new WithdrawWarehouseItemResult
                {
                    Success = true,
                    UpdatedItem = alreadyExistentItem.ItemInstance.Amount == 0 ? null : alreadyExistentItem,
                    WithdrawnItem = newItemInstance
                };
            }
            finally
            {
                _familyLock.Release();
            }
        }

        public async Task<MoveWarehouseItemResult> MoveWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot, long? characterId = null)
        {
            long familyId = warehouseItemDtoToMove.FamilyId;

            if (amount is < 1 or > 999 || !await CheckSlot(familyId, warehouseItemDtoToMove.Slot, newSlot) ||
                !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put))
            {
                return new MoveWarehouseItemResult
                {
                    Success = false
                };
            }

            await _familyLock.WaitAsync();
            try
            {
                Dictionary<short, FamilyWarehouseItemDto> familyWarehouse = await GetFamilyWarehouse(familyId);
                if (familyWarehouse == null)
                {
                    return new MoveWarehouseItemResult
                    {
                        Success = false
                    };
                }

                FamilyWarehouseItemDto toMoveItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToMove.Slot);
                FamilyWarehouseItemDto toMergeItem = familyWarehouse.GetValueOrDefault(newSlot);
                if (toMoveItem == null || toMoveItem.ItemInstance.Amount < amount)
                {
                    return new MoveWarehouseItemResult
                    {
                        Success = false
                    };
                }

                if (toMergeItem == null)
                {
                    if (amount == toMoveItem.ItemInstance.Amount)
                    {
                        familyWarehouse.Remove(toMoveItem.Slot);
                        await SetItemChangeWithLock(new FamilyWarehouseItemDto
                        {
                            FamilyId = familyId,
                            Slot = toMoveItem.Slot
                        }, true);
                        toMoveItem.Slot = newSlot;
                        familyWarehouse[toMoveItem.Slot] = toMoveItem;
                        await SetItemChangeWithLock(toMoveItem, false);
                        return new MoveWarehouseItemResult
                        {
                            Success = true,
                            OldItem = null,
                            NewItem = toMoveItem
                        };
                    }

                    toMoveItem.ItemInstance.Amount -= amount;
                    await SetItemChangeWithLock(toMoveItem, false);
                    var newItem = new FamilyWarehouseItemDto
                    {
                        FamilyId = familyId,
                        ItemInstance = toMoveItem.ItemInstance.Adapt<ItemInstanceDTO>(),
                        Slot = newSlot
                    };
                    newItem.ItemInstance.Amount = amount;
                    familyWarehouse[newItem.Slot] = newItem;
                    await SetItemChangeWithLock(newItem, false);
                    return new MoveWarehouseItemResult
                    {
                        Success = true,
                        OldItem = toMoveItem,
                        NewItem = newItem
                    };
                }

                if (toMoveItem.ItemInstance.ItemVNum != toMergeItem.ItemInstance.ItemVNum)
                {
                    toMergeItem.Slot = toMoveItem.Slot;
                    toMoveItem.Slot = newSlot;
                    familyWarehouse[toMoveItem.Slot] = toMoveItem;
                    await SetItemChangeWithLock(toMoveItem, false);
                    familyWarehouse[toMergeItem.Slot] = toMergeItem;
                    await SetItemChangeWithLock(toMergeItem, false);
                    return new MoveWarehouseItemResult
                    {
                        Success = true,
                        OldItem = toMergeItem,
                        NewItem = toMoveItem
                    };
                }

                if (toMoveItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || toMergeItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || amount + toMergeItem.ItemInstance.Amount > 999)
                {
                    return new MoveWarehouseItemResult
                    {
                        Success = false
                    };
                }

                toMoveItem.ItemInstance.Amount -= amount;
                toMergeItem.ItemInstance.Amount += amount;

                bool toRemove = toMoveItem.ItemInstance.Amount == 0;
                if (toRemove)
                {
                    familyWarehouse.Remove(toMoveItem.Slot);
                }

                await SetItemChangeWithLock(toMoveItem, toRemove);
                await SetItemChangeWithLock(toMergeItem, false);

                return new MoveWarehouseItemResult
                {
                    Success = true,
                    OldItem = toRemove ? null : toMoveItem,
                    NewItem = toMergeItem
                };
            }
            finally
            {
                _familyLock.Release();
            }
        }

        public async Task FlushWarehouseSaves()
        {
            if (_itemChanges.Count < 1)
            {
                return;
            }

            await _itemChangesSemaphore.WaitAsync();
            try
            {
                List<(FamilyWarehouseItemDto dto, bool remove)> unsavedChanges = new();

                var globalWatch = Stopwatch.StartNew();
                foreach ((long familyId, Dictionary<short, (FamilyWarehouseItemDto dto, bool remove)> warehouseChanges) in _itemChanges)
                {
                    List<FamilyWarehouseItemDto> itemsToSave = new();
                    List<FamilyWarehouseItemDto> itemsToRemove = new();

                    foreach ((short _, (FamilyWarehouseItemDto dto, bool remove)) in warehouseChanges)
                    {
                        (remove ? itemsToRemove : itemsToSave).Add(dto);
                    }

                    if (itemsToSave.Count > 0)
                    {
                        try
                        {
                            int countSavedItems = await _familyWarehouseItemDao.SaveAsync(itemsToSave);
                            Log.Warn($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Saved {countSavedItems.ToString()} warehouseItems");
                        }
                        catch (Exception e)
                        {
                            Log.Error(
                                $"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Error while trying to save {itemsToSave.Count.ToString()} warehouseItems. Re-queueing. ",
                                e);
                            unsavedChanges.AddRange(itemsToSave.Select(x => (x, false)));
                        }
                    }

                    if (itemsToRemove.Count < 1)
                    {
                        continue;
                    }

                    try
                    {
                        await _familyWarehouseItemDao.DeleteAsync(itemsToRemove);
                        Log.Warn($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Removed (at maximum) {itemsToRemove.Count.ToString()} warehouseItems");
                    }
                    catch (Exception e)
                    {
                        Log.Error(
                            $"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Error while trying to remove {itemsToRemove.Count.ToString()} warehouseItems. Re-queueing. ",
                            e);
                        unsavedChanges.AddRange(itemsToRemove.Select(x => (x, true)));
                    }
                }

                globalWatch.Stop();
                Log.Debug($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES] Saving all warehouses took {globalWatch.ElapsedMilliseconds.ToString()}ms");

                _itemChanges.Clear();

                foreach ((FamilyWarehouseItemDto dto, bool remove) in unsavedChanges)
                {
                    SetItemChange(dto, remove);
                }
            }
            finally
            {
                _itemChangesSemaphore.Release();
            }

            await _familyWarehouseLogManager.FlushLogSaves();
        }

        private async Task<bool> CheckSlot(long familyId, short slot, short? slot2 = null)
        {
            FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest
            {
                FamilyId = familyId
            });

            if (family == null)
            {
                return false;
            }

            short warehouseCapacity = family.Family.Upgrades.UpgradeValues.GetValueOrDefault(FamilyUpgradeType.INCREASE_FAMILY_WAREHOUSE);
            if (slot < 0 || warehouseCapacity <= slot)
            {
                return false;
            }

            if (slot2 < 0 || warehouseCapacity <= slot2)
            {
                return false;
            }

            return true;
        }

        private async Task<bool> CheckPutWithdrawPermission(long familyId, long? characterId, FamilyWarehouseAuthorityType authorityRequested)
        {
            if (characterId == null)
            {
                return true;
            }

            FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest
            {
                FamilyId = familyId
            });

            FamilyMembershipDto member = family?.Members.Find(x => x.CharacterId == characterId);

            return member != null && member.CheckPutWithdrawPermission(family.Family, authorityRequested);
        }

        private async Task<bool> CheckLogHistoryPermission(long familyId, long? characterId)
        {
            if (characterId == null)
            {
                return true;
            }

            FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest
            {
                FamilyId = familyId
            });

            FamilyMembershipDto member = family?.Members.Find(x => x.CharacterId == characterId);

            return member != null && member.CheckLogHistoryPermission(family.Family);
        }

        private async Task SetItemChangeWithLock(FamilyWarehouseItemDto dto, bool remove)
        {
            await _itemChangesSemaphore.WaitAsync();
            try
            {
                SetItemChange(dto, remove);
            }
            finally
            {
                _itemChangesSemaphore.Release();
            }
        }

        /// <summary>
        ///     Not to be used outside SemaphoreSlim
        /// </summary>
        private void SetItemChange(FamilyWarehouseItemDto dto, bool remove)
        {
            _itemChanges.GetOrSetDefault(dto.FamilyId, new Dictionary<short, (FamilyWarehouseItemDto dto, bool remove)>())[dto.Slot] = (dto, remove);
        }

        private async Task<Dictionary<short, FamilyWarehouseItemDto>> GetFamilyWarehouse(long familyId)
        {
            Dictionary<short, FamilyWarehouseItemDto> cachedItems = _cachedFamilyItems.Get(familyId);
            if (cachedItems != null)
            {
                return cachedItems;
            }

            cachedItems = (await _familyWarehouseItemDao.GetByFamilyIdAsync(familyId))?.ToDictionary(x => x.Slot);
            _cachedFamilyItems.Set(familyId, cachedItems ?? new Dictionary<short, FamilyWarehouseItemDto>(), LifeTime);
            return cachedItems;
        }

        private async Task AddLog(long familyId, ItemInstanceDTO item, FamilyWarehouseLogEntryType logEntryType, long? characterId, string characterName)
        {
            await _familyWarehouseLogManager.AddLog(familyId, new FamilyWarehouseLogEntryDto
            {
                CharacterId = characterId ?? -1,
                CharacterName = characterName,
                DateOfLog = DateTime.UtcNow,
                Type = logEntryType,
                ItemVnum = item.ItemVNum,
                Amount = item.Amount
            });
        }
    }
}