// 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 System.Collections.Concurrent;
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;

namespace Catalogger.Backend.Cache.InMemoryCache;

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

    private readonly ConcurrentDictionary<
        (Snowflake GuildId, Snowflake UserId),
        IGuildMember
    > _members = new();

    private readonly ConcurrentDictionary<Snowflake, byte> _guilds = new();

#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type.
    public Task<IGuildMember?> TryGetAsync(Snowflake guildId, Snowflake userId) =>
        _members.TryGetValue((guildId, userId), out var member)
            ? Task.FromResult(member)
            : Task.FromResult<IGuildMember?>(null);
#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type.

    public Task SetAsync(Snowflake guildId, IGuildMember member)
    {
        if (!member.User.IsDefined())
            throw new CataloggerError(
                "Member with undefined User passed to InMemoryMemberCache.SetAsync"
            );
        _members[(guildId, member.User.Value.ID)] = member;
        return Task.CompletedTask;
    }

    public async Task SetManyAsync(Snowflake guildId, IReadOnlyList<IGuildMember> members)
    {
        foreach (var member in members)
            await SetAsync(guildId, member);
    }

    public Task RemoveAsync(Snowflake guildId, Snowflake userId)
    {
        _members.Remove((guildId, userId), out _);
        return Task.CompletedTask;
    }

    public Task<bool> IsGuildCachedAsync(Snowflake guildId) =>
        Task.FromResult(_guilds.ContainsKey(guildId));

    public Task MarkAsCachedAsync(Snowflake guildId)
    {
        _guilds[guildId] = 1;
        return Task.CompletedTask;
    }

    public Task MarkAsUncachedAsync(Snowflake guildId)
    {
        _guilds.Remove(guildId, out _);
        return Task.CompletedTask;
    }

    public async Task UpdateAsync(IGuildMemberUpdate newMember)
    {
        if (!_members.TryGetValue((newMember.GuildID, newMember.User.ID), out var oldMember))
        {
            _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;
            }

            _members[(newMember.GuildID, newMember.User.ID)] = memberResult.Entity;
            return;
        }

        // Unwrapping the interface because fuck it, we ball
        var member = (GuildMember)oldMember with
        {
            User = new Optional<IUser>(newMember.User),
            Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname,
            Avatar = newMember.Avatar.HasValue ? newMember.Avatar : oldMember.Avatar,
            Roles = newMember.Roles.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,
        };

        _members[(newMember.GuildID, newMember.User.ID)] = member;
    }

    public Task SetMemberNamesAsync(Snowflake guildId, IEnumerable<IGuildMember> members) =>
        Task.CompletedTask;

    public Task UpdateMemberNameAsync(
        Snowflake guildId,
        Snowflake userId,
        string prevName,
        string newName
    ) => Task.CompletedTask;

    public Task<IEnumerable<(string Name, string Id)>> GetMemberNamesAsync(
        Snowflake guildId,
        string prefix
    ) => Task.FromResult<IEnumerable<(string, string)>>([]);

    public Task TryRemoveMemberNameAsync(Snowflake guildId, string username) => Task.CompletedTask;

    public Task RemoveAllMembersAsync(Snowflake guildId)
    {
        foreach (var kv in _members.Where(kv => kv.Key.GuildId == guildId))
            _members.Remove(kv.Key, out _);

        _guilds.Remove(guildId, out _);
        return Task.CompletedTask;
    }
}
