package com.tiedup.remake.blocks.entity;

import com.tiedup.remake.cells.*;
import com.tiedup.remake.core.TiedUpMod;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.client.model.data.ModelData;
import net.minecraftforge.client.model.data.ModelProperty;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Block entity for the Cell Core block.
 *
 * Stores the cell ID link, spawn/delivery points, disguise block,
 * and which face of the Core points into the cell interior.
 */
public class CellCoreBlockEntity extends BlockEntity {

    /** Shared ModelProperty for disguise — defined here (not in client-only CellCoreBakedModel) to avoid server classloading issues. */
    public static final ModelProperty<BlockState> DISGUISE_PROPERTY =
        new ModelProperty<>();

    @Nullable
    private UUID cellId;

    @Nullable
    private BlockPos spawnPoint;

    @Nullable
    private BlockPos deliveryPoint;

    @Nullable
    private BlockState disguiseState;

    @Nullable
    private Direction interiorFace;

    @Nullable
    private List<BlockPos> pathWaypoints;

    /** Transient: pathWaypoints set by MarkerBlockEntity during V1→V2 conversion */
    @Nullable
    private transient List<BlockPos> pendingPathWaypoints;

    public CellCoreBlockEntity(BlockPos pos, BlockState state) {
        super(ModBlockEntities.CELL_CORE.get(), pos, state);
    }

    // ==================== LIFECYCLE ====================

    @Override
    public void onLoad() {
        super.onLoad();

        if (!(level instanceof ServerLevel serverLevel)) {
            return;
        }

        // If we have a cellId but the cell isn't in CellRegistryV2,
        // this is a structure-placed Core that needs flood-fill registration.
        if (cellId != null) {
            CellRegistryV2 registry = CellRegistryV2.get(serverLevel);

            if (
                registry.getCell(cellId) == null &&
                registry.getCellAtCore(worldPosition) == null
            ) {
                // Structure-placed Core: run flood-fill and create cell
                FloodFillResult result = FloodFillAlgorithm.tryFill(
                    serverLevel,
                    worldPosition
                );

                CellDataV2 newCell;
                if (result.isSuccess()) {
                    newCell = registry.createCell(worldPosition, result, null);
                    if (result.getInteriorFace() != null) {
                        this.interiorFace = result.getInteriorFace();
                    }
                } else {
                    // Flood-fill failed (e.g. chunk not fully loaded) — create minimal cell
                    newCell = new CellDataV2(UUID.randomUUID(), worldPosition);
                    registry.registerExistingCell(newCell);
                    TiedUpMod.LOGGER.warn(
                        "[CellCoreBlockEntity] Flood-fill failed at {}: {}. Created minimal cell.",
                        worldPosition.toShortString(),
                        result.getErrorKey()
                    );
                }

                // Update our cellId to match the new cell
                this.cellId = newCell.getId();

                // Transfer spawn/delivery to cell data
                newCell.setSpawnPoint(this.spawnPoint);
                newCell.setDeliveryPoint(this.deliveryPoint);

                // Transfer pathWaypoints: persistent field (from NBT) or V1 migration pending
                List<BlockPos> waypointsToTransfer = null;
                if (
                    this.pathWaypoints != null && !this.pathWaypoints.isEmpty()
                ) {
                    waypointsToTransfer = this.pathWaypoints;
                } else if (
                    this.pendingPathWaypoints != null &&
                    !this.pendingPathWaypoints.isEmpty()
                ) {
                    waypointsToTransfer = this.pendingPathWaypoints;
                    this.pendingPathWaypoints = null;
                }
                if (waypointsToTransfer != null) {
                    newCell.setPathWaypoints(waypointsToTransfer);
                    TiedUpMod.LOGGER.info(
                        "[CellCoreBlockEntity] Transferred {} pathWaypoints to cell {}",
                        waypointsToTransfer.size(),
                        cellId.toString().substring(0, 8)
                    );
                }

                // Mark as camp-owned and link to nearest camp
                newCell.setOwnerType(CellOwnerType.CAMP);

                CampOwnership ownership = CampOwnership.get(serverLevel);
                CampOwnership.CampData nearestCamp =
                    ownership.findNearestAliveCamp(worldPosition, 40);

                if (nearestCamp != null) {
                    newCell.setOwnerId(nearestCamp.getCampId());
                    TiedUpMod.LOGGER.info(
                        "[CellCoreBlockEntity] Created cell {} linked to camp {} at {}",
                        cellId.toString().substring(0, 8),
                        nearestCamp.getCampId().toString().substring(0, 8),
                        worldPosition.toShortString()
                    );
                } else {
                    // No camp yet: generate deterministic camp ID (same algo as MarkerBlockEntity)
                    UUID structureCampId = generateStructureCampId(
                        worldPosition
                    );
                    newCell.setOwnerId(structureCampId);
                    TiedUpMod.LOGGER.info(
                        "[CellCoreBlockEntity] Created cell {} with structure camp ID {} at {}",
                        cellId.toString().substring(0, 8),
                        structureCampId.toString().substring(0, 8),
                        worldPosition.toShortString()
                    );
                }

                registry.updateCampIndex(newCell, null);
                registry.setDirty();
                setChangedAndSync();
            } else {
                // Existing cell: sync pathWaypoints from CellDataV2 → Core BE
                // (so re-saving structures picks them up in the new offset format)
                CellDataV2 existingCell = registry.getCell(cellId);
                if (existingCell == null) {
                    existingCell = registry.getCellAtCore(worldPosition);
                }
                if (existingCell != null) {
                    if (
                        this.pathWaypoints == null ||
                        this.pathWaypoints.isEmpty()
                    ) {
                        List<BlockPos> cellWaypoints =
                            existingCell.getPathWaypoints();
                        if (!cellWaypoints.isEmpty()) {
                            this.pathWaypoints = new ArrayList<>(cellWaypoints);
                            setChanged(); // persist without network sync
                        }
                    }
                }
            }
        }
    }

    /**
     * Generate a deterministic camp ID based on structure position.
     * Uses the same 128-block grid algorithm as MarkerBlockEntity.
     */
    private static UUID generateStructureCampId(BlockPos pos) {
        int gridX = (pos.getX() / 128) * 128;
        int gridZ = (pos.getZ() / 128) * 128;
        long mostSigBits = ((long) gridX << 32) | (gridZ & 0xFFFFFFFFL);
        long leastSigBits = 0x8000000000000000L | (gridX ^ gridZ);
        return new UUID(mostSigBits, leastSigBits);
    }

    // ==================== GETTERS/SETTERS ====================

    @Nullable
    public UUID getCellId() {
        return cellId;
    }

    public void setCellId(@Nullable UUID cellId) {
        this.cellId = cellId;
        setChangedAndSync();
    }

    @Nullable
    public BlockPos getSpawnPoint() {
        return spawnPoint;
    }

    public void setSpawnPoint(@Nullable BlockPos spawnPoint) {
        this.spawnPoint = spawnPoint;
        setChangedAndSync();
    }

    @Nullable
    public BlockPos getDeliveryPoint() {
        return deliveryPoint;
    }

    public void setDeliveryPoint(@Nullable BlockPos deliveryPoint) {
        this.deliveryPoint = deliveryPoint;
        setChangedAndSync();
    }

    @Nullable
    public BlockState getDisguiseState() {
        return disguiseState;
    }

    public void setDisguiseState(@Nullable BlockState disguiseState) {
        this.disguiseState = disguiseState;
        setChangedAndSync();
        requestModelDataUpdate();
    }

    @Nullable
    public Direction getInteriorFace() {
        return interiorFace;
    }

    public void setInteriorFace(@Nullable Direction interiorFace) {
        this.interiorFace = interiorFace;
        setChangedAndSync();
    }

    @Nullable
    public List<BlockPos> getPathWaypoints() {
        return pathWaypoints;
    }

    public void setPathWaypoints(@Nullable List<BlockPos> pathWaypoints) {
        this.pathWaypoints = pathWaypoints;
        setChangedAndSync();
    }

    public void setPendingPathWaypoints(List<BlockPos> waypoints) {
        this.pendingPathWaypoints = waypoints;
        this.pathWaypoints = waypoints; // also persist
    }

    // ==================== MODEL DATA (Camouflage) ====================

    @Override
    public @NotNull ModelData getModelData() {
        BlockState disguise = resolveDisguise();
        if (disguise != null) {
            return ModelData.builder()
                .with(DISGUISE_PROPERTY, disguise)
                .build();
        }
        return ModelData.EMPTY;
    }

    @Nullable
    public BlockState resolveDisguise() {
        // Explicit disguise takes priority (preserves full BlockState including slab type, etc.)
        if (disguiseState != null) {
            return disguiseState;
        }

        // Auto-detect most common solid neighbor
        if (level != null) {
            return detectMostCommonNeighbor();
        }
        return null;
    }

    @Nullable
    private BlockState detectMostCommonNeighbor() {
        if (level == null) return null;

        // Track full BlockState (preserves slab type, stair facing, etc.)
        Map<BlockState, Integer> counts = new HashMap<>();
        for (Direction dir : Direction.values()) {
            BlockPos neighborPos = worldPosition.relative(dir);
            BlockState neighbor = level.getBlockState(neighborPos);
            if (
                neighbor.getBlock() instanceof
                    com.tiedup.remake.blocks.BlockCellCore
            ) continue;
            if (
                neighbor.isSolidRender(level, neighborPos) ||
                neighbor.getBlock() instanceof
                    net.minecraft.world.level.block.SlabBlock
            ) {
                counts.merge(neighbor, 1, Integer::sum);
            }
        }

        if (counts.isEmpty()) return null;

        BlockState mostCommon = null;
        int maxCount = 0;
        for (Map.Entry<BlockState, Integer> entry : counts.entrySet()) {
            if (entry.getValue() > maxCount) {
                maxCount = entry.getValue();
                mostCommon = entry.getKey();
            }
        }
        return mostCommon;
    }

    // ==================== NBT ====================

    @Override
    protected void saveAdditional(CompoundTag tag) {
        super.saveAdditional(tag);
        if (cellId != null) tag.putUUID("cellId", cellId);
        // Save positions as relative offsets from core position (survives structure placement + rotation)
        if (spawnPoint != null) {
            tag.put(
                "spawnOffset",
                NbtUtils.writeBlockPos(toOffset(spawnPoint, worldPosition))
            );
        }
        if (deliveryPoint != null) {
            tag.put(
                "deliveryOffset",
                NbtUtils.writeBlockPos(toOffset(deliveryPoint, worldPosition))
            );
        }
        if (pathWaypoints != null && !pathWaypoints.isEmpty()) {
            ListTag list = new ListTag();
            for (BlockPos wp : pathWaypoints) {
                list.add(NbtUtils.writeBlockPos(toOffset(wp, worldPosition)));
            }
            tag.put("pathWaypointOffsets", list);
        }
        if (disguiseState != null) tag.put(
            "disguiseState",
            NbtUtils.writeBlockState(disguiseState)
        );
        if (interiorFace != null) tag.putString(
            "interiorFace",
            interiorFace.getSerializedName()
        );
    }

    @Override
    public void load(CompoundTag tag) {
        super.load(tag);
        cellId = tag.contains("cellId") ? tag.getUUID("cellId") : null;

        // Spawn point: new relative offset format, then old absolute fallback
        if (tag.contains("spawnOffset")) {
            spawnPoint = fromOffset(
                NbtUtils.readBlockPos(tag.getCompound("spawnOffset")),
                worldPosition
            );
        } else if (tag.contains("spawnPoint")) {
            spawnPoint = NbtUtils.readBlockPos(tag.getCompound("spawnPoint"));
        } else {
            spawnPoint = null;
        }

        // Delivery point: new relative offset format, then old absolute fallback
        if (tag.contains("deliveryOffset")) {
            deliveryPoint = fromOffset(
                NbtUtils.readBlockPos(tag.getCompound("deliveryOffset")),
                worldPosition
            );
        } else if (tag.contains("deliveryPoint")) {
            deliveryPoint = NbtUtils.readBlockPos(
                tag.getCompound("deliveryPoint")
            );
        } else {
            deliveryPoint = null;
        }

        // Path waypoints (relative offsets)
        if (tag.contains("pathWaypointOffsets")) {
            ListTag list = tag.getList("pathWaypointOffsets", Tag.TAG_COMPOUND);
            pathWaypoints = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                pathWaypoints.add(
                    fromOffset(
                        NbtUtils.readBlockPos(list.getCompound(i)),
                        worldPosition
                    )
                );
            }
        } else {
            pathWaypoints = null;
        }

        // Retrocompat: old saves stored "disguiseBlock" as ResourceLocation string
        if (tag.contains("disguiseState")) {
            disguiseState = NbtUtils.readBlockState(
                BuiltInRegistries.BLOCK.asLookup(),
                tag.getCompound("disguiseState")
            );
        } else if (tag.contains("disguiseBlock")) {
            // V1 compat: convert old ResourceLocation to default BlockState
            ResourceLocation oldId = ResourceLocation.tryParse(
                tag.getString("disguiseBlock")
            );
            if (oldId != null) {
                Block block = BuiltInRegistries.BLOCK.get(oldId);
                disguiseState = (block !=
                    net.minecraft.world.level.block.Blocks.AIR)
                    ? block.defaultBlockState()
                    : null;
            }
        } else {
            disguiseState = null;
        }
        interiorFace = tag.contains("interiorFace")
            ? Direction.byName(tag.getString("interiorFace"))
            : null;
    }

    // ==================== OFFSET HELPERS ====================

    /** Convert absolute position to relative offset from origin. */
    private static BlockPos toOffset(BlockPos absolute, BlockPos origin) {
        return new BlockPos(
            absolute.getX() - origin.getX(),
            absolute.getY() - origin.getY(),
            absolute.getZ() - origin.getZ()
        );
    }

    /** Convert relative offset back to absolute position. */
    private static BlockPos fromOffset(BlockPos offset, BlockPos origin) {
        return origin.offset(offset.getX(), offset.getY(), offset.getZ());
    }

    // ==================== NETWORK SYNC ====================

    private void setChangedAndSync() {
        if (level != null) {
            setChanged();
            level.sendBlockUpdated(
                worldPosition,
                getBlockState(),
                getBlockState(),
                3
            );
        }
    }

    @Override
    public CompoundTag getUpdateTag() {
        CompoundTag tag = super.getUpdateTag();
        saveAdditional(tag);
        return tag;
    }

    @Nullable
    @Override
    public Packet<ClientGamePacketListener> getUpdatePacket() {
        return ClientboundBlockEntityDataPacket.create(this);
    }

    @Override
    public void onDataPacket(
        Connection net,
        ClientboundBlockEntityDataPacket pkt
    ) {
        CompoundTag tag = pkt.getTag();
        if (tag != null) {
            handleUpdateTag(tag);
        }
    }

    @Override
    public void handleUpdateTag(CompoundTag tag) {
        load(tag);
        requestModelDataUpdate();
        // Force chunk section re-render to pick up new model data immediately.
        // Enqueue to main thread since handleUpdateTag may run on network thread.
        if (level != null && level.isClientSide()) {
            net.minecraft.client.Minecraft.getInstance().tell(
                this::markRenderDirty
            );
        }
    }

    /**
     * Marks the chunk section containing this block entity for re-rendering.
     * Must be called on the client main thread only.
     */
    private void markRenderDirty() {
        if (level == null || !level.isClientSide()) return;
        net.minecraft.client.Minecraft mc =
            net.minecraft.client.Minecraft.getInstance();
        if (mc.levelRenderer != null) {
            mc.levelRenderer.blockChanged(
                level,
                worldPosition,
                getBlockState(),
                getBlockState(),
                8
            );
        }
    }
}
