// Copyright (C) 2021-present sam (starshines.gay)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

using Catalogger.Backend.Database.Redis;
using Catalogger.Backend.Extensions;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Rest.Core;
using StackExchange.Redis;

namespace Catalogger.Backend.Cache.RedisCache;

public class RedisMemberCache(
    RedisService redisService,
    IDiscordRestGuildAPI guildApi,
    ILogger logger
) : IMemberCache
{
    private readonly ILogger _logger = logger.ForContext<RedisMemberCache>();

    public async Task<IGuildMember?> TryGetAsync(Snowflake guildId, Snowflake userId)
    {
        var redisMember = await TryGetInnerAsync(guildId, userId);
        return redisMember?.ToRemoraMember();
    }

    private async Task<RedisMember?> TryGetInnerAsync(Snowflake guildId, Snowflake userId) =>
        await redisService.GetHashAsync<RedisMember>(GuildMembersKey(guildId), userId.ToString());

    public async Task SetAsync(Snowflake guildId, IGuildMember member)
    {
        if (!member.User.IsDefined())
            throw new CataloggerError(
                "Member with undefined User passed to RedisMemberCache.SetAsync"
            );

        await SetInnerAsync(guildId, RedisMember.FromIGuildMember(member));
    }

    private async Task SetInnerAsync(Snowflake guildId, RedisMember member)
    {
        await redisService.SetHashAsync(
            GuildMembersKey(guildId),
            member.User.Id.ToString(),
            member
        );
    }

    public async Task SetManyAsync(Snowflake guildId, IReadOnlyList<IGuildMember> members)
    {
        if (members.Any(m => !m.User.IsDefined()))
            throw new CataloggerError(
                "Member with undefined User passed to RedisMemberCache.SetAsync"
            );
        var redisMembers = members.Select(RedisMember.FromIGuildMember).ToList();

        await redisService.SetHashAsync(
            GuildMembersKey(guildId),
            redisMembers,
            m => m.User.Id.ToString()
        );
    }

    public async Task RemoveAsync(Snowflake guildId, Snowflake userId) =>
        await redisService
            .GetDatabase()
            .HashDeleteAsync(GuildMembersKey(guildId), userId.ToString());

    public async Task<bool> IsGuildCachedAsync(Snowflake guildId) =>
        await redisService.GetDatabase().SetContainsAsync(GuildCacheKey, guildId.ToString());

    public async Task MarkAsCachedAsync(Snowflake guildId) =>
        await redisService.GetDatabase().SetAddAsync(GuildCacheKey, guildId.ToString());

    public async Task MarkAsUncachedAsync(Snowflake guildId) =>
        await redisService.GetDatabase().SetRemoveAsync(GuildCacheKey, guildId.ToString());

    public async Task UpdateAsync(IGuildMemberUpdate newMember)
    {
        _logger.Debug(
            "Updating member {MemberId} in {GuildId}",
            newMember.User.ID,
            newMember.GuildID
        );

        var oldMember = await TryGetInnerAsync(newMember.GuildID, newMember.User.ID);
        if (oldMember == null)
        {
            _logger.Warning(
                "Received member update event for {MemberId} in {GuildId}, but member wasn't found in cache. Fetching them and storing.",
                newMember.User.ID,
                newMember.GuildID
            );

            var memberResult = await guildApi.GetGuildMemberAsync(
                newMember.GuildID,
                newMember.User.ID
            );
            if (!memberResult.IsSuccess)
            {
                _logger.Error(
                    "Could not get uncached member {MemberId} in {GuildId} via REST: {Error}",
                    newMember.User.ID,
                    newMember.GuildID,
                    memberResult.Error
                );

                return;
            }

            await SetAsync(newMember.GuildID, memberResult.Entity);
            return;
        }

        var member = oldMember with
        {
            User = RedisUser.FromIUser(newMember.User),
            Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname,
            Avatar = newMember.Avatar.HasValue ? newMember.Avatar.Value?.Value : oldMember.Avatar,
            Roles = newMember.Roles.Select(id => id.Value).ToArray(),
            JoinedAt = newMember.JoinedAt ?? oldMember.JoinedAt,
            PremiumSince = newMember.PremiumSince.HasValue
                ? newMember.PremiumSince.Value
                : oldMember.PremiumSince,
            IsPending = newMember.IsPending.HasValue
                ? newMember.IsPending.Value
                : oldMember.IsPending,
            CommunicationDisabledUntil = newMember.CommunicationDisabledUntil.HasValue
                ? newMember.CommunicationDisabledUntil.Value
                : oldMember.CommunicationDisabledUntil,
        };

        await SetInnerAsync(newMember.GuildID, member);
    }

    public async Task SetMemberNamesAsync(Snowflake guildId, IEnumerable<IGuildMember> members)
    {
        await redisService
            .GetDatabase()
            .HashSetAsync(
                MemberNamesKey(guildId),
                members
                    .Select(m => new HashEntry(m.User.Value.Tag(), m.User.Value.ID.ToString()))
                    .ToArray()
            );
    }

    public async Task UpdateMemberNameAsync(
        Snowflake guildId,
        Snowflake userId,
        string prevName,
        string newName
    ) =>
        await Task.WhenAll(
            redisService.GetDatabase().HashDeleteAsync(MemberNamesKey(guildId), prevName),
            redisService
                .GetDatabase()
                .HashSetAsync(MemberNamesKey(guildId), newName, userId.ToString())
        );

    public async Task<IEnumerable<(string Name, string Id)>> GetMemberNamesAsync(
        Snowflake guildId,
        string prefix
    )
    {
        var entries = redisService
            .GetDatabase()
            .HashScanAsync(MemberNamesKey(guildId), $"{prefix}*", 50);

        var names = new List<(string Name, string Id)>();
        await foreach (var entry in entries)
            names.Add((entry.Name, entry.Value)!);

        return names;
    }

    public async Task TryRemoveMemberNameAsync(Snowflake guildId, string username) =>
        await redisService.GetDatabase().HashDeleteAsync(MemberNamesKey(guildId), username);

    public async Task RemoveAllMembersAsync(Snowflake guildId) =>
        await redisService
            .GetDatabase()
            .KeyDeleteAsync([GuildMembersKey(guildId), MemberNamesKey(guildId)]);

    private const string GuildCacheKey = "cached-guilds";

    private static string GuildMembersKey(Snowflake guildId) => $"guild-members:{guildId}";

    private static string MemberNamesKey(Snowflake guildId) => $"guild-member-names:{guildId}";
}

internal record RedisMember(
    RedisUser User,
    string? Nickname,
    string? Avatar,
    ulong[] Roles,
    DateTimeOffset JoinedAt,
    DateTimeOffset? PremiumSince,
    GuildMemberFlags Flags,
    bool? IsPending,
    DateTimeOffset? CommunicationDisabledUntil
)
{
    public static RedisMember FromIGuildMember(IGuildMember member) =>
        new(
            RedisUser.FromIUser(member.User.Value),
            member.Nickname.OrDefault(null),
            member.Avatar.OrDefault(null)?.Value,
            member.Roles.Select(id => id.Value).ToArray(),
            member.JoinedAt,
            member.PremiumSince.OrDefault(null),
            member.Flags,
            member.IsPending.OrDefault(null),
            member.CommunicationDisabledUntil.OrDefault(null)
        );

    public GuildMember ToRemoraMember() =>
        new(
            User.ToRemoraUser(),
            Nickname,
            Avatar != null ? new ImageHash(Avatar) : null,
            Roles.Select(DiscordSnowflake.New).ToList(),
            JoinedAt,
            PremiumSince,
            false,
            false,
            Flags,
            IsPending,
            default,
            CommunicationDisabledUntil
        );
}

internal record RedisUser(
    ulong Id,
    string Username,
    ushort Discriminator,
    string? GlobalName,
    string? Avatar,
    bool IsBot,
    bool IsSystem,
    string? Banner
)
{
    public static RedisUser FromIUser(IUser user) =>
        new(
            user.ID.Value,
            user.Username,
            user.Discriminator,
            user.GlobalName.OrDefault(null),
            user.Avatar?.Value,
            user.IsBot.OrDefault(false),
            user.IsSystem.OrDefault(false),
            user.Banner.OrDefault(null)?.Value
        );

    public User ToRemoraUser() =>
        new(
            DiscordSnowflake.New(Id),
            Username,
            Discriminator,
            GlobalName,
            Avatar != null ? new ImageHash(Avatar) : null,
            IsBot,
            IsSystem,
            Banner: Banner != null ? new ImageHash(Banner) : null
        );
}
