using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using PhoenixLib.Logging;
using WingsAPI.Communication.ServerApi.Protocol;
using WingsAPI.Data.GameData;
using WingsEmu.Core.Extensions;
using WingsEmu.Core.Generics;
using WingsEmu.DTOs.Maps;
using WingsEmu.DTOs.ServerDatas;
using WingsEmu.Game.Configurations;
using WingsEmu.Game.Entities;
using WingsEmu.Game.Entities.Event;
using WingsEmu.Game.Extensions;
using WingsEmu.Game.Helpers.Damages;
using WingsEmu.Game.Managers.ServerData;
using WingsEmu.Game.Maps;
using WingsEmu.Game.Networking;
using WingsEmu.Game.Portals;
using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects;
using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Maps;
using WingsEmu.Plugins.BasicImplementations.ServerConfigs.ImportObjects.Portals;

namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs;

public class MapManager : IMapManager
{
    private readonly Dictionary<int, IMapInstance> _baseMapInstancesByMapId = new();
    private readonly SerializableGameServer _gameServer;
    private readonly Dictionary<int, ServerMapDto> _mapConfigByMapId = new();
    private readonly IEnumerable<ConfiguredMapImportFile> _mapConfigurations;

    private readonly Dictionary<int, MapDataDTO> _mapDataByMapVNum = new();
    private readonly Dictionary<int, HashSet<MapFlags>> _mapFlags = new();
    private readonly IMapInstanceFactory _mapInstanceFactory;
    private readonly ConcurrentDictionary<MapFlags, ThreadSafeList<IMapInstance>> _mapInstancesByFlags = new();
    private readonly ConcurrentDictionary<Guid, IMapInstance> _mapInstancesByGuid = new();
    private readonly IResourceLoader<MapDataDTO> _mapLoader;
    private readonly IMapMonsterManager _mapMonsterManager;
    private readonly IMapNpcManager _mapNpcManager;
    private readonly IMonsterEntityFactory _monsterEntityFactory;
    private readonly INpcEntityFactory _npcEntityFactory;
    private readonly IEnumerable<PortalImportFile> _portalConfigurationFiles;
    private readonly Dictionary<int, List<PortalDTO>> _portalDataByMapId = new();
    private readonly ITimeSpaceConfiguration _timeSpaceConfiguration;
    private readonly ITimeSpacePortalFactory _timeSpacePortalFactory;

    public MapManager(IEnumerable<PortalImportFile> portalConfigurationFiles, IResourceLoader<MapDataDTO> mapLoader, IEnumerable<ConfiguredMapImportFile> mapConfigurations,
        SerializableGameServer gameServer, IMapInstanceFactory mapInstanceFactory,
        IMonsterEntityFactory monsterEntityFactory, INpcEntityFactory npcEntityFactory, IMapMonsterManager mapMonsterManager, IMapNpcManager mapNpcManager,
        ITimeSpaceConfiguration timeSpaceConfiguration, ITimeSpacePortalFactory timeSpacePortalFactory)
    {
        _portalConfigurationFiles = portalConfigurationFiles;
        _mapLoader = mapLoader;
        _mapConfigurations = mapConfigurations;
        _gameServer = gameServer;
        _mapInstanceFactory = mapInstanceFactory;
        _monsterEntityFactory = monsterEntityFactory;
        _npcEntityFactory = npcEntityFactory;
        _mapMonsterManager = mapMonsterManager;
        _mapNpcManager = mapNpcManager;
        _timeSpaceConfiguration = timeSpaceConfiguration;
        _timeSpacePortalFactory = timeSpacePortalFactory;
    }

    public async Task Initialize()
    {
        int count = 0;
        IEnumerable<MapDataDTO> maps = await _mapLoader.LoadAsync();
        foreach (MapDataDTO map in maps)
        {
            _mapDataByMapVNum[map.Id] = map;
            count++;
        }

        Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapClientData");

        count = 0;
        IEnumerable<PortalDTO> portals = _portalConfigurationFiles.SelectMany(s => s.Portals.Select(p => p.ToDto())).ToList();
        foreach (PortalDTO portal in portals)
        {
            if (!_portalDataByMapId.TryGetValue(portal.SourceMapId, out List<PortalDTO> portalDtos))
            {
                portalDtos = _portalDataByMapId[portal.SourceMapId] = new List<PortalDTO>();
            }

            portalDtos.Add(portal);
            count++;
        }

        Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapPortals");
        DateTime initTime = DateTime.UtcNow;
        count = 0;
        int countBaseMaps = 0;
        var configuredMaps = _mapConfigurations.SelectMany(s => s.Select(p => p.ToDto())).ToList();
        foreach (ServerMapDto configuredMap in configuredMaps)
        {
            // Load MapFlags at the beginning
            _mapFlags[configuredMap.Id] = configuredMap.Flags.ToHashSet();

            if (_gameServer.ChannelType == GameChannelType.ACT_4
                    ? !configuredMap.Flags.Contains(MapFlags.ACT_4) && !configuredMap.Flags.Contains(MapFlags.IS_MINILAND_MAP)
                    : configuredMap.Flags.Contains(MapFlags.ACT_4))
            {
                continue;
            }

            _mapConfigByMapId[configuredMap.Id] = configuredMap;
            count++;

            if (!configuredMap.Flags.Contains(MapFlags.IS_BASE_MAP))
            {
                continue;
            }

            IMapInstance mapInstance = GenerateMapInstanceByMapId(configuredMap.Id, MapInstanceType.BaseMapInstance);
            mapInstance.Initialize(initTime);
            _baseMapInstancesByMapId[configuredMap.Id] = mapInstance;
            countBaseMaps++;
        }

        Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapConfigurations");
        Log.Info($"[MAP_MANAGER] Instantiated {countBaseMaps.ToString()} BaseMaps");
    }

    public MapDataDTO GetMapByMapId(int mapId) => _mapDataByMapVNum.GetOrDefault(mapId);

    public IMapInstance GenerateMapInstanceByMapId(int mapId, MapInstanceType type)
    {
        if (!_mapConfigByMapId.TryGetValue(mapId, out ServerMapDto mapConfiguration))
        {
            Log.Warn($"[MAP_MANAGER] Couldn't find a ServerMapDto/MapConfiguration while trying to generate a MapInstance with MapId: '{mapId.ToString()}'");
            return null;
        }

        if (!_mapDataByMapVNum.TryGetValue(mapConfiguration.MapVnum, out MapDataDTO map))
        {
            Log.Warn($"[MAP_MANAGER] Couldn't find a MapDataDto while trying to generate a MapInstance with MapVNum: '{mapConfiguration.MapVnum.ToString()}'");
            return null;
        }

        IMapInstance mapInstance = _mapInstanceFactory.CreateMap(new Map
        {
            Flags = mapConfiguration.Flags,
            Grid = map.Grid,
            Width = map.Width,
            Height = map.Height,
            MapId = mapConfiguration.Id,
            MapVnum = map.Id,
            MapNameId = mapConfiguration.NameId,
            Music = mapConfiguration.MusicId
        }, type);

        if (_portalDataByMapId.TryGetValue(mapId, out List<PortalDTO> portals))
        {
            mapInstance.LoadPortals(portals);
        }

        IEnumerable<TimeSpaceFileConfiguration> timeSpacePortals = _timeSpaceConfiguration.GetTimeSpaceConfigurationsByMapId(mapId);
        if (timeSpacePortals != null)
        {
            foreach (TimeSpaceFileConfiguration timeSpacePortal in timeSpacePortals)
            {
                foreach (TimeSpacePlacement placement in timeSpacePortal.Placement.Where(x => x.MapId == mapId))
                {
                    ITimeSpacePortalEntity portal = _timeSpacePortalFactory.CreateTimeSpacePortal(timeSpacePortal, new Position(placement.MapX, placement.MapY));
                    mapInstance.TimeSpacePortals.Add(portal);
                }
            }
        }

        IEnumerable<MapMonsterDTO> monsters = _mapMonsterManager.GetByMapId(mapId);
        if (monsters != null)
        {
            foreach (MapMonsterDTO monster in monsters)
            {
                try
                {
                    IMonsterEntity mapMonster = _monsterEntityFactory.CreateMapMonster(monster, mapInstance);
                    mapMonster.EmitEvent(new MapJoinMonsterEntityEvent(mapMonster));
                }
                catch
                {
                    Log.Warn("[MOB] Couldn't load monster: " + monster.MonsterVNum + $" on map: {mapId}");
                }
            }
        }

        IEnumerable<MapNpcDTO> npcs = _mapNpcManager.GetByMapId(mapId);
        if (npcs != null)
        {
            foreach (MapNpcDTO npc in npcs)
            {
                try
                {
                    INpcEntity npcEntity = _npcEntityFactory.CreateMapNpc(npc, mapInstance);
                    npcEntity.EmitEvent(new MapJoinNpcEntityEvent(npcEntity));
                }
                catch
                {
                    Log.Warn("[NPC] Couldn't load NPC: " + npc.NpcVNum + $" on map: {mapId}");
                }
            }
        }

        _mapInstancesByGuid.TryAdd(mapInstance.Id, mapInstance);
        foreach (MapFlags flag in mapConfiguration.Flags)
        {
            _mapInstancesByFlags.GetOrAdd(flag, new ThreadSafeList<IMapInstance>()).Add(mapInstance);
        }

        return mapInstance;
    }

    public IMapInstance GenerateMapInstanceByMapVNum(ServerMapDto serverMapDto, MapInstanceType type)
    {
        if (!_mapDataByMapVNum.TryGetValue(serverMapDto.MapVnum, out MapDataDTO map))
        {
            Log.Warn($"[MAP_MANAGER] Couldn't find a MapDataDto while trying to generate a MapInstance with MapVNum: '{serverMapDto.MapVnum.ToString()}'");
            return null;
        }

        IMapInstance mapInstance = _mapInstanceFactory.CreateMap(new Map
        {
            Flags = serverMapDto.Flags,
            Grid = map.Grid,
            Width = map.Width,
            Height = map.Height,
            MapId = serverMapDto.Id,
            MapVnum = map.Id,
            MapNameId = serverMapDto.NameId,
            Music = serverMapDto.MusicId
        }, type);

        _mapInstancesByGuid.TryAdd(mapInstance.Id, mapInstance);
        foreach (MapFlags flag in serverMapDto.Flags)
        {
            _mapInstancesByFlags.GetOrAdd(flag, new ThreadSafeList<IMapInstance>()).Add(mapInstance);
        }

        return mapInstance;
    }

    public async Task TeleportOnRandomPlaceInMapAsync(IClientSession session, IMapInstance mapInstance, bool isSameMap = false)
    {
        if (mapInstance == default)
        {
            return;
        }

        Position pos = mapInstance.GetRandomPosition();

        switch (isSameMap)
        {
            case false:
                session.ChangeMap(mapInstance, pos.X, pos.Y);
                break;
            case true:
                session.PlayerEntity.TeleportOnMap(pos.X, pos.Y);
                break;
        }
    }

    public IEnumerable<IMapInstance> GetMapsWithFlag(MapFlags flags)
    {
        if (!_mapInstancesByFlags.TryGetValue(flags, out ThreadSafeList<IMapInstance> maps) || maps == null)
        {
            return Array.Empty<IMapInstance>();
        }

        return maps;
    }

    public IMapInstance GetMapInstance(Guid id) => _mapInstancesByGuid.GetOrDefault(id);

    public IMapInstance GetBaseMapInstanceByMapId(int mapId) => _baseMapInstancesByMapId.GetOrDefault(mapId);

    public Guid GetBaseMapInstanceIdByMapId(int mapId) => _baseMapInstancesByMapId.TryGetValue(mapId, out IMapInstance map) ? map.Id : Guid.Empty;

    public bool HasMapFlagByMapId(int mapId, MapFlags mapFlag)
    {
        if (!_mapFlags.TryGetValue(mapId, out HashSet<MapFlags> mapFlags))
        {
            return false;
        }

        return mapFlags != null && mapFlags.Contains(mapFlag);
    }

    public IReadOnlyList<MapFlags> GetMapFlagByMapId(int mapId) => _mapFlags.TryGetValue(mapId, out HashSet<MapFlags> mapFlags) ? mapFlags.ToList() : Array.Empty<MapFlags>();

    public void RemoveMapInstance(Guid mapId)
    {
        if (!_mapInstancesByGuid.TryRemove(mapId, out IMapInstance map))
        {
            return;
        }

        if (map.HasMapFlag(MapFlags.IS_BASE_MAP))
        {
            _baseMapInstancesByMapId.Remove(map.MapId);
        }

        if (_mapConfigByMapId.TryGetValue(map.MapId, out ServerMapDto mapConfiguration))
        {
            foreach (MapFlags flag in mapConfiguration.Flags)
            {
                if (_mapInstancesByFlags.TryGetValue(flag, out ThreadSafeList<IMapInstance> list) && list != null)
                {
                    list.Remove(map);
                }
            }
        }

        map.Destroy();
    }
}