package com.xfestudio.mydimension.builder.blueprint;

import com.xfestudio.mydimension.network.ModNetwork;
import com.xfestudio.mydimension.builder.PendingBuildData;
import com.xfestudio.mydimension.builder.RealmwrightData;
import com.xfestudio.mydimension.builder.BuilderReachValidator;
import com.xfestudio.mydimension.config.BuilderConfig;
import com.xfestudio.mydimension.network.blueprint.BlueprintTransferBeginPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintTransferChunkPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintTransferEndPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintTransferResultPacket;
import com.xfestudio.mydimension.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.Level;
import net.minecraftforge.network.PacketDistributor;

import java.io.IOException;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.UUID;
import java.util.WeakHashMap;

/** Server-owned blueprint transfer, validation, cache and whole-plan queue. */
public final class BlueprintServerService {
    private static final int CAPTURE_COOLDOWN_TICKS = 20;
    private static final int PLACE_COOLDOWN_TICKS = 5;
    private static final Map<MinecraftServer, BlueprintServerService> SERVICES = new WeakHashMap<>();
    private final MinecraftServer server;
    private final BlueprintServerCache cache = new BlueprintServerCache();
    private final Map<UUID, UploadSession> uploads = new HashMap<>();
    private final Map<UUID, Queue<QueuedPlacement>> placementQueues = new HashMap<>();
    private final BlueprintSelectionStore selections = new BlueprintSelectionStore();
    private final Map<UUID, Long> lastCaptureTicks = new HashMap<>();
    private final Map<UUID, Long> lastPlaceTicks = new HashMap<>();

    private BlueprintServerService(MinecraftServer server) {
        this.server = server;
    }

    public static synchronized BlueprintServerService get(MinecraftServer server) {
        return SERVICES.computeIfAbsent(server, BlueprintServerService::new);
    }

    public static synchronized void shutdown(MinecraftServer server) {
        BlueprintServerService service = SERVICES.remove(server);
        if (service != null) {
            service.pauseQueuedPlacements();
            service.clear();
        }
    }

    /**
     * Records the first corner at click time.  The completed capture packet is
     * intentionally not trusted to introduce a first corner of its own: doing
     * so would either force a long selection's old corner to remain in reach,
     * or let a modified client read an arbitrary loaded area.
     */
    public void beginSelection(ServerPlayer player, BlockPos first) {
        UUID playerId = player.getUUID();
        selections.clear(playerId);
        try {
            if (!BuilderConfig.isEnabled()
                    || !player.getMainHandItem().is(ModItems.REALMWRIGHT_SCEPTER.get())) {
                throw new IllegalArgumentException("The Realmwright Scepter must be enabled and held");
            }
            requireBuildablePosition(player, first, "Blueprint selection corner");
            requireAirAnchorReachAndSight(player, first);
            selections.begin(playerId, first, player.level().dimension(),
                    RealmwrightData.id(player.getMainHandItem()));
        } catch (Exception exception) {
            player.displayClientMessage(Component.literal(safeMessage(exception)), false);
        }
    }

    public void capture(ServerPlayer player, UUID requestId, BlockPos first, BlockPos second,
                        BlueprintSaveMode mode, String name, boolean finishSelection) {
        if (!acquireCooldown(lastCaptureTicks, player.getUUID(), CAPTURE_COOLDOWN_TICKS)) {
            sendResult(player, requestId, false, null, "Blueprint capture is cooling down");
            return;
        }
        try {
            BlueprintSelectionStore.Selection selection = requireSelection(player, first, second);
            BlueprintSaveMode effectiveMode = mode;
            if (mode == BlueprintSaveMode.FULL && !mayUseFullData(player)) {
                effectiveMode = BlueprintSaveMode.BLOCKS_ONLY;
                player.displayClientMessage(Component.translatable(
                        "message.mydimension.builder.blueprint_downgraded"), false);
            }
            BlueprintData blueprint = BlueprintCapture.capture(player.serverLevel(), player, first, second,
                    effectiveMode, name);
            byte[] compressed = BlueprintIo.encode(blueprint,
                    BuilderConfig.MAX_BLUEPRINT_UNCOMPRESSED_BYTES.get(),
                    BuilderConfig.MAX_BLUEPRINT_COMPRESSED_BYTES.get());
            BlueprintServerCache.Entry entry = cache.put(player, blueprint, compressed, server.getTickCount());
            sendDownload(player, requestId, entry);
            if (selection.second() == null || finishSelection) {
                // Keep the validated pair while its source cuboid remains visible on the client. This lets
                // SAVE be used repeatedly for Save As / replace without asking the player to select both
                // corners again. The tiny selection lives until an actual lifecycle event clears it.
                selections.complete(player.getUUID(), selection, second);
            }
        } catch (Exception exception) {
            sendResult(player, requestId, false, null, safeMessage(exception));
        }
    }

    public void beginUpload(ServerPlayer player, UUID transferId, int byteLength, int chunkCount, byte[] sha256) {
        UUID playerId = player.getUUID();
        if (uploads.containsKey(playerId)) {
            sendResult(player, transferId, false, null, "A blueprint upload is already active");
            return;
        }
        try {
            if (byteLength > Math.min(BlueprintLimits.MAX_COMPRESSED_BYTES,
                    BuilderConfig.MAX_BLUEPRINT_COMPRESSED_BYTES.get())) {
                throw new IllegalArgumentException("Blueprint upload exceeds the configured compressed limit");
            }
            BlueprintTransferAssembler assembler = new BlueprintTransferAssembler(transferId, byteLength,
                    chunkCount, sha256, server.getTickCount());
            uploads.put(playerId, new UploadSession(assembler));
        } catch (IllegalArgumentException exception) {
            sendResult(player, transferId, false, null, exception.getMessage());
        }
    }

    public void acceptUploadChunk(ServerPlayer player, UUID transferId, int sequence, byte[] data) {
        UploadSession session = uploads.get(player.getUUID());
        if (session == null || !session.assembler().transferId().equals(transferId)) {
            sendResult(player, transferId, false, null, "No matching blueprint upload exists");
            return;
        }
        try {
            session.assembler().accept(sequence, data, server.getTickCount());
        } catch (IOException exception) {
            uploads.remove(player.getUUID());
            sendResult(player, transferId, false, null, exception.getMessage());
        }
    }

    public void finishUpload(ServerPlayer player, UUID transferId) {
        UploadSession session = uploads.remove(player.getUUID());
        if (session == null || !session.assembler().transferId().equals(transferId)) {
            sendResult(player, transferId, false, null, "No matching blueprint upload exists");
            return;
        }
        try {
            byte[] compressed = session.assembler().finish(server.getTickCount());
            BlueprintData blueprint = BlueprintIo.decode(compressed,
                    BuilderConfig.MAX_BLUEPRINT_COMPRESSED_BYTES.get(),
                    BuilderConfig.MAX_BLUEPRINT_UNCOMPRESSED_BYTES.get());
            BlueprintServerCache.Entry entry = cache.put(player, blueprint, compressed, server.getTickCount());
            sendResult(player, transferId, true, entry.token(), "Blueprint ready");
        } catch (Exception exception) {
            sendResult(player, transferId, false, null, safeMessage(exception));
        }
    }

    public void cancelUpload(ServerPlayer player, UUID transferId) {
        UploadSession session = uploads.get(player.getUUID());
        if (session != null && session.assembler().transferId().equals(transferId)) {
            uploads.remove(player.getUUID());
        }
    }

    public void requestPlacement(ServerPlayer player, UUID requestId, UUID cacheToken,
                                 BlockPos targetAnchor, BlueprintTransform transform) {
        if (!acquireCooldown(lastPlaceTicks, player.getUUID(), PLACE_COOLDOWN_TICKS)) {
            sendResult(player, requestId, false, null, "Blueprint placement is cooling down");
            return;
        }
        ItemStack scepter = player.getMainHandItem();
        if (!BuilderConfig.isEnabled() || !scepter.is(ModItems.REALMWRIGHT_SCEPTER.get())) {
            sendResult(player, requestId, false, null, "The Realmwright Scepter must be enabled and held");
            return;
        }
        UUID scepterId = RealmwrightData.id(scepter);
        boolean recordHistory = RealmwrightData.recordsHistory(scepter);
        ResourceKey<Level> dimension = player.level().dimension();
        if (hasWorkConflict(player)) {
            sendResult(player, requestId, false, null,
                    "Finish or cancel the active or material-waiting builder task first");
            return;
        }
        Optional<BlueprintServerCache.Entry> resolved = cache.resolve(player, cacheToken, server.getTickCount());
        if (resolved.isEmpty()) {
            sendResult(player, requestId, false, null, "Blueprint cache token is missing or expired");
            return;
        }
        BlueprintPlacementPlan plan;
        try {
            requireBuildablePosition(player, targetAnchor, "Blueprint anchor");
            requireAirAnchorReachAndSight(player, targetAnchor);
            plan = BlueprintPlacementPlan.create(resolved.get().blueprint(), transform, targetAnchor);
            if (!plan.fullDataAllowedFor(player)) {
                throw new IllegalArgumentException("Full-data blueprint placement is forbidden by the server policy");
            }
            validateDestination(player, plan);
        } catch (Exception exception) {
            sendResult(player, requestId, false, null, safeMessage(exception));
            return;
        }
        // Revalidate the identity-bearing execution context immediately before publication. All accesses
        // happen on the server thread, but keeping this check here prevents future asynchronous callers
        // from accidentally enqueueing a plan under a changed hand or dimension.
        if (!BuilderConfig.isEnabled() || !player.level().dimension().equals(dimension)
                || !player.getMainHandItem().is(ModItems.REALMWRIGHT_SCEPTER.get())
                || !RealmwrightData.id(player.getMainHandItem()).equals(scepterId)
                || hasWorkConflict(player)) {
            sendResult(player, requestId, false, null, "The blueprint execution context changed");
            return;
        }
        Queue<QueuedPlacement> queue = placementQueues.computeIfAbsent(player.getUUID(), ignored -> new ArrayDeque<>());
        if (queue.size() >= BlueprintLimits.MAX_QUEUED_PLANS_PER_PLAYER) {
            sendResult(player, requestId, false, null, "Blueprint placement queue is full");
            return;
        }
        queue.add(new QueuedPlacement(plan, dimension, scepterId, recordHistory));
        sendResult(player, requestId, true, cacheToken, "Blueprint queued");
    }

    /** Called by the builder transaction engine; each value represents one whole auto-queued blueprint. */
    public Optional<QueuedPlacement> pollPlacementPlan(ServerPlayer player, ItemStack scepter) {
        Queue<QueuedPlacement> queue = placementQueues.get(player.getUUID());
        if (queue == null) return Optional.empty();
        QueuedPlacement queued = queue.peek();
        if (queued == null || !BuilderConfig.isEnabled()
                || !scepter.is(ModItems.REALMWRIGHT_SCEPTER.get())
                || !queued.scepterId().equals(RealmwrightData.id(scepter))
                || !queued.dimension().equals(player.level().dimension())
                || PendingBuildData.get(server).get(player.getUUID()) != null
                || BlueprintTaskManager.get(server).hasActive(player.getUUID())) {
            return Optional.empty();
        }
        queue.remove();
        if (queue.isEmpty()) placementQueues.remove(player.getUUID());
        return Optional.of(queued);
    }

    /** Cancels all of this player's not-yet-started plans, including one bound in another dimension. */
    public boolean cancelQueuedPlacement(ServerPlayer player) {
        Queue<QueuedPlacement> removed = placementQueues.remove(player.getUUID());
        return removed != null && !removed.isEmpty();
    }

    public void tick() {
        int tick = server.getTickCount();
        Iterator<Map.Entry<UUID, UploadSession>> iterator = uploads.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<UUID, UploadSession> entry = iterator.next();
            if (entry.getValue().assembler().timedOut(tick)) {
                ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey());
                if (player != null) {
                    sendResult(player, entry.getValue().assembler().transferId(), false, null,
                            "Blueprint upload timed out");
                }
                iterator.remove();
            }
        }
        cache.tick(tick);
    }

    public void removePlayer(ServerPlayer player) {
        UUID playerId = player.getUUID();
        persistQueuedPlacement(player, placementQueues.get(playerId));
        uploads.remove(playerId);
        placementQueues.remove(playerId);
        selections.clear(playerId);
        lastCaptureTicks.remove(playerId);
        lastPlaceTicks.remove(playerId);
        cache.removePlayer(playerId);
    }

    private void pauseQueuedPlacements() {
        for (Map.Entry<UUID, Queue<QueuedPlacement>> entry : List.copyOf(placementQueues.entrySet())) {
            ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey());
            if (player != null) persistQueuedPlacement(player, entry.getValue());
        }
    }

    private void persistQueuedPlacement(ServerPlayer player, Queue<QueuedPlacement> queue) {
        if (queue == null || queue.isEmpty()
                || PendingBuildData.get(server).get(player.getUUID()) != null) return;
        QueuedPlacement queued = queue.peek();
        List<PendingBuildData.Entry> remaining = queued.plan().blocks().stream()
                .map(block -> new PendingBuildData.Entry(block.worldPos(), block.state(), block.blockEntityTag()))
                .toList();
        PendingBuildData.get(server).put(player.getUUID(), new PendingBuildData.Task(queued.scepterId(),
                UUID.randomUUID(), queued.dimension(),
                com.xfestudio.mydimension.builder.history.BuilderTransaction.Type.BLUEPRINT,
                queued.recordHistory(), true, false, 0, remaining.size(), remaining,
                System.currentTimeMillis()));
    }

    public void clear() {
        uploads.clear();
        placementQueues.clear();
        selections.clearAll();
        lastCaptureTicks.clear();
        lastPlaceTicks.clear();
        cache.clear();
    }

    public void rejectRequest(ServerPlayer player, UUID requestId, String message) {
        UploadSession upload = uploads.get(player.getUUID());
        if (upload != null && upload.assembler().transferId().equals(requestId)) {
            uploads.remove(player.getUUID());
        }
        sendResult(player, requestId, false, null, message);
    }

    private static void requireAirAnchorReachAndSight(ServerPlayer player, BlockPos pos) {
        if (!BuilderReachValidator.canReach(player, pos)) {
            throw new IllegalArgumentException("Blueprint position is outside the allowed interaction distance");
        }
        if (!BuilderReachValidator.canReachAirAnchor(player, pos)) {
            throw new IllegalArgumentException("Blueprint position is not in the player's line of sight");
        }
    }

    public void clearSelection(UUID playerId) {
        selections.clear(playerId);
    }

    private BlueprintSelectionStore.Selection requireSelection(ServerPlayer player, BlockPos first,
                                                                BlockPos second) {
        BlueprintSelectionStore.Selection selection = selections.get(player.getUUID());
        if (selection == null) {
            throw new IllegalArgumentException("Blueprint selection start is missing");
        }
        if (!selection.first().equals(first)) {
            throw new IllegalArgumentException("Blueprint selection start does not match the validated corner");
        }
        if (!selection.dimension().equals(player.level().dimension())
                || !player.getMainHandItem().is(ModItems.REALMWRIGHT_SCEPTER.get())
                || !selection.scepterId().equals(RealmwrightData.id(player.getMainHandItem()))) {
            selections.clear(player.getUUID());
            throw new IllegalArgumentException("Blueprint selection tool or dimension changed");
        }
        requireBuildablePosition(player, first, "Blueprint selection corner");
        requireBuildablePosition(player, second, "Blueprint selection corner");
        if (selection.second() == null) {
            // Only the newly clicked endpoint must still be visible.  The first endpoint was verified by
            // beginSelection at its own click time and remains bound to this player, tool and dimension.
            requireAirAnchorReachAndSight(player, second);
        } else if (!selection.second().equals(second)) {
            throw new IllegalArgumentException("Blueprint selection end does not match the validated corner");
        }
        return selection;
    }

    private static void requireBuildablePosition(ServerPlayer player, BlockPos pos, String label) {
        if (player.serverLevel().isOutsideBuildHeight(pos)
                || !player.serverLevel().getWorldBorder().isWithinBounds(pos)) {
            throw new IllegalArgumentException(label + " is outside the buildable world");
        }
    }

    private static boolean mayUseFullData(ServerPlayer player) {
        return switch (BuilderConfig.fullBlockEntityPolicy()) {
            case NEVER -> false;
            case CREATIVE_ONLY -> player.isCreative();
            case OP_ONLY -> player.hasPermissions(2);
            case CREATIVE_OR_OP -> player.isCreative() || player.hasPermissions(2);
        };
    }

    private boolean hasWorkConflict(ServerPlayer player) {
        if (PendingBuildData.get(server).get(player.getUUID()) != null
                || BlueprintTaskManager.get(server).hasActive(player.getUUID())) {
            return true;
        }
        Queue<QueuedPlacement> queued = placementQueues.get(player.getUUID());
        return queued != null && !queued.isEmpty();
    }

    private boolean acquireCooldown(Map<UUID, Long> timestamps, UUID playerId, int minimumTicks) {
        long now = server.overworld().getGameTime();
        Long previous = timestamps.get(playerId);
        if (previous != null && now - previous < minimumTicks) return false;
        timestamps.put(playerId, now);
        return true;
    }

    private static void validateDestination(ServerPlayer player, BlueprintPlacementPlan plan) {
        for (BlueprintPlacementPlan.PlannedBlock block : plan.blocks()) {
            BlockPos pos = block.worldPos();
            if (player.serverLevel().isOutsideBuildHeight(pos)
                    || !player.serverLevel().getWorldBorder().isWithinBounds(pos)) {
                throw new IllegalArgumentException("Blueprint extends outside the buildable world");
            }
            // Chunks are leased one at a time by BlueprintTaskManager immediately before mutation.
        }
    }

    private static void sendDownload(ServerPlayer player, UUID transferId, BlueprintServerCache.Entry entry) {
        byte[] compressed = entry.compressed();
        int chunks = (compressed.length + BlueprintLimits.TRANSFER_CHUNK_BYTES - 1)
                / BlueprintLimits.TRANSFER_CHUNK_BYTES;
        ModNetwork.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player),
                new BlueprintTransferBeginPacket(transferId, entry.token(), compressed.length, chunks, entry.sha256()));
        for (int sequence = 0; sequence < chunks; sequence++) {
            int offset = sequence * BlueprintLimits.TRANSFER_CHUNK_BYTES;
            int length = Math.min(BlueprintLimits.TRANSFER_CHUNK_BYTES, compressed.length - offset);
            byte[] data = java.util.Arrays.copyOfRange(compressed, offset, offset + length);
            ModNetwork.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player),
                    new BlueprintTransferChunkPacket(transferId, sequence, data));
        }
        ModNetwork.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player),
                new BlueprintTransferEndPacket(transferId));
    }

    private static void sendResult(ServerPlayer player, UUID requestId, boolean success, UUID token, String message) {
        ModNetwork.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player),
                new BlueprintTransferResultPacket(requestId, success, token, message));
    }

    private static String safeMessage(Exception exception) {
        String message = exception.getMessage();
        String safe = message == null || message.isBlank() ? exception.getClass().getSimpleName() : message;
        return safe.length() <= 240 ? safe : safe.substring(0, 240);
    }

    private record UploadSession(BlueprintTransferAssembler assembler) {
    }

    /** Immutable binding captured when the server accepts a placement request. */
    public record QueuedPlacement(BlueprintPlacementPlan plan, ResourceKey<Level> dimension, UUID scepterId,
                                  boolean recordHistory) {
    }
}
