using System.Data;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Utils;
using Dapper;
using Microsoft.Extensions.Logging;
using MySqlConnector;
using Serilog;

namespace Skinchanger;

public class SkinChanger : BasePlugin, IPluginConfig<Config>
{
    public override string ModuleAuthor => "Muinez";
    public override string ModuleName => "SkinChanger";
    public override string ModuleVersion => "v1.0.0";
    public Config Config { get; set; }

    private Dictionary<int, string> _knifes = new()
    {
        { 80, "weapon_knife_ghost" },
        { 500, "weapon_bayonet" },
        { 503, "weapon_knife_css" },
        { 505, "weapon_knife_flip" },
        { 506, "weapon_knife_gut" },
        { 507, "weapon_knife_karambit" },
        { 508, "weapon_knife_m9_bayonet" },
        { 509, "weapon_knife_tactical" },
        { 512, "weapon_knife_falchion" },
        { 514, "weapon_knife_survival_bowie" },
        { 515, "weapon_knife_butterfly" },
        { 516, "weapon_knife_push" },
        { 517, "weapon_knife_cord" },
        { 518, "weapon_knife_canis" },
        { 519, "weapon_knife_ursus" },
        { 520, "weapon_knife_gypsy_jackknife" },
        { 521, "weapon_knife_outdoor" },
        { 522, "weapon_knife_stiletto" },
        { 523, "weapon_knife_widowmaker" },
        { 525, "weapon_knife_skeleton" },
        { 526, "weapon_knife_kukri" },
    };

    private string _connectionString = null!;
    private readonly SkinsPlayer?[] _players = new SkinsPlayer?[70];

    private static readonly MemoryFunctionVoid<nint, string, float> CAttributeListSetOrAddAttributeValueByNameFunc =
        new(GameData.GetSignature("CAttributeList_SetOrAddAttributeValueByName"));

    private static readonly Action<nint, string, float> SetAttribute =
        CAttributeListSetOrAddAttributeValueByNameFunc.Invoke;

    private static readonly MemoryFunctionWithReturn<nint, string, int, int> SetBodygroupFunc = new(
        GameData.GetSignature("CBaseModelEntity_SetBodygroup"));

    private static readonly Func<nint, string, int, int> SetBodygroup = SetBodygroupFunc.Invoke;

    public override void Load(bool hotReload)
    {
        RegisterListener<Listeners.OnClientAuthorized>(OnClientAuthorized);
        RegisterListener<Listeners.OnClientDisconnect>(slot => _players[slot] = null);

        VirtualFunctions.GiveNamedItemFunc.Hook(OnGiveNamedItem, HookMode.Pre);
        VirtualFunctions.GiveNamedItemFunc.Hook(OnGiveNamedItemPost, HookMode.Post);

        AddCommand("css_ws", "", OnWsCommand);
        AddCommand("css_wp", "", OnWpCommand);

        RegisterEventHandler<EventPlayerSpawn>(OnPlayerSpawn);
    }

    private void OnWsCommand(CCSPlayerController? player, CommandInfo info)
    {
        info.ReplyToCommand(" \x04🎨 Choose your favorite skins at: \x0C https://cswars.com/skins");
        info.ReplyToCommand(" \x04🔄 Use the \x0B!wp\x04 command to refresh your skins.");
    }

    private void OnWpCommand(CCSPlayerController? player, CommandInfo info)
    {
        if (player == null)
            return;

        var skinsPlayer = _players[player.Slot];
        if (skinsPlayer == null)
            return;

        var timeNow = DateTimeOffset.Now;
        var delay = timeNow - skinsPlayer.LastReloadTime;
        if (delay.TotalSeconds < 30)
        {
            player.PrintToChat($" \x07 Command is on delay, wait \x03{30 - delay.TotalSeconds:0.0}\x07 more seconds.");
            return;
        }

        skinsPlayer.LastReloadTime = timeNow;

        player.PrintToChat(" \x04🔄 Refreshing your skins... Please wait a moment. 🔄");
        Task.Run(async () =>
        {
            await LoadPlayerData(skinsPlayer);

            Server.NextFrame(() =>
            {
                player.PrintToChat(" \x04✅ Your skins have been successfully updated! Enjoy! 🎉");
                UpdateAllWeapons(skinsPlayer.Controller);
                UpdateGloves(skinsPlayer);
            });
        });
    }

    private void UpdateAllWeapons(CCSPlayerController controller)
    {
        var pawn = controller.PlayerPawn.Value;
        if (pawn == null)
            return;


        var services = pawn.WeaponServices!;

        var slot = services.ActiveWeapon.Value?.As<CCSWeaponBase>().VData?.GearSlot;

        var weaponNames = services.MyWeapons.Select(w =>
        {
            var weapon = w.Value;
            if (weapon == null)
                return null;

            return new
            {
                weapon.DesignerName, weapon.Clip1, weapon.Clip2
            };
        }).ToList();

        controller.RemoveWeapons();
        foreach (var weaponData in weaponNames)
        {
            if (weaponData != null)
            {
                //controller.PrintToChat(weaponName.ToString());
                var item = controller.GiveNamedItem<CBasePlayerWeapon>(weaponData.DesignerName);
                if (item != null)
                {
                    item.Clip1 = weaponData.Clip1;
                    item.Clip2 = weaponData.Clip2;
                }
            }
        }

        if (slot != null)
        {
            controller.ExecuteClientCommand($"slot{(int)slot.Value + 1}");
        }

        /*foreach (var weaponServicesMyWeapon in pawn.WeaponServices.MyWeapons)
        {
            Server.PrintToConsole(weaponServicesMyWeapon.Value.DesignerName);
        }*/
    }

    private HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info)
    {
        var player = @event.Userid;
        if (player == null || !player.IsValid || !player.PawnIsAlive)
            return HookResult.Continue;

        var skinsPlayer = _players[player.Slot];
        if (skinsPlayer == null)
            return HookResult.Continue;

        UpdateGloves(skinsPlayer);
        return HookResult.Continue;
    }

    private void UpdateGloves(SkinsPlayer player)
    {
        var controller = player.Controller;

        var team = controller.TeamNum - 2;
        if (team < 0)
            return;

        if (player.TeamSettings == null)
            return;

        var settings = player.TeamSettings[team];
        var gloves = settings.Gloves;
        if (gloves is null or 0)
            return;

        var pawn = controller.PlayerPawn.Value;
        if (pawn == null)
            return;

        var model = pawn.CBodyComponent?.SceneNode?.GetSkeletonInstance().ModelState.ModelName ?? string.Empty;
        if (!string.IsNullOrEmpty(model))
        {
            pawn.SetModel("characters/models/tm_jumpsuit/tm_jumpsuit_varianta.vmdl");
            pawn.SetModel(model);
        }

        settings.WeaponSettings.TryGetValue(gloves.Value, out var weaponData);

        AddTimer(0.08f, () =>
        {
            CEconItemView item = pawn.EconGloves;
            if (!pawn.IsValid)
                return;

            item.ItemDefinitionIndex = (ushort)gloves.Value;
            item.ItemIDLow = 16384 & 0xFFFFFFFF;
            item.ItemIDHigh = 16384;

            if (weaponData != null)
            {
                var attributes = item.NetworkedDynamicAttributes.Handle;
                SetAttribute.Invoke(attributes, "set item texture prefab", weaponData.Paint);
                SetAttribute.Invoke(attributes, "set item texture seed", weaponData.Pattern);
                SetAttribute.Invoke(attributes, "set item texture wear", weaponData.Quality);

                item.Initialized = true;
            }

            SetBodygroup(pawn.Handle, "default_gloves", 1);
        }, TimerFlags.STOP_ON_MAPCHANGE);
    }

    public override void Unload(bool hotReload)
    {
        VirtualFunctions.GiveNamedItemFunc.Unhook(OnGiveNamedItem, HookMode.Pre);
        VirtualFunctions.GiveNamedItemFunc.Unhook(OnGiveNamedItemPost, HookMode.Post);
    }

    private void OnClientAuthorized(int slot, SteamID steamId)
    {
        var player = _players[slot] = new SkinsPlayer()
        {
            Controller = Utilities.GetPlayerFromSlot(slot)!,
            SteamId = steamId,
        };

        Task.Run(() => LoadPlayerData(player));
    }

    private async Task LoadPlayerData(SkinsPlayer player)
    {
        try
        {
            using var conn = await OpenConnection();
            var userId = await conn.QuerySingleOrDefaultAsync<int?>("select id from sc_player where steamid=@steamId",
                new
                {
                    steamId = player.SteamId.SteamId64
                });

            if (userId == null)
                return;
            player.TeamSettings = new TeamSettings[2] { new(), new() };

            var items = await conn.QueryAsync<(int team, int knife, int glove)>(
                $"select team,knife,glove from sc_items where player_id={userId}");
            foreach (var (team, knife, glove) in items)
            {
                player.TeamSettings[team].Knife = knife;
                player.TeamSettings[team].Gloves = glove;
            }

            var paints =
                await conn.QueryAsync<(int team, int weaponIndex, string skinData, string stickers, string name)>(
                    $"select team,weapon_index,skin,stickers,tag from sc_skins where player_id={userId}");
            foreach (var (team, weaponIndex, skinData, stickers, tag) in paints)
            {
                var datas = skinData.Split(";");

                var weaponData = new WeaponData()
                {
                    Name = tag,
                    Paint = int.Parse(datas[0]),
                    Pattern = int.Parse(datas[1]),
                    Quality = float.Parse(datas[2]),
                    Stickers = stickers.Split(";").Select(int.Parse).ToArray()
                };
                player.TeamSettings[team].WeaponSettings[weaponIndex] = weaponData;
            }
        }
        catch (Exception e)
        {
            Logger.LogError("Error while loading player data: {e}", e);
        }
    }

    private IntPtr _weaponNamePtr = 0;

    private HookResult OnGiveNamedItem(DynamicHook arg)
    {
        try
        {
            var weaponName = arg.GetParam<string>(1);
            if (IsKnife(weaponName))
            {
                var services = arg.GetParam<CCSPlayer_WeaponServices>(0);
                var player = FindPlayerByWeaponServices(services);
                if (player?.TeamSettings == null)
                    return HookResult.Continue;

                var team = player.Controller.TeamNum - 2;
                if (team < 0)
                    return HookResult.Continue;

                if (!_knifes.TryGetValue(player.TeamSettings[team].Knife ?? -1, out var knife))
                    return HookResult.Continue;

                var strPtr = Marshal.AllocHGlobal(knife.Length + 1);
                var strPtrCopy = strPtr;
                foreach (var b in Encoding.ASCII.GetBytes(knife))
                {
                    Marshal.WriteByte(strPtrCopy++, b);
                }

                Marshal.WriteByte(strPtrCopy, 0);

                arg.SetParam<IntPtr>(1, strPtr);
                _weaponNamePtr = strPtr;
                return HookResult.Changed;
            }
        }
        catch (Exception e)
        {
            Logger.LogError("Error while handling GiveNamedItemPre: {e}", e);
        }

        return HookResult.Continue;
    }

    private HookResult OnGiveNamedItemPost(DynamicHook arg)
    {
        try
        {
            var weapon = arg.GetReturn<CBasePlayerWeapon>();
            if (weapon.Handle == 0)
                return HookResult.Continue;

            if (!IsKnife(weapon.DesignerName))
                weapon.Entity!.DesignerName = arg.GetParam<string>(1);

            var services = arg.GetParam<CCSPlayer_WeaponServices>(0);
            var player = FindPlayerByWeaponServices(services);
            if (player?.TeamSettings == null)
                return HookResult.Continue;

            var team = player.Controller.TeamNum - 2;
            if (team < 0)
                return HookResult.Continue;

            var manager = weapon.AttributeManager;
            var itemDefId = manager.Item.ItemDefinitionIndex;

            if (!player.TeamSettings[team].WeaponSettings.TryGetValue(itemDefId, out var weaponSettings))
                return HookResult.Continue;

            manager.Item.ItemID = 16384;
            manager.Item.ItemIDLow = 16384 & 0xFFFFFFFF;
            manager.Item.ItemIDHigh = manager.Item.ItemIDLow >> 32;

            if (!string.IsNullOrWhiteSpace(weaponSettings.Name))
                manager.Item.CustomName = weaponSettings.Name;

            weapon.FallbackPaintKit = weaponSettings.Paint;
            weaponSettings.Quality += 0.001f;
            weapon.FallbackWear = weaponSettings.Quality;
            weapon.FallbackSeed = weaponSettings.Pattern;

            if (weapon.CBodyComponent is { SceneNode: not null } cBodyComponent)
            {
                var skeleton = cBodyComponent.SceneNode.GetSkeletonInstance();
                skeleton.ModelState.MeshGroupMask = 2;
            }

            var attributesHandle = manager.Item.NetworkedDynamicAttributes.Handle;
            SetAttribute(attributesHandle, "set item texture prefab", weaponSettings.Paint);
            SetAttribute(attributesHandle, "set item texture wear", weaponSettings.Quality);
            SetAttribute(attributesHandle, "set item texture seed", weaponSettings.Quality);

            for (var i = 0; i < weaponSettings.Stickers.Length; i++)
            {
                unsafe
                {
                    var stickerId = weaponSettings.Stickers[i];
                    var stickerIdFloat = *(float*)&stickerId;
                    SetAttribute(attributesHandle, $"sticker slot {i} id", stickerIdFloat);
                    SetAttribute(manager.Item.AttributeList.Handle, $"sticker slot {i} id", stickerIdFloat);
                }
            }
        }
        catch (Exception e)
        {
            Logger.LogError("Error while handling GiveNamedItemPost: {e}", e);
        }
        finally
        {
            if (_weaponNamePtr != 0)
            {
                Marshal.FreeHGlobal(_weaponNamePtr);
                _weaponNamePtr = 0;
            }
        }


        return HookResult.Continue;
    }

    private SkinsPlayer? FindPlayerByWeaponServices(CCSPlayer_WeaponServices? services)
    {
        var pawn = services?.Pawn.Value;
        if (pawn == null)
            return null;

        return _players[pawn.As<CCSPlayerPawn>().OriginalController.Value!.Slot];
    }

    private bool IsKnife(string weapon)
    {
        return weapon.Contains("knife") || weapon.Contains("bayonet");
    }

    public void OnConfigParsed(Config config)
    {
        Config = config;
        var builder = new MySqlConnectionStringBuilder();
        builder.Server = config.DbHost;
        builder.Database = config.DbName;
        builder.UserID = config.DbUser;
        builder.Password = config.DbPassword;
        _connectionString = builder.ConnectionString;
    }

    private async Task<IDbConnection> OpenConnection()
    {
        var conn = new MySqlConnection(_connectionString);
        await conn.OpenAsync();
        return conn;
    }
}


find place that crashing the server, problem not with exceptions bcs there are no error logs there is most likely related to memory or smth