/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   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/>.
 */

package de.steamwar.velocitycore;

import com.google.inject.Inject;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Dependency;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.scheduler.Scheduler;
import de.steamwar.command.*;
import de.steamwar.linkage.AbstractLinker;
import de.steamwar.linkage.EventMode;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.messages.Chatter;
import de.steamwar.network.packets.PacketHandler;
import de.steamwar.persistent.ReloadablePlugin;
import de.steamwar.sql.Punishment;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.Team;
import de.steamwar.sql.internal.Statement;
import de.steamwar.velocitycore.commands.PunishmentCommand;
import de.steamwar.velocitycore.commands.ServerSwitchCommand;
import de.steamwar.velocitycore.commands.TeamCommand;
import de.steamwar.velocitycore.discord.DiscordBot;
import de.steamwar.velocitycore.discord.DiscordConfig;
import de.steamwar.velocitycore.listeners.BasicListener;
import lombok.Getter;
import lombok.NonNull;

import java.nio.file.Path;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

@Plugin(
        id = "velocitycore",
        name = "VelocityCore",
        dependencies = {@Dependency(id = "persistentvelocitycore"), @Dependency(id = "depencendiesvelocitycore")}
)
public class VelocityCore implements ReloadablePlugin {

    public static boolean MAIN_SERVER;

    private static VelocityCore instance;

    public static VelocityCore get() {
        return instance;
    }

    public static Node local;

    public static Scheduler.TaskBuilder schedule(Runnable runnable) {
        return instance.proxyServer.getScheduler().buildTask(instance, runnable);
    }

    public static ProxyServer getProxy() {
        return instance.proxyServer;
    }

    public static Logger getLogger() {
        return instance.logger;
    }

    private final ProxyServer proxyServer;
    private final Logger logger;
    @Getter
    private final Path dataDirectory;

    private Driver sqlDriver;
    @Getter
    private Config config;
    private ErrorLogger errorLogger;

    @Getter
    @LinkedInstance
    private TeamCommand teamCommand;

    @Inject
    public VelocityCore(ProxyServer proxyServer, Logger logger, @DataDirectory Path dataDirectory) {
        this.proxyServer = proxyServer;
        this.logger = logger;
        this.dataDirectory = dataDirectory;
    }

    private AbstractLinker<ReloadablePlugin> linker;

    @Subscribe
    @Override
    public void onProxyInitialization(ProxyInitializeEvent event) {
        instance = this;
        config = Config.load();
        MAIN_SERVER = proxyServer.getBoundAddress().getPort() == 25565;

        errorLogger = new ErrorLogger();

        SWCommandUtils.init((SWTypeMapperCreator<TypeMapper<Object>, Chatter, Object>) (mapper, tabCompleter) -> new TypeMapper<>() {
            @Override
            public Object map(Chatter sender, PreviousArguments previousArguments, String s) {
                return mapper.apply(s);
            }

            @Override
            public Collection<String> tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) {
                return tabCompleter.apply(sender, s);
            }
        });
        schedule(TabCompletionCache::invalidateOldEntries).repeat(1, TimeUnit.SECONDS).schedule();

        initStaticServers();

        local = new Node.LocalNode();
        if (MAIN_SERVER) {
            //new Node.RemoteNode("lx");
        }

        // Punishment Commands:
        new PunishmentCommand("ban", Punishment.PunishmentType.Ban);
        new PunishmentCommand("mute", Punishment.PunishmentType.Mute);
        new PunishmentCommand("noschemreceiving", Punishment.PunishmentType.NoSchemReceiving);
        new PunishmentCommand("noschemsharing", Punishment.PunishmentType.NoSchemSharing);
        new PunishmentCommand("noschemsubmitting", Punishment.PunishmentType.NoSchemSubmitting);
        new PunishmentCommand("nodev", Punishment.PunishmentType.NoDevServer);
        new PunishmentCommand("nofight", Punishment.PunishmentType.NoFightServer);
        new PunishmentCommand("noteamserver", Punishment.PunishmentType.NoTeamServer);
        new PunishmentCommand("note", Punishment.PunishmentType.Note);

        linker = new AbstractLinker<>(this) {
            @Override
            protected boolean eventModeCheck(@NonNull Class<?> clazz, EventMode eventMode) {
                return eventMode == null || eventMode.value() == config.isEventmode();
            }

            @Override
            protected void linkObject(Object any) {
                if (any instanceof PacketHandler) {
                    ((PacketHandler) any).register();
                }
            }

            @Override
            protected void unlinkObject(Object any) {
                if (any instanceof PacketHandler) {
                    ((PacketHandler) any).unregister();
                }
                if (any instanceof BasicListener) {
                    VelocityCore.getProxy().getEventManager().unregisterListener(instance, any);
                }
            }
        };
        try {
            linker.link();
        } catch (AbstractLinker.LinkException e) {
            logger.log(Level.SEVERE, e.getMessage(), e);
            getProxy().shutdown();
            return;
        }

        schedule(() -> {
            SteamwarUser.clear();
            Team.clear();
        }).repeat(1, TimeUnit.HOURS).schedule();

        DiscordConfig discordConfig = DiscordConfig.load();
        if (discordConfig != null) {
            try {
                new DiscordBot(discordConfig);
            } catch (Throwable e) {
                logger.log(Level.SEVERE, "Could not initialize discord bot", e);
            }
        }
    }

    @Subscribe
    @Override
    public void onProxyShutdown(ProxyShutdownEvent event) {
        try {
            DiscordBot.withBot(bot -> {
                bot.getJda().shutdownNow();
                bot.getJda().getHttpClient().connectionPool().evictAll();
                bot.getJda().getHttpClient().dispatcher().executorService().shutdown();
                try {
                    if (!bot.getJda().awaitShutdown(1, TimeUnit.SECONDS)) {
                        logger.log(Level.SEVERE, "Could not await discord bot shutdown");
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        } catch (Throwable e) {
            logger.log(Level.SEVERE, "Could not shutdown discord bot", e);
        }

        linker.unlink();
        errorLogger.unregister();
        Statement.closeAll();

        try {
            DriverManager.deregisterDriver(sqlDriver);
        } catch (SQLException e) {
            throw new SecurityException(e);
        }
    }

    private void initStaticServers() {
        for (Map.Entry<String, Config.Server> entry : config.getServers().entrySet()) {
            Config.Server server = entry.getValue();
            List<String> cmds = server.getCommands();
            String cmd = cmds.remove(0);

            if (server.getSpectatePort() != 0) {
                EventStarter.addSpectateServer(server.getSpectatePort(), cmd);
            }

            new ServerSwitchCommand(cmd, entry.getKey(), server.getSpectatePort() != 0, cmds.toArray(new String[0]));
        }
    }
}
