#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Modules.Gambling.Common;
using EllieBot.Modules.Gambling.Common.Waifu;

namespace EllieBot.Modules.Gambling.Services;

public class WaifuService : IEService, IReadyExecutor
{
    private readonly DbService _db;
    private readonly ICurrencyService _cs;
    private readonly IBotCache _cache;
    private readonly GamblingConfigService _gss;
    private readonly IBotCreds _creds;
    private readonly DiscordSocketClient _client;

    public WaifuService(
        DbService db,
        ICurrencyService cs,
        IBotCache cache,
        GamblingConfigService gss,
        IBotCreds creds,
        DiscordSocketClient client)
    {
        _db = db;
        _cs = cs;
        _cache = cache;
        _gss = gss;
        _creds = creds;
        _client = client;
    }

    public async Task<bool> WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner)
    {
        if (owner.Id == newOwner.Id || waifuId == newOwner.Id)
            return false;

        var settings = _gss.Data;

        await using var uow = _db.GetDbContext();
        var waifu = uow.Set<WaifuInfo>().ByWaifuUserId(waifuId);
        var ownerUser = uow.GetOrCreateUser(owner);

        // owner has to be the owner of the waifu
        if (waifu is null || waifu.ClaimerId != ownerUser.Id)
            return false;

        // if waifu likes the person, gotta pay the penalty
        if (waifu.AffinityId == ownerUser.Id)
        {
            if (!await _cs.RemoveAsync(owner.Id, (long)(waifu.Price * 0.6), new("waifu", "affinity-penalty")))
                // unable to pay 60% penalty
                return false;

            waifu.Price = (long)(waifu.Price * 0.7); // half of 60% = 30% price reduction
            if (waifu.Price < settings.Waifu.MinPrice)
                waifu.Price = settings.Waifu.MinPrice;
        }
        else // if not, pay 10% fee
        {
            if (!await _cs.RemoveAsync(owner.Id, waifu.Price / 10, new("waifu", "transfer")))
                return false;

            waifu.Price = (long)(waifu.Price * 0.95); // half of 10% = 5% price reduction
            if (waifu.Price < settings.Waifu.MinPrice)
                waifu.Price = settings.Waifu.MinPrice;
        }

        //new claimerId is the id of the new owner
        var newOwnerUser = uow.GetOrCreateUser(newOwner);
        waifu.ClaimerId = newOwnerUser.Id;

        await uow.SaveChangesAsync();

        return true;
    }

    public long GetResetPrice(IUser user)
    {
        var settings = _gss.Data;
        using var uow = _db.GetDbContext();
        var waifu = uow.Set<WaifuInfo>().ByWaifuUserId(user.Id);

        if (waifu is null)
            return settings.Waifu.MinPrice;

        var divorces = uow.Set<WaifuUpdate>()
                          .Count(x
                              => x.Old != null
                                 && x.Old.UserId == user.Id
                                 && x.UpdateType == WaifuUpdateType.Claimed
                                 && x.New == null);
        var affs = uow.Set<WaifuUpdate>()
                      .AsQueryable()
                      .Where(w => w.User.UserId == user.Id
                                  && w.UpdateType == WaifuUpdateType.AffinityChanged
                                  && w.New != null)
                      .ToList()
                      .GroupBy(x => x.New)
                      .Count();

        return (long)Math.Ceiling(waifu.Price * 1.25f)
               + ((divorces + affs + 2) * settings.Waifu.Multipliers.WaifuReset);
    }

    public async Task<bool> TryReset(IUser user)
    {
        await using var uow = _db.GetDbContext();
        var price = GetResetPrice(user);
        if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset")))
            return false;

        var affs = uow.Set<WaifuUpdate>()
                      .AsQueryable()
                      .Where(w => w.User.UserId == user.Id
                                  && w.UpdateType == WaifuUpdateType.AffinityChanged
                                  && w.New != null);

        var divorces = uow.Set<WaifuUpdate>()
                          .AsQueryable()
                          .Where(x => x.Old != null
                                      && x.Old.UserId == user.Id
                                      && x.UpdateType == WaifuUpdateType.Claimed
                                      && x.New == null);

        //reset changes of heart to 0
        uow.Set<WaifuUpdate>().RemoveRange(affs);
        //reset divorces to 0
        uow.Set<WaifuUpdate>().RemoveRange(divorces);
        var waifu = uow.Set<WaifuInfo>().ByWaifuUserId(user.Id);
        //reset price, remove items
        //remove owner, remove affinity
        waifu.Price = 50;
        waifu.Items.Clear();
        waifu.ClaimerId = null;
        waifu.AffinityId = null;

        //wives stay though

        await uow.SaveChangesAsync();

        return true;
    }

    public async Task<(WaifuInfo, bool, WaifuClaimResult)> ClaimWaifuAsync(IUser user, IUser target, long amount)
    {
        var settings = _gss.Data;
        WaifuClaimResult result;
        WaifuInfo w;
        bool isAffinity;
        await using (var uow = _db.GetDbContext())
        {
            w = uow.Set<WaifuInfo>().ByWaifuUserId(target.Id);
            isAffinity = w?.Affinity?.UserId == user.Id;
            if (w is null)
            {
                var claimer = uow.GetOrCreateUser(user);
                var waifu = uow.GetOrCreateUser(target);
                if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim")))
                    result = WaifuClaimResult.NotEnoughFunds;
                else
                {
                    uow.Set<WaifuInfo>()
                       .Add(w = new()
                       {
                           Waifu = waifu,
                           Claimer = claimer,
                           Affinity = null,
                           Price = amount
                       });
                    uow.Set<WaifuUpdate>()
                       .Add(new()
                       {
                           User = waifu,
                           Old = null,
                           New = claimer,
                           UpdateType = WaifuUpdateType.Claimed
                       });
                    result = WaifuClaimResult.Success;
                }
            }
            else if (isAffinity && amount > w.Price * settings.Waifu.Multipliers.CrushClaim)
            {
                if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim")))
                    result = WaifuClaimResult.NotEnoughFunds;
                else
                {
                    var oldClaimer = w.Claimer;
                    w.Claimer = uow.GetOrCreateUser(user);
                    w.Price = amount + (amount / 4);
                    result = WaifuClaimResult.Success;

                    uow.Set<WaifuUpdate>()
                       .Add(new()
                       {
                           User = w.Waifu,
                           Old = oldClaimer,
                           New = w.Claimer,
                           UpdateType = WaifuUpdateType.Claimed
                       });
                }
            }
            else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity
            {
                if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim")))
                    result = WaifuClaimResult.NotEnoughFunds;
                else
                {
                    var oldClaimer = w.Claimer;
                    w.Claimer = uow.GetOrCreateUser(user);
                    w.Price = amount;
                    result = WaifuClaimResult.Success;

                    uow.Set<WaifuUpdate>()
                       .Add(new()
                       {
                           User = w.Waifu,
                           Old = oldClaimer,
                           New = w.Claimer,
                           UpdateType = WaifuUpdateType.Claimed
                       });
                }
            }
            else
                result = WaifuClaimResult.InsufficientAmount;


            await uow.SaveChangesAsync();
        }

        return (w, isAffinity, result);
    }

    public async Task<(DiscordUser, bool, TimeSpan?)> ChangeAffinityAsync(IUser user, IGuildUser target)
    {
        DiscordUser oldAff = null;
        var success = false;
        TimeSpan? remaining = null;
        await using (var uow = _db.GetDbContext())
        {
            var w = uow.Set<WaifuInfo>().ByWaifuUserId(user.Id);
            var newAff = target is null ? null : uow.GetOrCreateUser(target);
            if (w?.Affinity?.UserId == target?.Id)
            {
                return (null, false, null);
            }

            remaining = await _cache.GetRatelimitAsync(GetAffinityKey(user.Id),
                30.Minutes());

            if (remaining is not null)
            {
            }
            else if (w is null)
            {
                var thisUser = uow.GetOrCreateUser(user);
                uow.Set<WaifuInfo>()
                   .Add(new()
                   {
                       Affinity = newAff,
                       Waifu = thisUser,
                       Price = 1,
                       Claimer = null
                   });
                success = true;

                uow.Set<WaifuUpdate>()
                   .Add(new()
                   {
                       User = thisUser,
                       Old = null,
                       New = newAff,
                       UpdateType = WaifuUpdateType.AffinityChanged
                   });
            }
            else
            {
                if (w.Affinity is not null)
                    oldAff = w.Affinity;
                w.Affinity = newAff;
                success = true;

                uow.Set<WaifuUpdate>()
                   .Add(new()
                   {
                       User = w.Waifu,
                       Old = oldAff,
                       New = newAff,
                       UpdateType = WaifuUpdateType.AffinityChanged
                   });
            }

            await uow.SaveChangesAsync();
        }

        return (oldAff, success, remaining);
    }

    public async Task<IReadOnlyList<WaifuLbResult>> GetTopWaifusAtPage(int page, int perPage = 9)
    {
        await using var uow = _db.GetDbContext();
        return await uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
    }

    public ulong GetWaifuUserId(ulong ownerId, string name)
    {
        using var uow = _db.GetDbContext();
        return uow.Set<WaifuInfo>().GetWaifuUserId(ownerId, name);
    }

    private static TypedKey<long> GetDivorceKey(ulong userId)
        => new($"waifu:divorce_cd:{userId}");

    private static TypedKey<long> GetAffinityKey(ulong userId)
        => new($"waifu:affinity:{userId}");

    public async Task<(WaifuInfo, DivorceResult, long)> DivorceWaifuAsync(IUser user, ulong targetId)
    {
        DivorceResult result;
        long amount = 0;
        WaifuInfo w;
        await using (var uow = _db.GetDbContext())
        {
            w = uow.Set<WaifuInfo>().ByWaifuUserId(targetId);
            if (w?.Claimer is null || w.Claimer.UserId != user.Id)
            {
                result = DivorceResult.NotYourWife;
            }
            else
            {

                amount = w.Price / 2;

                if (w.Affinity?.UserId == user.Id)
                {
                    await _cs.AddAsync(w.Waifu.UserId, amount, new("waifu", "compensation"));
                    w.Price = (long)Math.Floor(w.Price * _gss.Data.Waifu.Multipliers.DivorceNewValue);
                    result = DivorceResult.SucessWithPenalty;
                }
                else
                {
                    await _cs.AddAsync(user.Id, amount, new("waifu", "refund"));

                    result = DivorceResult.Success;
                }

                var oldClaimer = w.Claimer;
                w.Claimer = null;

                uow.Set<WaifuUpdate>()
                   .Add(new()
                   {
                       User = w.Waifu,
                       Old = oldClaimer,
                       New = null,
                       UpdateType = WaifuUpdateType.Claimed
                   });
            }

            await uow.SaveChangesAsync();
        }

        return (w, result, amount);
    }

    public async Task<bool> GiftWaifuAsync(
        IUser from,
        IUser giftedWaifu,
        WaifuItemModel itemObj,
        int count)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(count, 1, nameof(count));

        if (!await _cs.RemoveAsync(from, itemObj.Price * count, new("waifu", "item")))
            return false;

        var totalValue = itemObj.Price * count;

        await using var uow = _db.GetDbContext();
        var w = uow.Set<WaifuInfo>()
                   .ByWaifuUserId(giftedWaifu.Id,
                       set => set
                              .Include(x => x.Items)
                              .Include(x => x.Claimer));
        if (w is null)
        {
            uow.Set<WaifuInfo>()
               .Add(w = new()
               {
                   Affinity = null,
                   Claimer = null,
                   Price = 1,
                   Waifu = uow.GetOrCreateUser(giftedWaifu)
               });
        }

        if (!itemObj.Negative)
        {
            w.Items.AddRange(Enumerable.Range(0, count)
                                       .Select((_) => new WaifuItem()
                                       {
                                           Name = itemObj.Name.ToLowerInvariant(),
                                           ItemEmoji = itemObj.ItemEmoji
                                       }));

            if (w.Claimer?.UserId == from.Id)
                w.Price += (long)(totalValue * _gss.Data.Waifu.Multipliers.GiftEffect);
            else
                w.Price += totalValue / 2;
        }
        else
        {
            w.Price -= (long)(totalValue * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
            if (w.Price < 1)
                w.Price = 1;
        }

        await uow.SaveChangesAsync();

        return true;
    }

    public async Task<WaifuInfoStats> GetFullWaifuInfoAsync(ulong targetId)
    {
        await using var uow = _db.GetDbContext();
        var wi = await uow.GetWaifuInfoAsync(targetId);
        if (wi is null)
        {
            wi = new()
            {
                AffinityCount = 0,
                AffinityName = null,
                ClaimCount = 0,
                ClaimerName = null,
                DivorceCount = 0,
                FullName = null,
                Price = 1
            };
        }

        return wi;
    }

    public string GetClaimTitle(int count)
    {
        ClaimTitle title;
        if (count == 0)
            title = ClaimTitle.Lonely;
        else if (count == 1)
            title = ClaimTitle.Devoted;
        else if (count < 3)
            title = ClaimTitle.Rookie;
        else if (count < 6)
            title = ClaimTitle.Schemer;
        else if (count < 10)
            title = ClaimTitle.Dilettante;
        else if (count < 17)
            title = ClaimTitle.Intermediate;
        else if (count < 25)
            title = ClaimTitle.Seducer;
        else if (count < 35)
            title = ClaimTitle.Expert;
        else if (count < 50)
            title = ClaimTitle.Veteran;
        else if (count < 75)
            title = ClaimTitle.Incubis;
        else if (count < 100)
            title = ClaimTitle.Harem_King;
        else
            title = ClaimTitle.Harem_God;

        return title.ToString().Replace('_', ' ');
    }

    public string GetAffinityTitle(int count)
    {
        AffinityTitle title;
        if (count < 1)
            title = AffinityTitle.Pure;
        else if (count < 2)
            title = AffinityTitle.Faithful;
        else if (count < 4)
            title = AffinityTitle.Playful;
        else if (count < 8)
            title = AffinityTitle.Cheater;
        else if (count < 11)
            title = AffinityTitle.Tainted;
        else if (count < 15)
            title = AffinityTitle.Corrupted;
        else if (count < 20)
            title = AffinityTitle.Lewd;
        else if (count < 25)
            title = AffinityTitle.Sloot;
        else if (count < 35)
            title = AffinityTitle.Depraved;
        else
            title = AffinityTitle.Harlot;

        return title.ToString().Replace('_', ' ');
    }

    public IReadOnlyList<WaifuItemModel> GetWaifuItems()
    {
        var conf = _gss.Data;
        return conf.Waifu.Items.Select(x
                       => new WaifuItemModel(x.ItemEmoji,
                           (long)(x.Price * conf.Waifu.Multipliers.AllGiftPrices),
                           x.Name,
                           x.Negative))
                   .ToList();
    }

    private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay";

    public async Task OnReadyAsync()
    {
        // only decay waifu values from shard 0
        if (_client.ShardId != 0)
            return;

        while (true)
        {
            try
            {
                var decay = _gss.Data.Waifu.Decay;

                var unclaimedMulti = 1 - (decay.UnclaimedDecayPercent / 100f);
                var claimedMulti = 1 - (decay.ClaimedDecayPercent / 100f);

                var minPrice = decay.MinPrice;
                var decayInterval = decay.HourInterval;

                if (decayInterval <= 0)
                    continue;

                if ((unclaimedMulti < 0 || unclaimedMulti > 1) && (claimedMulti < 0 || claimedMulti > 1))
                    continue;

                var now = DateTime.UtcNow;
                var nowB = now.ToBinary();

                var result = await _cache.GetAsync(_waifuDecayKey);

                if (result.TryGetValue(out var val))
                {
                    var lastDecay = DateTime.FromBinary(val);
                    var toWait = decayInterval.Hours() - (DateTime.UtcNow - lastDecay);

                    if (toWait > 0.Hours())
                        continue;
                }

                await _cache.AddAsync(_waifuDecayKey, nowB);

                if (unclaimedMulti is > 0 and <= 1)
                {
                    await using var uow = _db.GetDbContext();

                    await uow.GetTable<WaifuInfo>()
                             .Where(x => x.Price > minPrice && x.ClaimerId == null)
                             .UpdateAsync(old => new()
                             {
                                 Price = (long)(old.Price * unclaimedMulti)
                             });
                }

                if (claimedMulti is > 0 and <= 1)
                {
                    await using var uow = _db.GetDbContext();
                    await uow.GetTable<WaifuInfo>()
                             .Where(x => x.Price > minPrice && x.ClaimerId != null)
                             .UpdateAsync(old => new()
                             {
                                 Price = (long)(old.Price * claimedMulti)
                             });
                }
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Unexpected error occured in waifu decay loop: {ErrorMessage}", ex.Message);
            }
            finally
            {
                await Task.Delay(1.Hours());
            }
        }
    }

    public async Task<IReadOnlyCollection<string>> GetClaimNames(int waifuId)
    {
        await using var ctx = _db.GetDbContext();
        return await ctx.GetTable<DiscordUser>()
                        .Where(x => ctx.GetTable<WaifuInfo>()
                                       .Where(wi => wi.ClaimerId == waifuId)
                                       .Select(wi => wi.WaifuId)
                                       .Contains(x.Id))
                        .Select(x => x.Username)
                        .ToListAsyncEF();
    }

    public async Task<IReadOnlyCollection<string>> GetFansNames(int waifuId)
    {
        await using var ctx = _db.GetDbContext();
        return await ctx.GetTable<DiscordUser>()
                        .Where(x => ctx.GetTable<WaifuInfo>()
                                       .Where(wi => wi.AffinityId == waifuId)
                                       .Select(wi => wi.WaifuId)
                                       .Contains(x.Id))
                        .Select(x => x.Username)
                        .ToListAsyncEF();
    }

    public async Task<IReadOnlyCollection<WaifuItem>> GetItems(int waifuId)
    {
        await using var ctx = _db.GetDbContext();
        return await ctx.GetTable<WaifuItem>()
                        .Where(x => x.WaifuInfoId
                                    == ctx.GetTable<WaifuInfo>()
                                          .Where(x => x.WaifuId == waifuId)
                                          .Select(x => x.Id)
                                          .FirstOrDefault())
                        .ToListAsyncEF();
    }

    public async Task<IReadOnlyCollection<WaifuClaimsResult>> GetClaimsAsync(ulong userId, int page)
    {
        await using var ctx = _db.GetDbContext();

        var wid = ctx.GetTable<DiscordUser>()
                     .Where(x => x.UserId == userId)
                     .Select(x => x.Id)
                     .FirstOrDefault();

        if (wid == 0)
            return [];

        return await ctx.GetTable<WaifuInfo>()
                        .Where(x => x.ClaimerId == wid)
                        .LeftJoin(ctx.GetTable<DiscordUser>(),
                            (wi, du) => wi.WaifuId == du.Id,
                            (wi, du) => new WaifuClaimsResult(
                                du.Username,
                                du.UserId,
                                wi.Price
                            ))
                        .OrderByDescending(x => x.Price)
                        .Skip(page * 9)
                        .Take(9)
                        .ToListAsyncLinqToDB();
    }
}

public sealed class WaifuClaimsResult(string username, ulong userId, long price)
{
    public string Username { get; } = username;
    public ulong UserId { get; } = userId;
    public long Price { get; } = price;
}