package de.simolzimol.mclogger.velocity.listeners;

import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.event.player.PlayerChatEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.proxy.Player;
import de.simolzimol.mclogger.velocity.VelocityLoggerPlugin;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * All Velocity event listeners:
 *  - Login / Disconnect
 *  - Server switch
 *  - Chat
 *  - Commands
 *
 * @author SimolZimol
 */
public class VelocityEventListener {

    private final VelocityLoggerPlugin plugin;
    /** Login timestamps for session duration calculation */
    private final Map<UUID, Long> loginTimes = new ConcurrentHashMap<>();
    /** Commands already logged by a Paper backend – Velocity skips these */
    private final Set<String> paperLoggedCommands = ConcurrentHashMap.newKeySet();

    public VelocityEventListener(VelocityLoggerPlugin plugin) {
        this.plugin = plugin;
    }

    // --------------------------------------------------------
    // Login
    // --------------------------------------------------------

    @Subscribe(order = PostOrder.LAST)
    public void onLogin(PostLoginEvent event) {
        Player p = event.getPlayer();
        loginTimes.put(p.getUniqueId(), System.currentTimeMillis());

        String ip = p.getRemoteAddress() != null
                ? p.getRemoteAddress().getAddress().getHostAddress() : "unknown";

        // Upsert player in DB
        plugin.getDb().upsertPlayer(p.getUniqueId().toString(), p.getUsername(), ip);

        // Open session row (server_name is filled in on disconnect with last known backend)
        String clientBrand = p.getClientBrand() != null ? p.getClientBrand() : "unknown";
        plugin.getDb().insertSessionLogin(p.getUniqueId().toString(), p.getUsername(), ip, clientBrand);

        Map<String, Object> details = new HashMap<>();
        details.put("ip", ip);
        details.put("client_brand", p.getClientBrand() != null ? p.getClientBrand() : "unknown");
        details.put("protocol", p.getProtocolVersion().getProtocol());
        details.put("ping", p.getPing());
        details.put("online_mode", !p.isOnlineMode() ? "offline/bedrock" : "java");

        plugin.getDb().insertProxyEvent(
                "login",
                p.getUniqueId().toString(),
                p.getUsername(),
                null, null, ip, details
        );

        plugin.getLogger().info("[MCLogger] LOGIN: " + p.getUsername() + " (" + ip + ")");
    }

    // --------------------------------------------------------
    // Disconnect
    // --------------------------------------------------------

    @Subscribe(order = PostOrder.LAST)
    public void onDisconnect(DisconnectEvent event) {
        Player p = event.getPlayer();
        UUID uuid = p.getUniqueId();

        Long loginTimeMs = loginTimes.remove(uuid);
        long durationSec = loginTimeMs != null ? (System.currentTimeMillis() - loginTimeMs) / 1000 : 0;

        String lastServer = p.getCurrentServer()
                .map(s -> s.getServerInfo().getName())
                .orElse(plugin.getDb().getProxyName());

        // Close the session opened in onLogin
        plugin.getDb().closeSession(uuid.toString(), lastServer, durationSec);

        Map<String, Object> details = new HashMap<>();
        details.put("reason", event.getLoginStatus().name());
        details.put("session_duration_sec", durationSec);
        details.put("current_server", lastServer);

        plugin.getDb().insertProxyEvent(
                "disconnect",
                uuid.toString(),
                p.getUsername(),
                p.getCurrentServer().map(s -> s.getServerInfo().getName()).orElse(null),
                null, null, details
        );

        plugin.getLogger().info("[MCLogger] DISCONNECT: " + p.getUsername() +
                " (duration: " + durationSec + "s, reason: " + event.getLoginStatus().name() + ")");
    }

    // --------------------------------------------------------
    // Server switch (after switching)
    // --------------------------------------------------------

    @Subscribe(order = PostOrder.LAST)
    public void onServerConnected(ServerConnectedEvent event) {
        Player p = event.getPlayer();
        String from = event.getPreviousServer().map(s -> s.getServerInfo().getName()).orElse(null);
        String to   = event.getServer().getServerInfo().getName();

        Map<String, Object> details = new HashMap<>();
        details.put("ping", p.getPing());

        plugin.getDb().insertProxyEvent(
                "server_switch",
                p.getUniqueId().toString(),
                p.getUsername(),
                from, to, null, details
        );

        plugin.getLogger().info("[MCLogger] SERVER-SWITCH: " + p.getUsername() +
                " | " + (from != null ? from : "joining") + " → " + to);
    }

    // --------------------------------------------------------
    // Server connect attempt (before switching)
    // --------------------------------------------------------

    @Subscribe(order = PostOrder.LAST)
    public void onServerPreConnect(ServerPreConnectEvent event) {
        if (event.getResult().getServer().isEmpty()) return;
        Player p = event.getPlayer();

        Map<String, Object> details = new HashMap<>();
        details.put("target_server", event.getResult().getServer().get().getServerInfo().getName());

        plugin.getDb().insertProxyEvent(
                "server_switch",
                p.getUniqueId().toString(),
                p.getUsername(),
                p.getCurrentServer().map(s -> s.getServerInfo().getName()).orElse(null),
                event.getResult().getServer().get().getServerInfo().getName(),
                null, details
        );
    }

    // --------------------------------------------------------
    // Commands – only proxy-native commands (e.g. /server, /glist)
    // Commands – Paper logs with position and notifies proxy; Velocity is fallback.
    // --------------------------------------------------------

    /**
     * Receives plugin messages from Paper backends.
     * When Paper has logged a command, it sends the key here so Velocity skips it.
     */
    @Subscribe
    public void onPluginMessage(PluginMessageEvent event) {
        if (!event.getIdentifier().getId().equals("mclogger:logged")) return;
        // Prevent Velocity from forwarding this internal message to the client
        event.setResult(PluginMessageEvent.ForwardResult.handled());
        String key = new String(event.getData(), StandardCharsets.UTF_8);
        paperLoggedCommands.add(key);
    }

    /**
     * Schedules command logging with a 300 ms delay.
     * If a Paper backend signals it already logged the command, the task is cancelled.
     * On servers without the Paper plugin, Velocity logs it as fallback.
     */
    @Subscribe(order = PostOrder.LAST)
    public void onCommand(CommandExecuteEvent event) {
        if (!(event.getCommandSource() instanceof Player)) return;
        Player p = (Player) event.getCommandSource();

        String rawCommand = event.getCommand();
        // Key format matches what Paper sends: uuid|/command args
        String key = p.getUniqueId() + "|/" + rawCommand;
        String serverName = p.getCurrentServer()
                .map(s -> s.getServerInfo().getName())
                .orElse(plugin.getDb().getProxyName());

        plugin.getServer().getScheduler()
                .buildTask(plugin, () -> {
                    // If Paper already sent us a confirmation, skip – avoid duplicate
                    if (paperLoggedCommands.remove(key)) return;
                    plugin.getDb().insertCommandWithServer(
                            p.getUniqueId().toString(),
                            p.getUsername(),
                            serverName,
                            "/" + rawCommand
                    );
                })
                .delay(Duration.ofMillis(300))
                .schedule();
    }

    // Note: Chat is logged at proxy level (see onChat below).

    // --------------------------------------------------------
    // Chat – logged at proxy level with correct backend server name
    // --------------------------------------------------------

    @Subscribe(order = PostOrder.LAST)
    public void onChat(PlayerChatEvent event) {
        if (!event.getResult().isAllowed()) return;
        Player p = event.getPlayer();
        String serverName = p.getCurrentServer()
                .map(s -> s.getServerInfo().getName())
                .orElse(plugin.getDb().getProxyName());
        plugin.getDb().insertChatWithServer(
                p.getUniqueId().toString(),
                p.getUsername(),
                serverName,
                event.getMessage()
        );
    }
}
