package de.simolzimol.mclogger.consent.listeners;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;

import de.simolzimol.mclogger.consent.ConsentConfig;
import de.simolzimol.mclogger.consent.ConsentConfig.EnforcementMode;
import de.simolzimol.mclogger.consent.ConsentPlugin;
import de.simolzimol.mclogger.consent.util.MessageUtil;
import net.kyori.adventure.text.minimessage.MiniMessage;

/**
 * Listens to player lifecycle events and enforces the configured consent mode.
 *
 * <p>Three modes:
 * <ul>
 *   <li><b>KICK</b>  – player is kicked at {@link PlayerLoginEvent} if no consent.</li>
 *   <li><b>HOLD</b>  – player joins but movement/chat/commands are blocked until
 *                       they type {@code /consent accept}.  The grace-period timer
 *                       runs as a scheduled task and kicks after the configured
 *                       timeout.</li>
 *   <li><b>REMIND</b>- player joins normally; a repeating task sends reminder
 *                       messages until they accept.</li>
 * </ul>
 *
 * @author SimolZimol
 */
public class ConsentListener implements Listener {

    private final ConsentPlugin plugin;
    /** Last time a move-blocked message was sent to a player (millis). */
    private final Map<UUID, Long> lastMoveMsgTime = new ConcurrentHashMap<>();

    public ConsentListener(ConsentPlugin plugin) {
        this.plugin = plugin;
    }

    // ─────────────────────────────────────────────────────────
    // LoginEvent: KICK mode enforcement
    // ─────────────────────────────────────────────────────────

    @EventHandler(priority = EventPriority.HIGHEST)
    public void onLogin(PlayerLoginEvent event) {
        ConsentConfig cfg = plugin.getConsentConfig();
        if (!cfg.isEnabled()) return;

        Player player = event.getPlayer();
        if (player.hasPermission(cfg.getBypassPermission())) return;

        String uuid    = player.getUniqueId().toString();
        String version = cfg.getPolicyVersion();

        // Only kick immediately in KICK mode; other modes handle it on join
        if (cfg.getEnforcementMode() == EnforcementMode.KICK) {
            boolean consented = plugin.getConsentDatabase().hasConsented(uuid, version);
            if (!consented) {
                String kickText = MessageUtil.replace(cfg.getMsgKick(), player, cfg);
                event.disallow(PlayerLoginEvent.Result.KICK_OTHER, kickText);
            }
        }
    }

    // ─────────────────────────────────────────────────────────
    // JoinEvent: HOLD / REMIND mode setup
    // ─────────────────────────────────────────────────────────

    @EventHandler(priority = EventPriority.MONITOR)
    public void onJoin(PlayerJoinEvent event) {
        ConsentConfig cfg    = plugin.getConsentConfig();
        Player        player = event.getPlayer();

        if (!cfg.isEnabled()) return;
        if (player.hasPermission(cfg.getBypassPermission())) return;
        if (isExemptWorld(player, cfg)) return;

        String uuid    = player.getUniqueId().toString();
        String version = cfg.getPolicyVersion();

        if (plugin.getConsentDatabase().hasConsented(uuid, version)) return;

        EnforcementMode mode = cfg.getEnforcementMode();

        // Open the policy book 1 tick after join (openBook can't be called during join)
        String promptMini = MessageUtil.replace(cfg.getMsgJoinPrompt(), player, cfg);
        Bukkit.getScheduler().runTaskLater(plugin, () -> {
            if (!player.isOnline()) return;
            openPolicyBook(player, promptMini);
        }, 1L);

        if (mode == EnforcementMode.HOLD) {
            plugin.getPendingConsent().add(player.getUniqueId());
            scheduleGraceKick(player);
        } else if (mode == EnforcementMode.REMIND) {
            int interval = cfg.getReminderIntervalSeconds();
            if (interval > 0) {
                scheduleReminder(player);
            }
        }
    }

    // ─────────────────────────────────────────────────────────
    // QuitEvent: clean up
    // ─────────────────────────────────────────────────────────

    @EventHandler(priority = EventPriority.MONITOR)
    public void onQuit(PlayerQuitEvent event) {
        UUID uuid = event.getPlayer().getUniqueId();
        plugin.getPendingConsent().remove(uuid);
        lastMoveMsgTime.remove(uuid);
    }

    // ─────────────────────────────────────────────────────────
    // HOLD-mode: block movement
    // ─────────────────────────────────────────────────────────

    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
    public void onMove(PlayerMoveEvent event) {
        if (!isHoldPending(event.getPlayer())) return;
        // Block if the player moved to a different block (ignore head rotation)
        if (event.getFrom().getBlockX() == event.getTo().getBlockX()
                && event.getFrom().getBlockY() == event.getTo().getBlockY()
                && event.getFrom().getBlockZ() == event.getTo().getBlockZ()) {
            return;
        }
        event.setCancelled(true);
        sendThrottled(event.getPlayer(),
                plugin.getConsentConfig().getMsgHoldMoveBlocked());
    }

    // ─────────────────────────────────────────────────────────
    // HOLD-mode: block chat
    // ─────────────────────────────────────────────────────────

    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
    public void onChat(AsyncPlayerChatEvent event) {
        if (!isHoldPending(event.getPlayer())) return;
        event.setCancelled(true);
        // Must run on main thread; schedule a tick delayed task
        UUID uuid = event.getPlayer().getUniqueId();
        Bukkit.getScheduler().runTask(plugin, () -> {
            Player p = Bukkit.getPlayer(uuid);
            if (p != null) {
                String msg = MessageUtil.replace(
                        plugin.getConsentConfig().getMsgHoldChatBlocked(), p,
                        plugin.getConsentConfig());
                p.sendMessage(MiniMessage.miniMessage().deserialize(msg));
            }
        });
    }

    // ─────────────────────────────────────────────────────────
    // HOLD-mode: block commands (except /consent and /pp)
    // ─────────────────────────────────────────────────────────

    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
    public void onCommand(PlayerCommandPreprocessEvent event) {
        if (!isHoldPending(event.getPlayer())) return;
        String cmd = event.getMessage().toLowerCase();
        // Allow /consent, /privacy, /pp (any of the registered aliases)
        if (cmd.startsWith("/consent") || cmd.startsWith("/privacy") || cmd.startsWith("/pp")) {
            return;
        }
        event.setCancelled(true);
        String msg = MessageUtil.replace(
                plugin.getConsentConfig().getMsgHoldCmdBlocked(),
                event.getPlayer(), plugin.getConsentConfig());
        event.getPlayer().sendMessage(MiniMessage.miniMessage().deserialize(msg));
    }

    // ─────────────────────────────────────────────────────────
    // HOLD-mode: block block-break / place / interact
    // ─────────────────────────────────────────────────────────

    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
    public void onBlockBreak(BlockBreakEvent event) {
        if (!isHoldPending(event.getPlayer())) return;
        event.setCancelled(true);
    }

    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
    public void onBlockPlace(BlockPlaceEvent event) {
        if (!isHoldPending(event.getPlayer())) return;
        event.setCancelled(true);
    }

    @EventHandler(priority = EventPriority.HIGHEST)
    public void onInteract(PlayerInteractEvent event) {
        if (!isHoldPending(event.getPlayer())) return;
        event.setCancelled(true);
    }

    // ─────────────────────────────────────────────────────────
    // Internal helpers
    // ─────────────────────────────────────────────────────────

    private boolean isHoldPending(Player player) {
        ConsentConfig cfg = plugin.getConsentConfig();
        if (!cfg.isEnabled()) return false;
        if (cfg.getEnforcementMode() != EnforcementMode.HOLD) return false;
        return plugin.getPendingConsent().contains(player.getUniqueId());
    }

    private boolean isExemptWorld(Player player, ConsentConfig cfg) {
        String worldName = player.getWorld().getName();
        return cfg.getExemptWorlds().contains(worldName);
    }

    /**
     * Opens a written-book GUI with the policy prompt so the player can read
     * the full text including a clickable URL, without cluttering chat.
     * A short plain-chat hint is also sent so players know why the book opened.
     */
    private void openPolicyBook(Player player, String promptMini) {
        MiniMessage mm = MiniMessage.miniMessage();

        ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
        BookMeta meta = (BookMeta) Objects.requireNonNull(book.getItemMeta());
        meta.title(mm.deserialize("<bold><blue>Privacy Policy</blue></bold>"));
        meta.author(Component.text("MCLogger"));

        // First page: the configured join-prompt text (supports MiniMessage incl. click URLs)
        Component page1 = mm.deserialize(promptMini);
        meta.pages(List.of(page1));

        book.setItemMeta(meta);
        player.openBook(book);

        // Also send a brief chat message so the book title isn't the only indicator
        String hint = MessageUtil.replace(
                plugin.getConsentConfig().getMsgJoinPrompt(), player, plugin.getConsentConfig());
        player.sendMessage(mm.deserialize(hint));
    }

    /**
     * Kicks a player from the server or – if velocity-kick is enabled – from
     * the entire network.  Delegates to {@link ConsentPlugin#networkKick}.
     */
    void doKick(Player player, Component reason) {
        plugin.networkKick(player, reason);
    }

    /**
     * Sends a MiniMessage-formatted message to the player with a cooldown to
     * avoid flooding the chat (e.g. when the player spams movement).
     */
    private void sendThrottled(Player player, String miniMessageText) {
        long now      = System.currentTimeMillis();
        long cooldown = plugin.getConsentConfig().getMoveMessageCooldownSeconds() * 1000L;
        Long lastSent = lastMoveMsgTime.get(player.getUniqueId());
        if (lastSent != null && (now - lastSent) < cooldown) return;

        lastMoveMsgTime.put(player.getUniqueId(), now);
        String msg = MessageUtil.replace(miniMessageText, player, plugin.getConsentConfig());
        player.sendMessage(MiniMessage.miniMessage().deserialize(msg));
    }

    /**
     * Schedules a task that kicks the player after the grace period if they
     * still haven't consented.  Silently cancelled if the player leaves first
     * or accepts in time.
     */
    private void scheduleGraceKick(Player player) {
        int graceSec = plugin.getConsentConfig().getGracePeriodSeconds();
        if (graceSec <= 0) return; // infinite grace

        UUID uuid = player.getUniqueId();

        // Warning at half of the grace period
        int warnTick = (graceSec / 2) * 20;
        if (warnTick > 0) {
            Bukkit.getScheduler().runTaskLater(plugin, () -> {
                Player p = Bukkit.getPlayer(uuid);
                if (p == null || !plugin.getPendingConsent().contains(uuid)) return;
                String msg = MessageUtil.replace(
                        plugin.getConsentConfig().getMsgGraceWarning()
                                .replace("{seconds}", String.valueOf(graceSec / 2)),
                        p, plugin.getConsentConfig());
                p.sendMessage(MiniMessage.miniMessage().deserialize(msg));
            }, warnTick);
        }

        // Final kick
        Bukkit.getScheduler().runTaskLater(plugin, () -> {
            Player p = Bukkit.getPlayer(uuid);
            if (p == null) return;
            if (!plugin.getPendingConsent().contains(uuid)) return;
            plugin.getPendingConsent().remove(uuid);
            String kickMsg = MessageUtil.replace(
                    plugin.getConsentConfig().getMsgGraceKick(), p, plugin.getConsentConfig());
            doKick(p, MiniMessage.miniMessage().deserialize(kickMsg));
        }, (long) graceSec * 20);
    }

    /**
     * Schedules a repeating reminder task in REMIND mode.
     * Cancels automatically when the player has consented or leaves.
     */
    private void scheduleReminder(Player player) {
        int intervalSec = plugin.getConsentConfig().getReminderIntervalSeconds();
        if (intervalSec <= 0) return;

        UUID uuid = player.getUniqueId();
        long intervalTicks = (long) intervalSec * 20;
        final int[] taskId = {-1};

        taskId[0] = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
            Player p = Bukkit.getPlayer(uuid);
            if (p == null) {
                Bukkit.getScheduler().cancelTask(taskId[0]);
                return;
            }
            // Stop reminding once they've accepted
            if (plugin.getConsentDatabase().hasConsented(
                    uuid.toString(), plugin.getConsentConfig().getPolicyVersion())) {
                Bukkit.getScheduler().cancelTask(taskId[0]);
                return;
            }
            String msg = MessageUtil.replace(
                    plugin.getConsentConfig().getMsgRemind(), p, plugin.getConsentConfig());
            p.sendMessage(MiniMessage.miniMessage().deserialize(msg));
        }, intervalTicks, intervalTicks).getTaskId();
    }
}
