package com.xfestudio.mydimension.network.builder;

import com.xfestudio.mydimension.builder.BuilderHistoryService;
import com.xfestudio.mydimension.builder.BuilderInteractionPolicy;
import com.xfestudio.mydimension.builder.BuilderMode;
import com.xfestudio.mydimension.builder.BuilderNetworkBridge;
import com.xfestudio.mydimension.builder.BuilderOperationManager;
import com.xfestudio.mydimension.builder.BuilderRuntime;
import com.xfestudio.mydimension.builder.BuilderReachValidator;
import com.xfestudio.mydimension.builder.PendingBuildData;
import com.xfestudio.mydimension.builder.RealmwrightData;
import com.xfestudio.mydimension.builder.ResonantAnchorTarget;
import com.xfestudio.mydimension.builder.SurfaceMatchMode;
import com.xfestudio.mydimension.builder.anchor.AnchorBindings;
import com.xfestudio.mydimension.builder.blueprint.BlueprintServerService;
import com.xfestudio.mydimension.builder.history.BuilderTransaction;
import com.xfestudio.mydimension.config.BuilderConfig;
import com.xfestudio.mydimension.network.ModNetwork;
import com.xfestudio.mydimension.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraftforge.network.NetworkEvent;

import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;

/**
 * Compact C2S intent packet for non-blueprint Realmwright operations. No item
 * or world state supplied by the client is trusted by the handler.
 */
public final class BuilderCommandPacket {
    private final Action action;
    @Nullable private final BuilderMode mode;
    @Nullable private final SurfaceMatchMode match;
    private final int first;
    private final int second;
    @Nullable private final UUID id;
    @Nullable private final Target target;
    @Nullable private final String text;

    private BuilderCommandPacket(Action action, @Nullable BuilderMode mode,
                                 @Nullable SurfaceMatchMode match, int first, int second,
                                 @Nullable UUID id, @Nullable Target target, @Nullable String text) {
        this.action = action;
        this.mode = mode;
        this.match = match;
        this.first = first;
        this.second = second;
        this.id = id;
        this.target = target;
        this.text = text;
    }

    public static BuilderCommandPacket requestSnapshot() {
        return simple(Action.REQUEST_SNAPSHOT);
    }

    public static BuilderCommandPacket openMenu() {
        return simple(Action.OPEN_MENU);
    }

    public static BuilderCommandPacket setMode(BuilderMode mode) {
        return new BuilderCommandPacket(Action.SET_MODE, mode, null, 0, 0, null, null, null);
    }

    public static BuilderCommandPacket setMatch(SurfaceMatchMode match) {
        return new BuilderCommandPacket(Action.SET_MATCH, null, match, 0, 0, null, null, null);
    }

    public static BuilderCommandPacket setLimits(int buildLimit, int demolishLimit) {
        return new BuilderCommandPacket(Action.SET_LIMITS, null, null,
                buildLimit, demolishLimit, null, null, null);
    }

    public static BuilderCommandPacket setHistoryRecording(boolean enabled) {
        return new BuilderCommandPacket(Action.SET_HISTORY_RECORDING, null, null,
                enabled ? 1 : 0, 0, null, null, null);
    }

    public static BuilderCommandPacket setAllowReplacement(boolean enabled) {
        return new BuilderCommandPacket(Action.SET_ALLOW_REPLACEMENT, null, null,
                enabled ? 1 : 0, 0, null, null, null);
    }

    public static BuilderCommandPacket use(Target target) {
        return use(target, false);
    }

    public static BuilderCommandPacket use(Target target, boolean interactionOverride) {
        return new BuilderCommandPacket(Action.USE, null, null,
                interactionOverride ? 1 : 0, 0, null, target, null);
    }

    public static BuilderCommandPacket resume(@Nullable UUID activeJobId) {
        return new BuilderCommandPacket(Action.RESUME, null, null, 0, 0, activeJobId, null, null);
    }

    public static BuilderCommandPacket cancel(@Nullable UUID activeJobId) {
        return new BuilderCommandPacket(Action.CANCEL, null, null, 0, 0, activeJobId, null, null);
    }

    public static BuilderCommandPacket cancelBlueprint() {
        return simple(Action.CANCEL_BLUEPRINT);
    }

    public static BuilderCommandPacket undo() {
        return simple(Action.UNDO);
    }

    public static BuilderCommandPacket redo() {
        return simple(Action.REDO);
    }

    public static BuilderCommandPacket unbindAnchor(UUID anchorId) {
        return new BuilderCommandPacket(Action.UNBIND_ANCHOR, null, null, 0, 0, anchorId, null, null);
    }

    public static BuilderCommandPacket moveAnchor(UUID anchorId, int delta) {
        return new BuilderCommandPacket(Action.MOVE_ANCHOR, null, null,
                Integer.compare(delta, 0), 0, anchorId, null, null);
    }

    public static BuilderCommandPacket setAnchorPublic(UUID anchorId, boolean value) {
        return new BuilderCommandPacket(Action.SET_ANCHOR_PUBLIC, null, null, value ? 1 : 0, 0,
                anchorId, null, null);
    }

    public static BuilderCommandPacket setAnchorPlayer(UUID anchorId, String playerName, boolean authorize) {
        return new BuilderCommandPacket(Action.SET_ANCHOR_PLAYER, null, null, authorize ? 1 : 0, 0,
                anchorId, null, playerName);
    }

    private static BuilderCommandPacket simple(Action action) {
        return new BuilderCommandPacket(action, null, null, 0, 0, null, null, null);
    }

    public static void encode(BuilderCommandPacket packet, FriendlyByteBuf buffer) {
        buffer.writeEnum(packet.action);
        switch (packet.action) {
            case SET_MODE -> buffer.writeEnum(packet.mode);
            case SET_MATCH -> buffer.writeEnum(packet.match);
            case SET_LIMITS -> {
                buffer.writeVarInt(packet.first);
                buffer.writeVarInt(packet.second);
            }
            case SET_HISTORY_RECORDING, SET_ALLOW_REPLACEMENT -> buffer.writeBoolean(packet.first != 0);
            case USE -> {
                packet.target.write(buffer);
                buffer.writeBoolean(packet.first != 0);
            }
            case RESUME, CANCEL -> writeOptionalUuid(buffer, packet.id);
            case UNBIND_ANCHOR -> buffer.writeUUID(packet.id);
            case MOVE_ANCHOR -> {
                buffer.writeUUID(packet.id);
                buffer.writeByte(packet.first);
            }
            case SET_ANCHOR_PUBLIC -> {
                buffer.writeUUID(packet.id);
                buffer.writeBoolean(packet.first != 0);
            }
            case SET_ANCHOR_PLAYER -> {
                buffer.writeUUID(packet.id);
                buffer.writeUtf(packet.text == null ? "" : packet.text, 64);
                buffer.writeBoolean(packet.first != 0);
            }
            default -> {
            }
        }
    }

    public static BuilderCommandPacket decode(FriendlyByteBuf buffer) {
        Action action = buffer.readEnum(Action.class);
        return switch (action) {
            case REQUEST_SNAPSHOT, OPEN_MENU, UNDO, REDO, CANCEL_BLUEPRINT -> simple(action);
            case SET_MODE -> setMode(buffer.readEnum(BuilderMode.class));
            case SET_MATCH -> setMatch(buffer.readEnum(SurfaceMatchMode.class));
            case SET_LIMITS -> setLimits(buffer.readVarInt(), buffer.readVarInt());
            case SET_HISTORY_RECORDING -> setHistoryRecording(buffer.readBoolean());
            case SET_ALLOW_REPLACEMENT -> setAllowReplacement(buffer.readBoolean());
            case USE -> use(Target.read(buffer), buffer.readBoolean());
            case RESUME -> resume(readOptionalUuid(buffer));
            case CANCEL -> cancel(readOptionalUuid(buffer));
            case UNBIND_ANCHOR -> unbindAnchor(buffer.readUUID());
            case MOVE_ANCHOR -> moveAnchor(buffer.readUUID(), buffer.readByte());
            case SET_ANCHOR_PUBLIC -> setAnchorPublic(buffer.readUUID(), buffer.readBoolean());
            case SET_ANCHOR_PLAYER -> setAnchorPlayer(buffer.readUUID(), buffer.readUtf(64), buffer.readBoolean());
        };
    }

    public static void handle(BuilderCommandPacket packet,
                              Supplier<NetworkEvent.Context> contextSupplier) {
        NetworkEvent.Context context = contextSupplier.get();
        context.enqueueWork(() -> handleOnServer(packet, context.getSender()));
        context.setPacketHandled(true);
    }

    private static void handleOnServer(BuilderCommandPacket packet, @Nullable ServerPlayer player) {
        if (player == null) return;
        ItemStack scepter = player.getMainHandItem();
        // This check is deliberately first and applies even to read-only/menu intents.
        if (!scepter.is(ModItems.REALMWRIGHT_SCEPTER.get())) {
            player.displayClientMessage(Component.translatable("message.mydimension.builder.hold_scepter"), true);
            return;
        }
        RealmwrightData.ensureId(scepter);

        if (packet.action == Action.OPEN_MENU) {
            // Shift + middle mouse is a client input gesture; opening this read-only screen has
            // no server-side modifier permission to enforce.
            BuilderNetworkBridge.openMenu(player);
            return;
        }
        if (packet.action == Action.REQUEST_SNAPSHOT) {
            // The menu polls lightweight metadata; the potentially large cell
            // preview is pushed only when a workflow actually changes.
            ModNetwork.sendBuilderSnapshot(player);
            return;
        }

        if (packet.action == Action.USE) {
            if (useTarget(player, scepter, packet.target, packet.first != 0)) {
                BuilderNetworkBridge.sync(player);
            }
            return;
        }

        if (packet.action == Action.RESUME) {
            // A blueprint resume moves the durable waiting task into the tick-driven task manager.
            // Publishing here would therefore observe the intentional hand-off gap and send an empty
            // missing-material preview. The task manager publishes once it reaches a terminal or
            // paused state; ordinary surface resumes remain synchronous and publish below.
            resume(player, scepter, packet.id);
            return;
        }

        switch (packet.action) {
            case SET_MODE -> RealmwrightData.setMode(scepter, packet.mode);
            case SET_MATCH -> RealmwrightData.setMatchMode(scepter, packet.match);
            case SET_LIMITS -> {
                RealmwrightData.setBuildLimit(scepter, packet.first,
                        BuilderRuntime.settings().maxBuildLimit());
                RealmwrightData.setDemolishLimit(scepter, packet.second,
                        BuilderRuntime.settings().maxDemolishLimit());
            }
            case SET_HISTORY_RECORDING -> RealmwrightData.setRecordsHistory(scepter, packet.first != 0);
            case SET_ALLOW_REPLACEMENT -> RealmwrightData.setAllowsReplacement(scepter, packet.first != 0);
            case USE -> { }
            case RESUME -> { }
            case CANCEL -> cancel(player, scepter, packet.id);
            case CANCEL_BLUEPRINT -> cancelBlueprint(player, scepter);
            case UNDO -> {
                BuilderHistoryService.undo(player, scepter);
            }
            case REDO -> {
                BuilderHistoryService.redo(player, scepter);
            }
            case UNBIND_ANCHOR -> AnchorBindings.unbind(scepter, packet.id);
            case MOVE_ANCHOR -> moveAnchor(scepter, packet.id, packet.first);
            case SET_ANCHOR_PUBLIC -> updateAnchorAcl(player, packet.id, null, packet.first != 0);
            case SET_ANCHOR_PLAYER -> updateAnchorAcl(player, packet.id, packet.text, packet.first != 0);
            default -> {
            }
        }
        BuilderNetworkBridge.sync(player);
    }

    private static boolean useTarget(ServerPlayer player, ItemStack scepter, @Nullable Target requested,
                                     boolean interactionOverride) {
        if (!BuilderRuntime.settings().enabled()) {
            player.displayClientMessage(Component.translatable("message.mydimension.builder.disabled"), true);
            return false;
        }
        BlockHitResult hit = authoritativeHit(player, requested);
        if (hit == null) {
            player.displayClientMessage(Component.translatable("message.mydimension.builder.out_of_reach"), true);
            return false;
        }

        BlockEntity blockEntity = player.serverLevel().getBlockEntity(hit.getBlockPos());
        if (!interactionOverride && blockEntity instanceof ResonantAnchorTarget anchor) {
            if (!anchor.mayUse(player)) {
                player.displayClientMessage(Component.translatable(
                        "message.mydimension.builder.anchor_forbidden"), true);
                return false;
            }
            AnchorBindings.BindResult result = AnchorBindings.bind(scepter, anchor.anchorId(),
                    BuilderConfig.MAX_BOUND_ANCHORS.get());
            String key = switch (result) {
                case BOUND -> "message.mydimension.builder.anchor_bound";
                case ALREADY_BOUND -> "message.mydimension.builder.anchor_already_bound";
                case LIMIT_REACHED -> "message.mydimension.builder.anchor_limit_reached";
            };
            player.displayClientMessage(Component.translatable(key), true);
            return result == AnchorBindings.BindResult.BOUND;
        }

        if (!BuilderInteractionPolicy.permitsScepter(interactionOverride,
                player.serverLevel().getBlockState(hit.getBlockPos()))) {
            // Never trust a client to have applied the interaction-priority rule. An unshifted
            // forged/stale USE packet aimed at a container, machine or button cannot modify it.
            return false;
        }

        BuilderOperationManager.Result result = BuilderOperationManager.executeValidatedSurface(
                player, scepter, hit);
        if (!result.accepted() && result.rejectionKey() != null) {
            player.displayClientMessage(Component.translatable(result.rejectionKey()), true);
        }
        return result.shouldSynchronize();
    }

    @Nullable
    private static BlockHitResult authoritativeHit(ServerPlayer player, @Nullable Target requested) {
        if (requested == null) return null;
        return BuilderReachValidator.validatedHit(player, requested.pos, requested.face, requested.inside);
    }

    private static void resume(ServerPlayer player, ItemStack scepter, @Nullable UUID requestedJob) {
        PendingBuildData.Task pending = matchingPending(player, scepter, requestedJob);
        if (pending == null) {
            player.displayClientMessage(Component.translatable(
                    "message.mydimension.builder.no_pending_task"), true);
            // Clear a genuinely stale client-side yellow preview. The deferred blueprint branch
            // below is used only after a matching task has successfully entered the active manager.
            BuilderNetworkBridge.sync(player);
            return;
        }
        BuilderOperationManager.Result result = BuilderOperationManager.resumePending(player, scepter);
        if (!result.accepted() && result.rejectionKey() != null) {
            player.displayClientMessage(Component.translatable(result.rejectionKey()), true);
        }
        if (!defersResumeSynchronization(pending.type(), result.accepted())) {
            BuilderNetworkBridge.sync(player);
        }
    }

    /** Blueprint execution finishes asynchronously; every other resume has its final state now. */
    static boolean defersResumeSynchronization(BuilderTransaction.Type type, boolean accepted) {
        return accepted
                && type == BuilderTransaction.Type.BLUEPRINT;
    }

    private static void cancel(ServerPlayer player, ItemStack scepter, @Nullable UUID requestedJob) {
        com.xfestudio.mydimension.builder.blueprint.BlueprintTaskManager manager =
                com.xfestudio.mydimension.builder.blueprint.BlueprintTaskManager.get(player.getServer());
        if (requestedJob == null) {
            BlueprintServerService.get(player.getServer()).cancelQueuedPlacement(player);
            manager.cancel(player, scepter);
            PendingBuildData.Task pending = PendingBuildData.get(player.getServer()).get(player.getUUID());
            if (pending != null && pending.scepterId().equals(RealmwrightData.id(scepter))) {
                BuilderOperationManager.cancelPending(player, scepter);
            }
            return;
        }
        com.xfestudio.mydimension.builder.blueprint.BlueprintTaskManager.Status running = manager.status(player);
        if (requestedJob != null && requestedJob.equals(running.transactionId())) {
            manager.cancel(player, scepter);
            return;
        }
        if (matchingPending(player, scepter, requestedJob) != null) {
            BuilderOperationManager.cancelPending(player, scepter);
        }
    }

    private static void cancelBlueprint(ServerPlayer player, ItemStack scepter) {
        BlueprintServerService.get(player.getServer()).cancelQueuedPlacement(player);
        com.xfestudio.mydimension.builder.blueprint.BlueprintTaskManager.get(player.getServer())
                .cancel(player, scepter);
    }

    @Nullable
    private static PendingBuildData.Task matchingPending(ServerPlayer player, ItemStack scepter,
                                                          @Nullable UUID requestedJob) {
        if (requestedJob == null) return null;
        PendingBuildData.Task pending = PendingBuildData.get(player.getServer()).get(player.getUUID());
        if (pending == null || !pending.scepterId().equals(RealmwrightData.id(scepter))) return null;
        return pending.transactionId().equals(requestedJob) ? pending : null;
    }

    private static void moveAnchor(ItemStack scepter, @Nullable UUID anchorId, int delta) {
        if (anchorId == null || delta == 0) return;
        List<UUID> anchors = AnchorBindings.read(scepter);
        int current = anchors.indexOf(anchorId);
        if (current >= 0) AnchorBindings.move(scepter, anchorId, current + Integer.signum(delta));
    }

    private static void updateAnchorAcl(ServerPlayer actor, @Nullable UUID anchorId,
                                        @Nullable String playerName, boolean value) {
        if (!BuilderRuntime.settings().enabled() || anchorId == null) return;
        var location = com.xfestudio.mydimension.builder.anchor.AnchorIndexSavedData.get(actor.getServer())
                .find(anchorId).orElse(null);
        if (location == null) return;
        var level = actor.getServer().getLevel(location.dimension());
        if (level == null || !level.hasChunkAt(location.position())
                || !(level.getBlockEntity(location.position()) instanceof
                com.xfestudio.mydimension.builder.anchor.ResonantSupplyAnchorBlockEntity anchor)
                || !anchor.anchorId().equals(anchorId) || !anchor.canManage(actor)) return;
        if (playerName == null) {
            anchor.setPublicAccess(actor, value);
            return;
        }
        ServerPlayer target = actor.getServer().getPlayerList().getPlayerByName(playerName.strip());
        if (target == null) {
            actor.displayClientMessage(Component.translatable("message.mydimension.builder.anchor_player_offline"),
                    true);
        } else if (value) {
            anchor.authorize(actor, target.getUUID());
        } else {
            anchor.revoke(actor, target.getUUID());
        }
    }

    private static void writeOptionalUuid(FriendlyByteBuf buffer, @Nullable UUID value) {
        buffer.writeBoolean(value != null);
        if (value != null) buffer.writeUUID(value);
    }

    @Nullable
    private static UUID readOptionalUuid(FriendlyByteBuf buffer) {
        return buffer.readBoolean() ? buffer.readUUID() : null;
    }

    public enum Action {
        REQUEST_SNAPSHOT,
        OPEN_MENU,
        SET_MODE,
        SET_MATCH,
        SET_LIMITS,
        SET_HISTORY_RECORDING,
        USE,
        RESUME,
        CANCEL,
        UNDO,
        REDO,
        UNBIND_ANCHOR,
        MOVE_ANCHOR,
        SET_ANCHOR_PUBLIC,
        SET_ANCHOR_PLAYER,
        CANCEL_BLUEPRINT,
        SET_ALLOW_REPLACEMENT
    }

    public record Target(BlockPos pos, Direction face, boolean inside) {
        public Target {
            pos = pos.immutable();
        }

        private void write(FriendlyByteBuf buffer) {
            buffer.writeBlockPos(pos);
            buffer.writeEnum(face);
            buffer.writeBoolean(inside);
        }

        private static Target read(FriendlyByteBuf buffer) {
            return new Target(buffer.readBlockPos(), buffer.readEnum(Direction.class), buffer.readBoolean());
        }
    }
}
