// 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.Data.Common;
using System.Diagnostics;
using System.Text.Json;
using Catalogger.Backend.Database;
using Catalogger.Backend.Database.Models;
using Dapper;
using NodaTime.Extensions;
using Serilog;

namespace Catalogger.GoImporter;

public static class GuildImport
{
    public static async Task DoImportAsync(DatabaseConnection conn, string filename)
    {
        var rawData = await File.OpenText(filename).ReadToEndAsync();
        var data = JsonSerializer.Deserialize<GuildsExport>(rawData, Program.JsonOptions);
        if (data == null)
        {
            Log.Error("Guilds JSON dump was deserialized as null, aborting");
            return;
        }

        var watch = new Stopwatch();
        watch.Start();

        await using var tx = await conn.BeginTransactionAsync();

        foreach (var g in data.Guilds)
            await ImportGuildAsync(conn, tx, g);
        foreach (var i in data.Invites)
            await ImportInviteAsync(conn, tx, i);
        foreach (var w in data.Watchlist)
            await ImportWatchlistEntryAsync(conn, tx, w);

        await tx.CommitAsync();

        Log.Information(
            "Imported guilds, invites, and watchlist entries in {Duration}",
            watch.Elapsed.ToDuration()
        );
    }

    private static async Task ImportGuildAsync(
        DatabaseConnection conn,
        DbTransaction tx,
        GoGuild guild
    )
    {
        var messages = new Guild.MessageConfig
        {
            IgnoredChannels = guild.IgnoredChannels.ToList(),
            IgnoredUsers = guild.IgnoredUsers.ToList(),
        };

        var channels = new Guild.ChannelConfig
        {
            GuildUpdate = guild.Channels.TryParse("GUILD_UPDATE"),
            GuildEmojisUpdate = guild.Channels.TryParse("GUILD_EMOJIS_UPDATE"),
            GuildRoleCreate = guild.Channels.TryParse("GUILD_ROLE_CREATE"),
            GuildRoleUpdate = guild.Channels.TryParse("GUILD_ROLE_UPDATE"),
            GuildRoleDelete = guild.Channels.TryParse("GUILD_ROLE_DELETE"),
            ChannelCreate = guild.Channels.TryParse("CHANNEL_CREATE"),
            ChannelUpdate = guild.Channels.TryParse("CHANNEL_UPDATE"),
            ChannelDelete = guild.Channels.TryParse("CHANNEL_DELETE"),
            GuildMemberAdd = guild.Channels.TryParse("GUILD_MEMBER_ADD"),
            GuildMemberUpdate = guild.Channels.TryParse("GUILD_MEMBER_UPDATE"),
            GuildKeyRoleUpdate = guild.Channels.TryParse("GUILD_KEY_ROLE_UPDATE"),
            GuildMemberNickUpdate = guild.Channels.TryParse("GUILD_MEMBER_NICK_UPDATE"),
            GuildMemberAvatarUpdate = guild.Channels.TryParse("GUILD_MEMBER_NICK_UPDATE"),
            GuildMemberTimeout = guild.Channels.TryParse("GUILD_MEMBER_KICK"),
            GuildMemberRemove = guild.Channels.TryParse("GUILD_MEMBER_REMOVE"),
            GuildMemberKick = guild.Channels.TryParse("GUILD_MEMBER_KICK"),
            GuildBanAdd = guild.Channels.TryParse("GUILD_BAN_ADD"),
            GuildBanRemove = guild.Channels.TryParse("GUILD_BAN_REMOVE"),
            InviteCreate = guild.Channels.TryParse("INVITE_CREATE"),
            InviteDelete = guild.Channels.TryParse("INVITE_DELETE"),
            MessageUpdate = guild.Channels.TryParse("MESSAGE_UPDATE"),
            MessageDelete = guild.Channels.TryParse("MESSAGE_DELETE"),
            MessageDeleteBulk = guild.Channels.TryParse("MESSAGE_DELETE_BULK"),
        };

        foreach (var (key, value) in guild.Redirects)
            if (ulong.TryParse(key, out var fromId) && ulong.TryParse(value, out var toId))
                channels.Redirects[fromId] = toId;

        await conn.ExecuteAsync(
            """
            insert into guilds (id, channels, messages, banned_systems, key_roles)
            values (@Id, @channels::jsonb, @messages::jsonb, @BannedSystems, @KeyRoles)
            """,
            new
            {
                guild.Id,
                messages,
                channels,
                guild.BannedSystems,
                guild.KeyRoles,
            },
            tx
        );
    }

    private static async Task ImportInviteAsync(
        DatabaseConnection conn,
        DbTransaction tx,
        GoInvite invite
    )
    {
        await conn.ExecuteAsync(
            "insert into invites (code, guild_id, name) values (@Code, @GuildId, @Name)",
            new
            {
                invite.GuildId,
                invite.Code,
                invite.Name,
            },
            tx
        );
    }

    private static async Task ImportWatchlistEntryAsync(
        DatabaseConnection conn,
        DbTransaction tx,
        GoWatchlistEntry watchlistEntry
    )
    {
        await conn.ExecuteAsync(
            """
            insert into watchlists (guild_id, user_id, added_at, moderator_id, reason)
            values (@GuildId, @UserId, @AddedAt, @ModeratorId, @Reason)
            """,
            new
            {
                watchlistEntry.GuildId,
                watchlistEntry.UserId,
                watchlistEntry.AddedAt,
                watchlistEntry.ModeratorId,
                watchlistEntry.Reason,
            },
            tx
        );
    }

    private static ulong TryParse(this Dictionary<string, string> dict, string key) =>
        dict.TryGetValue(key, out var raw) && ulong.TryParse(raw, out var id) ? id : 0;
}
