// Copyright (c) 2023 Timothy Schenk. Subject to the GNU AGPL Version 3 License.

using Continuity.AuthServer.DB;
using Continuity.AuthServer.DB.Documents;
using DotNext.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using NetCoreServer;
using Wonderking.Packets.Incoming;
using Wonderking.Packets.Outgoing;
using Wonderking.Packets.Outgoing.Data;

namespace Continuity.AuthServer.PacketHandlers;

public partial class ChannelSelectionHandler : IPacketHandler<ChannelSelectionPacket>
{
    private readonly WonderkingContext _wonderkingContext;

    public ChannelSelectionHandler(WonderkingContext wonderkingContext)
    {
        _wonderkingContext = wonderkingContext;
    }

    public async Task HandleAsync(ChannelSelectionPacket packet, TcpSession session)
    {
        if (session is not AuthSession authSession)
        {
            return;
        }

        ChannelSelectionResponsePacket responsePacket;
        var guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket { GuildNames = Array.Empty<string>() };

        var accountExists =
            await _wonderkingContext.Accounts.AsNoTracking().AnyAsync(a => a.Id == authSession.AccountId);

        var amountOfCharacter = await _wonderkingContext.Characters.AsNoTracking().Include(c => c.Account)
            .Where(c => c.Account.Id == authSession.AccountId).Take(3)
            .CountAsync();

        if (!accountExists)
        {
            return;
        }

        if (amountOfCharacter > 0)
        {
            responsePacket = new ChannelSelectionResponsePacket
            {
                ChannelIsFullFlag = 0,
                Endpoint = "127.0.0.1",
                Port = 2000,
                Characters = await GetCharacterDataAsync(authSession.AccountId).ToArrayAsync()
            };

            guildNameResponsePacket.GuildNames =
                await _wonderkingContext.Characters.AsNoTracking().Include(c => c.Account).Include(c => c.GuildMember)
                    .ThenInclude(gm => gm.Guild)
                    .Where(c => c.Account.Id == authSession.AccountId && c.GuildMember.Guild != null)
                    .Select(c => c.GuildMember.Guild.Name).Take(3).ToArrayAsync();
        }
        else
        {
            responsePacket = new ChannelSelectionResponsePacket
            {
                ChannelIsFullFlag = 0,
                Endpoint = "127.0.0.1",
                Port = 2000,
                Characters = Array.Empty<CharacterData>()
            };
        }

        await authSession.SendAsync(responsePacket);
        if (guildNameResponsePacket.GuildNames.Length > 0 &&
            guildNameResponsePacket.GuildNames.Select(n => n != string.Empty).Any())
        {
            await authSession.SendAsync(guildNameResponsePacket);
        }
    }

    private static ushort[] GetItemIDsByInventoryTab(IEnumerable<InventoryItemProjection> items)
    {
        var ids = new ushort[20];
        ids.AsSpan().Clear();

        foreach (var item in items)
        {
            if (item.Slot > 20)
            {
                continue;
            }

            ids[item.Slot] = item.ItemId;
        }

        return ids;
    }

    private async IAsyncEnumerable<CharacterData> GetCharacterDataAsync(Guid accountId)
    {
        var characterDataProjections = _wonderkingContext.Characters.AsNoTracking().AsSplitQuery()
            .Include(c => c.InventoryItems).Include(c => c.Account)
            .Where(c => c.Account.Id == accountId)
            .Select(c => new CharacterDataProjection(
                c.Name,
                c.JobData,
                c.Gender,
                c.Level,
                c.Experience,
                c.BaseStats,
                c.Health,
                c.Mana,
                c.InventoryItems.Select(i =>
                    new InventoryItemProjection(i.ItemId, i.Slot, i.InventoryTab))
            )).Take(3);

        await foreach (var c in characterDataProjections)
        {
            yield return new CharacterData
            {
                Name = c.Name,
                Job = c.JobData,
                Gender = c.Gender,
                Level = c.Level,
                // TODO: Calculate instead of clamping based on max experience for level
                Experience = Math.Clamp(c.Experience, 0, 100),
                Stats = c.BaseStats,
                Health = c.Health,
                Mana = c.Mana,
                EquippedItems =
                    GetItemIDsByInventoryTab(c.InventoryItems.Where(i =>
                        i.InventoryTab == InventoryTab.WornEquipment)),
                EquippedCashItems = GetItemIDsByInventoryTab(c.InventoryItems.Where(i =>
                    i.InventoryTab == InventoryTab.WornCashEquipment))
            };
        }
    }
}