package com.firefrostgaming.bitchbot;

import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.arguments.blocks.BlockStateParser;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Sponge Schematic Format v2 loader (the format that WorldEdit and FastAsyncWorldEdit
 * write by default — file extension .schem). The format is a gzip-compressed NBT
 * compound with the following keys we care about:
 *
 *   Width, Height, Length        (short)
 *   Offset                       (int[3])  — paste origin (corner)
 *   Palette                      (compound) — key=blockstate string, value=int id
 *   PaletteMax                   (int)
 *   BlockData                    (byte[])  — varint stream of palette indices
 *
 * Block index → coordinate mapping is x + (z * Width) + (y * Width * Length).
 *
 * For paste mode "center" we shift by -Width/2 and -Length/2 so the building
 * lands centered on the requested spawn_coords. For "corner" we paste verbatim.
 *
 * NOTE: We do NOT honor BlockEntities or Entities tags from the schematic.
 * Spawn buildings from Holly are intentionally vanilla blocks only.
 */
public class SchematicLoader {

    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    /** Download to memory and verify SHA-256. Returns bytes on success, null on failure. */
    public static byte[] downloadAndVerify(String url, String expectedSha256Hex) {
        try {
            HttpRequest req = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(Duration.ofSeconds(60))
                    .GET()
                    .build();
            HttpResponse<byte[]> resp = HTTP.send(req, HttpResponse.BodyHandlers.ofByteArray());
            if (resp.statusCode() != 200) {
                BitchBot.LOGGER.error("[BitchBot] Schematic HTTP {} from {}", resp.statusCode(), url);
                return null;
            }
            byte[] data = resp.body();

            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(data);
            StringBuilder hex = new StringBuilder(digest.length * 2);
            for (byte b : digest) hex.append(String.format("%02x", b));
            String actual = hex.toString();

            if (expectedSha256Hex == null || !expectedSha256Hex.equalsIgnoreCase(actual)) {
                BitchBot.LOGGER.error("[BitchBot] Schematic hash mismatch — expected {} actual {}", expectedSha256Hex, actual);
                return null;
            }
            BitchBot.LOGGER.info("[BitchBot] Schematic downloaded ({} bytes), SHA-256 verified.", data.length);
            return data;
        } catch (Exception e) {
            BitchBot.LOGGER.error("[BitchBot] Schematic download failed: {}", e.getMessage());
            return null;
        }
    }

    /**
     * Parse the schematic and place blocks into the given level.
     * Returns the number of blocks placed.
     */
    public static int pasteAt(ServerLevel level, byte[] schemBytes, BlockPos target, boolean centerOrigin) throws Exception {
        CompoundTag root;
        try (DataInputStream in = new DataInputStream(new java.util.zip.GZIPInputStream(new ByteArrayInputStream(schemBytes)))) {
            root = NbtIo.read(in, NbtAccounter.unlimitedHeap());
        }

        // Some schematics wrap the data inside a "Schematic" sub-compound (v3). Normalize.
        if (root.contains("Schematic")) root = root.getCompound("Schematic");

        short width  = root.getShort("Width");
        short height = root.getShort("Height");
        short length = root.getShort("Length");

        CompoundTag paletteTag = root.getCompound("Palette");
        Map<Integer, BlockState> palette = new HashMap<>();
        HolderLookup.Provider lookup = level.registryAccess();
        HolderLookup.RegistryLookup<Block> blockLookup = lookup.lookupOrThrow(Registries.BLOCK);

        for (String key : paletteTag.getAllKeys()) {
            int id = paletteTag.getInt(key);
            BlockState state;
            try {
                BlockStateParser.BlockResult result = BlockStateParser.parseForBlock(blockLookup, key, false);
                state = result.blockState();
            } catch (Exception e) {
                BitchBot.LOGGER.warn("[BitchBot] Unknown blockstate '{}' in palette → AIR", key);
                state = Blocks.AIR.defaultBlockState();
            }
            palette.put(id, state);
        }

        byte[] blockData = root.getByteArray("BlockData");
        int[] indices = decodeVarintArray(blockData, width * height * length);

        int xShift = centerOrigin ? -(width  / 2) : 0;
        int zShift = centerOrigin ? -(length / 2) : 0;
        int yShift = 0; // y stays anchored to spawn_coords.y

        int placed = 0;
        BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
        for (int y = 0; y < height; y++) {
            for (int z = 0; z < length; z++) {
                for (int x = 0; x < width; x++) {
                    int idx = x + (z * width) + (y * width * length);
                    BlockState state = palette.get(indices[idx]);
                    if (state == null || state.isAir()) continue;
                    cursor.set(target.getX() + x + xShift, target.getY() + y + yShift, target.getZ() + z + zShift);
                    level.setBlock(cursor, state, 2); // flag 2 = send to clients, no neighbor updates
                    placed++;
                }
            }
        }
        return placed;
    }

    /** LEB128-style varint decode (Sponge schematic v2 format). */
    private static int[] decodeVarintArray(byte[] data, int expectedCount) {
        int[] out = new int[expectedCount];
        int value, shift, b, i = 0, idx = 0;
        while (i < data.length && idx < expectedCount) {
            value = 0;
            shift = 0;
            while (true) {
                if (i >= data.length) break;
                b = data[i++] & 0xFF;
                value |= (b & 0x7F) << shift;
                if ((b & 0x80) == 0) break;
                shift += 7;
                if (shift > 35) throw new RuntimeException("varint too long");
            }
            out[idx++] = value;
        }
        return out;
    }
}
