package com.tiedup.remake.worldgen;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.commands.arguments.blocks.BlockStateParser;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.block.state.properties.Property;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;

/**
 * Parses JSON files into {@link RoomThemeDefinition} instances.
 *
 * <p>Uses manual field extraction (not Gson deserialization) for strict
 * validation control. Invalid required fields cause the entire definition
 * to be rejected; optional fields use safe defaults.</p>
 *
 * <p>Expected JSON files in {@code data/<namespace>/tiedup_room_themes/}.</p>
 */
public final class RoomThemeParser {

    private static final Logger LOGGER = LogManager.getLogger("TiedUpWorldgen");
    private static final String TAG = "[RoomThemeParser]";

    private RoomThemeParser() {}

    /**
     * Parse a JSON input stream into a RoomThemeDefinition.
     *
     * @param input  the JSON input stream
     * @param fileId the resource location derived from the file path
     * @return the parsed definition, or null if the file is invalid
     */
    @Nullable
    public static RoomThemeDefinition parse(InputStream input, ResourceLocation fileId) {
        try {
            JsonObject root = JsonParser.parseReader(
                new InputStreamReader(input, StandardCharsets.UTF_8)
            ).getAsJsonObject();

            return parseRoot(root, fileId);
        } catch (Exception e) {
            LOGGER.error("{} Failed to parse JSON {}: {}", TAG, fileId, e.getMessage());
            return null;
        }
    }

    /**
     * Parse the root JSON object into a RoomThemeDefinition.
     */
    @Nullable
    private static RoomThemeDefinition parseRoot(JsonObject root, ResourceLocation fileId) {
        // --- Required: weight (default 10 if missing, clamped [1, 1000]) ---
        int weight = clampInt(getIntOrDefault(root, "weight", 10), 1, 1000);

        // --- Required: wall_palette ---
        BlockPalette wallPalette = parsePaletteField(root, "wall_palette", fileId);
        if (wallPalette == null) {
            LOGGER.error("{} Skipping {}: missing or invalid 'wall_palette'", TAG, fileId);
            return null;
        }

        // --- Required: floor_palette ---
        BlockPalette floorPalette = parsePaletteField(root, "floor_palette", fileId);
        if (floorPalette == null) {
            LOGGER.error("{} Skipping {}: missing or invalid 'floor_palette'", TAG, fileId);
            return null;
        }

        // --- Required: ceiling_palette ---
        BlockPalette ceilingPalette = parsePaletteField(root, "ceiling_palette", fileId);
        if (ceilingPalette == null) {
            LOGGER.error("{} Skipping {}: missing or invalid 'ceiling_palette'", TAG, fileId);
            return null;
        }

        // --- Required: wall_shell (single block string) ---
        BlockState wallShell = parseBlockStateField(root, "wall_shell", fileId);
        if (wallShell == null) {
            LOGGER.error("{} Skipping {}: missing or invalid 'wall_shell'", TAG, fileId);
            return null;
        }

        // --- Required: wall_accent (single block string) ---
        BlockState wallAccent = parseBlockStateField(root, "wall_accent", fileId);
        if (wallAccent == null) {
            LOGGER.error("{} Skipping {}: missing or invalid 'wall_accent'", TAG, fileId);
            return null;
        }

        // --- Required: pillar_palette ---
        BlockPalette pillarPalette = parsePaletteField(root, "pillar_palette", fileId);
        if (pillarPalette == null) {
            LOGGER.error("{} Skipping {}: missing or invalid 'pillar_palette'", TAG, fileId);
            return null;
        }

        // --- Optional: scatter_palette ---
        BlockPalette scatterPalette = null;
        if (root.has("scatter_palette") && root.get("scatter_palette").isJsonObject()) {
            scatterPalette = parsePaletteField(root, "scatter_palette", fileId);
            // If scatter_palette is present but invalid, warn but don't reject
            if (scatterPalette == null) {
                LOGGER.warn(
                    "{} In {}: 'scatter_palette' present but invalid, ignoring", TAG, fileId
                );
            }
        }

        // --- Optional: decorations ---
        DecorationConfig decorations = new DecorationConfig(
            List.of(), List.of(), null, List.of(), false, false
        );
        if (root.has("decorations") && root.get("decorations").isJsonObject()) {
            DecorationConfig parsed = parseDecorations(
                root.getAsJsonObject("decorations"), fileId
            );
            if (parsed != null) {
                decorations = parsed;
            }
        }

        return new RoomThemeDefinition(
            fileId, weight,
            wallPalette, floorPalette, ceilingPalette,
            wallShell, wallAccent,
            pillarPalette, scatterPalette,
            decorations
        );
    }

    // ===== Palette Parsing =====

    /**
     * Parse a palette field from the root object. The field value must be a JSON object
     * mapping condition names to arrays of weighted entries.
     *
     * <p>Example:
     * <pre>{
     *   "default": [{"block": "minecraft:stone_bricks", "weight": 0.8}],
     *   "bottom_row": [{"block": "minecraft:mossy_stone_bricks", "weight": 1.0}]
     * }</pre>
     */
    @Nullable
    private static BlockPalette parsePaletteField(
        JsonObject root, String fieldName, ResourceLocation fileId
    ) {
        if (!root.has(fieldName) || !root.get(fieldName).isJsonObject()) {
            return null;
        }

        JsonObject paletteObj = root.getAsJsonObject(fieldName);
        Map<String, List<BlockPalette.WeightedEntry>> variants = new HashMap<>();

        for (Map.Entry<String, JsonElement> condEntry : paletteObj.entrySet()) {
            String condition = condEntry.getKey();
            if (!condEntry.getValue().isJsonArray()) {
                LOGGER.warn(
                    "{} In {} palette '{}': condition '{}' is not an array, skipping",
                    TAG, fileId, fieldName, condition
                );
                continue;
            }

            List<BlockPalette.WeightedEntry> entries = parseWeightedEntries(
                condEntry.getValue().getAsJsonArray(), fileId, fieldName, condition
            );
            if (!entries.isEmpty()) {
                variants.put(condition, List.copyOf(entries));
            }
        }

        if (variants.isEmpty() || !variants.containsKey("default")) {
            LOGGER.warn(
                "{} In {} palette '{}': no valid 'default' condition found",
                TAG, fileId, fieldName
            );
            return null;
        }

        return new BlockPalette(variants);
    }

    /**
     * Parse an array of weighted block entries for a single palette condition.
     *
     * <p>Each entry is a JSON object with:
     * <ul>
     *   <li>{@code "block"} — block state string (e.g. "minecraft:candle[lit=true]")</li>
     *   <li>{@code "weight"} — float weight (default 1.0)</li>
     *   <li>{@code "random_property"} — optional object with {@code "name"}, {@code "min"}, {@code "max"}
     *       to expand a single entry into multiple entries with varying integer property values</li>
     * </ul>
     */
    private static List<BlockPalette.WeightedEntry> parseWeightedEntries(
        JsonArray array, ResourceLocation fileId, String paletteName, String condition
    ) {
        List<BlockPalette.WeightedEntry> entries = new ArrayList<>();

        for (int i = 0; i < array.size(); i++) {
            if (!array.get(i).isJsonObject()) {
                LOGGER.warn(
                    "{} In {} palette '{}' condition '{}': entry [{}] is not an object, skipping",
                    TAG, fileId, paletteName, condition, i
                );
                continue;
            }

            JsonObject entryObj = array.get(i).getAsJsonObject();

            String blockStr = getStringOrNull(entryObj, "block");
            if (blockStr == null || blockStr.isEmpty()) {
                LOGGER.warn(
                    "{} In {} palette '{}' condition '{}': entry [{}] missing 'block', skipping",
                    TAG, fileId, paletteName, condition, i
                );
                continue;
            }

            BlockState baseState = parseBlockState(blockStr, fileId);
            if (baseState == null) {
                continue; // parseBlockState already logged warning
            }

            float weight = getFloatOrDefault(entryObj, "weight", 1.0f);
            if (weight <= 0) {
                LOGGER.warn(
                    "{} In {} palette '{}' condition '{}': entry [{}] has non-positive weight, skipping",
                    TAG, fileId, paletteName, condition, i
                );
                continue;
            }

            // Handle random_property expansion
            if (entryObj.has("random_property") && entryObj.get("random_property").isJsonObject()) {
                List<BlockPalette.WeightedEntry> expanded = expandRandomProperty(
                    baseState, weight, entryObj.getAsJsonObject("random_property"),
                    fileId, paletteName, condition, i
                );
                entries.addAll(expanded);
            } else {
                entries.add(new BlockPalette.WeightedEntry(baseState, weight));
            }
        }

        return entries;
    }

    /**
     * Expand a single weighted entry into multiple entries by varying an integer
     * block state property across a range.
     *
     * <p>For example, {@code candle[candles=1]} with {@code random_property: {name: "candles", min: 1, max: 3}}
     * and weight 0.30 produces three entries: candles=1 (0.10), candles=2 (0.10), candles=3 (0.10).</p>
     */
    @SuppressWarnings("unchecked")
    private static List<BlockPalette.WeightedEntry> expandRandomProperty(
        BlockState baseState, float totalWeight, JsonObject propObj,
        ResourceLocation fileId, String paletteName, String condition, int entryIndex
    ) {
        String propName = getStringOrNull(propObj, "name");
        if (propName == null) {
            LOGGER.warn(
                "{} In {} palette '{}' condition '{}': entry [{}] random_property missing 'name', using base state",
                TAG, fileId, paletteName, condition, entryIndex
            );
            return List.of(new BlockPalette.WeightedEntry(baseState, totalWeight));
        }

        int min = getIntOrDefault(propObj, "min", 1);
        int max = getIntOrDefault(propObj, "max", min);
        if (max < min) {
            LOGGER.warn(
                "{} In {} palette '{}' condition '{}': entry [{}] random_property max < min, using base state",
                TAG, fileId, paletteName, condition, entryIndex
            );
            return List.of(new BlockPalette.WeightedEntry(baseState, totalWeight));
        }

        Property<?> property = baseState.getBlock().getStateDefinition().getProperty(propName);
        if (!(property instanceof IntegerProperty intProp)) {
            LOGGER.warn(
                "{} In {} palette '{}' condition '{}': entry [{}] property '{}' is not an IntegerProperty, using base state",
                TAG, fileId, paletteName, condition, entryIndex, propName
            );
            return List.of(new BlockPalette.WeightedEntry(baseState, totalWeight));
        }

        int count = max - min + 1;
        float perWeight = totalWeight / count;
        List<BlockPalette.WeightedEntry> expanded = new ArrayList<>(count);

        for (int val = min; val <= max; val++) {
            if (intProp.getPossibleValues().contains(val)) {
                expanded.add(new BlockPalette.WeightedEntry(
                    baseState.setValue(intProp, val), perWeight
                ));
            } else {
                LOGGER.warn(
                    "{} In {} palette '{}' condition '{}': entry [{}] property '{}' does not accept value {}, skipping",
                    TAG, fileId, paletteName, condition, entryIndex, propName, val
                );
            }
        }

        if (expanded.isEmpty()) {
            return List.of(new BlockPalette.WeightedEntry(baseState, totalWeight));
        }

        return expanded;
    }

    // ===== Block State Parsing =====

    /**
     * Parse a block state string like "minecraft:stone_bricks" or "minecraft:candle[lit=true]"
     * using Minecraft's built-in BlockStateParser.
     *
     * @return the parsed BlockState, or null on failure (logged as warning)
     */
    @Nullable
    private static BlockState parseBlockState(String blockString, ResourceLocation fileId) {
        try {
            return BlockStateParser.parseForBlock(
                BuiltInRegistries.BLOCK.asLookup(),
                blockString,
                false  // don't allow NBT
            ).blockState();
        } catch (Exception e) {
            LOGGER.warn(
                "{} In {}: invalid block state '{}': {}",
                TAG, fileId, blockString, e.getMessage()
            );
            return null;
        }
    }

    /**
     * Parse a single block state from a string field in a JSON object.
     *
     * @return the parsed BlockState, or null if field is missing or invalid
     */
    @Nullable
    private static BlockState parseBlockStateField(
        JsonObject obj, String fieldName, ResourceLocation fileId
    ) {
        String blockStr = getStringOrNull(obj, fieldName);
        if (blockStr == null || blockStr.isEmpty()) {
            return null;
        }
        return parseBlockState(blockStr, fileId);
    }

    // ===== Decoration Parsing =====

    /**
     * Parse the "decorations" JSON object into a DecorationConfig.
     */
    @Nullable
    private static DecorationConfig parseDecorations(JsonObject obj, ResourceLocation fileId) {
        try {
            List<DecorationConfig.PositionedBlock> cornerDecos = parsePositionedBlockList(
                obj, "corner_decorations", fileId
            );
            List<DecorationConfig.PositionedBlock> wallMidpoints = parsePositionedBlockList(
                obj, "wall_midpoint_blocks", fileId
            );

            DecorationConfig.PositionedBlock firstCornerSpecial = null;
            if (obj.has("first_corner_special") && obj.get("first_corner_special").isJsonObject()) {
                firstCornerSpecial = parsePositionedBlock(
                    obj.getAsJsonObject("first_corner_special"), fileId
                );
            }

            List<DecorationConfig.PositionedBlock> furnitureCluster = parsePositionedBlockList(
                obj, "furniture_cluster", fileId
            );

            boolean useTorchLighting = getBooleanOrDefault(obj, "use_torch_lighting", false);
            boolean hasCeilingChain = getBooleanOrDefault(obj, "has_ceiling_chain", false);

            return new DecorationConfig(
                cornerDecos, wallMidpoints, firstCornerSpecial,
                furnitureCluster, useTorchLighting, hasCeilingChain
            );
        } catch (Exception e) {
            LOGGER.warn(
                "{} In {}: failed to parse decorations: {}", TAG, fileId, e.getMessage()
            );
            return null;
        }
    }

    /**
     * Parse an array of positioned blocks from a field.
     */
    private static List<DecorationConfig.PositionedBlock> parsePositionedBlockList(
        JsonObject parent, String fieldName, ResourceLocation fileId
    ) {
        if (!parent.has(fieldName) || !parent.get(fieldName).isJsonArray()) {
            return List.of();
        }

        JsonArray array = parent.getAsJsonArray(fieldName);
        List<DecorationConfig.PositionedBlock> result = new ArrayList<>();

        for (int i = 0; i < array.size(); i++) {
            if (!array.get(i).isJsonObject()) continue;

            DecorationConfig.PositionedBlock pb = parsePositionedBlock(
                array.get(i).getAsJsonObject(), fileId
            );
            if (pb != null) {
                result.add(pb);
            }
        }

        return List.copyOf(result);
    }

    /**
     * Parse a single positioned block: { "block": "...", "x_offset": 0, "y_offset": 0, "z_offset": 0 }
     */
    @Nullable
    private static DecorationConfig.PositionedBlock parsePositionedBlock(
        JsonObject obj, ResourceLocation fileId
    ) {
        String blockStr = getStringOrNull(obj, "block");
        if (blockStr == null || blockStr.isEmpty()) return null;

        BlockState state = parseBlockState(blockStr, fileId);
        if (state == null) return null;

        int xOffset = getIntOrDefault(obj, "x_offset", 0);
        int yOffset = getIntOrDefault(obj, "y_offset", 0);
        int zOffset = getIntOrDefault(obj, "z_offset", 0);
        return new DecorationConfig.PositionedBlock(state, xOffset, yOffset, zOffset);
    }

    // ===== Primitive Helpers =====

    @Nullable
    private static String getStringOrNull(JsonObject obj, String key) {
        if (!obj.has(key) || obj.get(key).isJsonNull()) return null;
        try {
            return obj.get(key).getAsString();
        } catch (Exception e) {
            return null;
        }
    }

    private static int getIntOrDefault(JsonObject obj, String key, int defaultValue) {
        if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
        try {
            return obj.get(key).getAsInt();
        } catch (Exception e) {
            return defaultValue;
        }
    }

    private static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) {
        if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
        try {
            return obj.get(key).getAsFloat();
        } catch (Exception e) {
            return defaultValue;
        }
    }

    private static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) {
        if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
        try {
            return obj.get(key).getAsBoolean();
        } catch (Exception e) {
            return defaultValue;
        }
    }

    private static int clampInt(int value, int min, int max) {
        return Math.max(min, Math.min(max, value));
    }
}
