package com.xfestudio.xfeservermanager.core.trigger;

import java.util.List;
import java.util.Map;
import java.util.Set;

/** Supported built-in event, comparison and action vocabulary. Custom events remain extensible. */
public final class TriggerCatalog {
    public static final List<String> EVENTS = List.of(
            "schedule.daily", "schedule.interval", "server.started", "server.stopping",
            "player.join", "player.leave", "player.chat", "player.command", "player.command_trigger",
            "player.death",
            "player.respawn", "player.dimension_change", "player.advancement", "player.item_pickup",
            "player.item_drop", "player.item_use", "player.item_use_finish", "player.item_craft",
            "player.attack", "player.hurt", "player.heal", "player.interact", "player.entity_interact",
            "player.sleep", "player.wake", "block.break", "block.place", "block.change", "block.grow",
            "block.tool_modify", "entity.spawn", "entity.remove", "entity.death", "world.explosion",
            "world.weather_change", "world.load", "world.unload", "chunk.load", "chunk.unload",
            "variable.changed", "menu.open", "menu.close", "menu.control",
            "economy.balance_changed", "economy.deposit", "economy.withdraw", "economy.set",
            "economy.transfer", "protection.item_overflow", "protection.mob_overflow",
            "protection.entity_overflow", "protection.mod_entity_overflow", "protection.spawn_burst",
            "protection.command_block_rate", "protection.slow_tick",
            "protection.loaded_chunk_overflow", "protection.memory_pressure",
            "region.enter", "region.leave", "region.stay", "custom");
    public static final List<String> OPERATORS = List.of(
            "eq", "neq", "contains", "not_contains",
            "starts_with", "not_starts_with", "ends_with", "not_ends_with",
            "matches", "not_matches", "gt", "gte", "lt", "lte", "between", "not_between",
            "in", "not_in", "exists", "not_exists", "empty", "not_empty", "true", "false");
    public static final List<String> ACTIONS = List.of(
            "send_player", "broadcast", "title", "actionbar", "sound", "server_command",
            "player_command", "kick", "teleport", "give_item", "clear_inventory",
            "set_gamemode", "add_effect", "remove_effects", "heal", "feed", "set_time",
            "set_weather", "whitelist_add", "whitelist_remove", "ban", "pardon", "log",
            "variable", "wait", "run_trigger", "open_menu", "close_menu",
            "economy_deposit", "economy_withdraw", "economy_set_balance", "economy_transfer",
            "economy_deposit_player", "economy_withdraw_player", "economy_set_player_balance",
            "economy_transfer_players", "set_health", "set_food", "set_experience",
            "set_spawnpoint", "scoreboard_set", "scoreboard_add", "team_join", "team_leave",
            "advancement_grant", "advancement_revoke", "spawn_entity", "damage_entity",
            "teleport_entity", "remove_entity", "set_entity_attribute", "tag_entity",
            "set_block", "fill_blocks", "explosion", "lightning", "set_difficulty",
            "set_gamerule", "world_border");
    public static final Map<String, List<String>> ACTION_PARAMETERS = Map.ofEntries(
            Map.entry("send_player", List.of("message")),
            Map.entry("broadcast", List.of("message")),
            Map.entry("title", List.of("title", "subtitle", "numberPrecision", "fadeIn", "stay", "fadeOut")),
            Map.entry("actionbar", List.of("message")),
            Map.entry("sound", List.of("sound", "volume", "pitch")),
            Map.entry("server_command", List.of("command", "showFeedback")),
            Map.entry("player_command", List.of("command", "showFeedback")),
            Map.entry("kick", List.of("message")),
            Map.entry("teleport", List.of("destination")),
            Map.entry("give_item", List.of("item", "count")),
            Map.entry("clear_inventory", List.of("item", "maxCount")),
            Map.entry("set_gamemode", List.of("gamemode")),
            Map.entry("add_effect", List.of("effect", "duration", "amplifier")),
            Map.entry("remove_effects", List.of()),
            Map.entry("heal", List.of()),
            Map.entry("feed", List.of()),
            Map.entry("set_time", List.of("time")),
            Map.entry("set_weather", List.of("weather", "duration")),
            Map.entry("whitelist_add", List.of()),
            Map.entry("whitelist_remove", List.of()),
            Map.entry("ban", List.of("reason")),
            Map.entry("pardon", List.of()),
            Map.entry("log", List.of("message", "level")),
            Map.entry("variable", List.of("name", "operation", "value", "extra")),
            Map.entry("wait", List.of("mode", "value", "timeout", "pollTicks", "field", "operator", "expected")),
            Map.entry("run_trigger", List.of("triggerId", "waitForCompletion")),
            Map.entry("open_menu", List.of("menuId")),
            Map.entry("close_menu", List.of()),
            Map.entry("economy_deposit", List.of("currency", "amount", "reason")),
            Map.entry("economy_withdraw", List.of("currency", "amount", "reason")),
            Map.entry("economy_set_balance", List.of("currency", "amount", "reason")),
            Map.entry("economy_transfer", List.of("currency", "amount", "target", "reason")),
            Map.entry("economy_deposit_player", List.of("player", "currency", "amount", "reason")),
            Map.entry("economy_withdraw_player", List.of("player", "currency", "amount", "reason")),
            Map.entry("economy_set_player_balance", List.of("player", "currency", "amount", "reason")),
            Map.entry("economy_transfer_players", List.of("source", "target", "currency", "amount", "reason")),
            Map.entry("set_health", List.of("value")),
            Map.entry("set_food", List.of("value")),
            Map.entry("set_experience", List.of("amount", "mode")),
            Map.entry("set_spawnpoint", List.of("position")),
            Map.entry("scoreboard_set", List.of("objective", "value")),
            Map.entry("scoreboard_add", List.of("objective", "value")),
            Map.entry("team_join", List.of("team")),
            Map.entry("team_leave", List.of()),
            Map.entry("advancement_grant", List.of("advancement")),
            Map.entry("advancement_revoke", List.of("advancement")),
            Map.entry("spawn_entity", List.of("entityType", "position", "nbt")),
            Map.entry("damage_entity", List.of("selector", "amount", "damageType")),
            Map.entry("teleport_entity", List.of("selector", "destination")),
            Map.entry("remove_entity", List.of("selector")),
            Map.entry("set_entity_attribute", List.of("selector", "attribute", "value")),
            Map.entry("tag_entity", List.of("selector", "operation", "tag")),
            Map.entry("set_block", List.of("dimension", "position", "block", "mode")),
            Map.entry("fill_blocks", List.of("dimension", "from", "to", "block", "mode")),
            Map.entry("explosion", List.of("dimension", "position", "power", "fire")),
            Map.entry("lightning", List.of("dimension", "position")),
            Map.entry("set_difficulty", List.of("difficulty")),
            Map.entry("set_gamerule", List.of("rule", "value")),
            Map.entry("world_border", List.of("operation", "value", "seconds")));
    private static final Set<String> EVENT_SET = Set.copyOf(EVENTS);
    private static final Set<String> OPERATOR_SET = Set.copyOf(OPERATORS);
    private static final Set<String> ACTION_SET = Set.copyOf(ACTIONS);
    private static final Set<String> PLAYER_CONTEXT_ACTIONS = Set.of(
            "send_player", "title", "actionbar", "sound", "player_command", "kick",
            "teleport", "give_item", "clear_inventory", "set_gamemode", "add_effect",
            "remove_effects", "heal", "feed", "whitelist_add", "whitelist_remove", "ban", "pardon",
            "open_menu", "close_menu", "economy_deposit", "economy_withdraw",
            "economy_set_balance", "economy_transfer", "set_health", "set_food",
            "set_experience", "set_spawnpoint", "scoreboard_set", "scoreboard_add",
            "team_join", "team_leave", "advancement_grant", "advancement_revoke");
    private static final Set<String> ONLINE_PLAYER_ACTIONS = Set.of(
            "send_player", "title", "actionbar", "sound", "player_command", "kick", "teleport",
            "give_item", "clear_inventory", "set_gamemode", "add_effect", "remove_effects", "heal", "feed",
            "open_menu", "close_menu", "set_health", "set_food", "set_experience",
            "set_spawnpoint", "scoreboard_set", "scoreboard_add", "team_join", "team_leave",
            "advancement_grant", "advancement_revoke");
    private static final Set<String> RESOURCE_ACTIONS = Set.of("sound", "give_item", "add_effect");

    private TriggerCatalog() { }

    public static void validate(TriggerDefinition.Program program) {
        String event = program.event().type();
        boolean builtIn = EVENT_SET.contains(event) && !event.equals("custom");
        if (!builtIn && !event.matches("custom\\.[a-z0-9_.-]+")) {
            throw new IllegalArgumentException("unsupported trigger event: " + event);
        }
        validateEventConfiguration(program.event());
        program.conditions().forEach(condition -> {
            if (!OPERATOR_SET.contains(condition.operator())) {
                throw new IllegalArgumentException("unsupported condition operator: " + condition.operator());
            }
            if (Set.of("matches", "not_matches").contains(condition.operator())) {
                TriggerEvaluator.validateRegex(condition.value());
            }
            if (Set.of("gt", "gte", "lt", "lte").contains(condition.operator())) {
                finiteNumber(condition.value(), "numeric comparison value");
            }
            if (Set.of("between", "not_between").contains(condition.operator())) {
                TriggerEvaluator.validateNumberRange(condition.value());
            }
        });
        if (program.actions().isEmpty()) throw new IllegalArgumentException("a trigger needs at least one action");
        Set<String> declaredVariables = program.event().variables().stream()
                .map(variable -> variable.visibility().equals("global")
                        ? "global." + variable.name() : variable.name())
                .collect(java.util.stream.Collectors.toSet());
        program.actions().forEach(action -> {
            if (!ACTION_SET.contains(action.type())) {
                throw new IllegalArgumentException("unsupported trigger action: " + action.type());
            }
            Set<String> acceptedParameters = Set.copyOf(ACTION_PARAMETERS.get(action.type()));
            for (String parameter : action.parameters().keySet()) {
                if (!acceptedParameters.contains(parameter)) {
                    throw new IllegalArgumentException(action.type() + " does not accept parameter " + parameter);
                }
            }
            String primary = switch (action.type()) {
                case "server_command", "player_command" -> "command";
                case "sound" -> "sound";
                case "title" -> "title";
                case "teleport" -> "destination";
                case "give_item" -> "item";
                case "set_gamemode" -> "gamemode";
                case "add_effect" -> "effect";
                case "set_time" -> "time";
                case "set_weather" -> "weather";
                case "variable" -> "name";
                case "wait" -> "mode";
                case "run_trigger" -> "triggerId";
                case "open_menu" -> "menuId";
                case "economy_deposit", "economy_withdraw", "economy_set_balance", "economy_transfer",
                        "economy_deposit_player", "economy_withdraw_player", "economy_set_player_balance",
                        "economy_transfer_players" -> "currency";
                case "set_health", "set_food" -> "value";
                case "set_experience" -> "amount";
                case "set_spawnpoint" -> "position";
                case "scoreboard_set", "scoreboard_add" -> "objective";
                case "team_join" -> "team";
                case "team_leave" -> null;
                case "advancement_grant", "advancement_revoke" -> "advancement";
                case "spawn_entity" -> "entityType";
                case "damage_entity", "teleport_entity", "remove_entity",
                        "set_entity_attribute", "tag_entity" -> "selector";
                case "set_block", "fill_blocks" -> "block";
                case "explosion", "lightning" -> "position";
                case "set_difficulty" -> "difficulty";
                case "set_gamerule" -> "rule";
                case "world_border" -> "operation";
                case "clear_inventory", "remove_effects", "heal", "feed", "whitelist_add",
                        "whitelist_remove", "ban", "pardon", "close_menu" -> null;
                default -> "message";
            };
            if (primary != null && (!action.parameters().containsKey(primary)
                    || action.parameters().get(primary) == null
                    || action.parameters().get(primary).isBlank())) {
                throw new IllegalArgumentException(action.type() + " requires " + primary);
            }
            if (PLAYER_CONTEXT_ACTIONS.contains(action.type()) && !providesPlayerContext(event)) {
                throw new IllegalArgumentException(action.type()
                        + " requires a player-context event; use broadcast/log/server actions instead");
            }
            if (event.equals("player.leave") && ONLINE_PLAYER_ACTIONS.contains(action.type())) {
                throw new IllegalArgumentException(action.type()
                        + " cannot run after the player has left; use broadcast/log or identity-list actions instead");
            }
            action.parameters().values().forEach(TriggerEvaluator::validateTemplateVariables);
            validateActionParameters(action);
            if (action.type().equals("variable")) {
                String name = action.parameters().getOrDefault("name", "").strip();
                if (!TriggerEvaluator.hasTemplateVariable(name) && !declaredVariables.contains(name)
                        && !name.matches("global\\.[A-Za-z_][A-Za-z0-9_-]{0,47}")) {
                    throw new IllegalArgumentException("variable action references an undeclared variable: " + name);
                }
            }
        });
    }

    private static void validateActionParameters(TriggerDefinition.Action action) {
        Map<String, String> values = action.parameters();
        if (RESOURCE_ACTIONS.contains(action.type())) {
            String key = action.type().equals("sound") ? "sound"
                    : action.type().equals("give_item") ? "item" : "effect";
            resourceIdentifier(values.get(key), key);
        }
        switch (action.type()) {
            case "server_command", "player_command" -> {
                String command = values.get("command");
                if (command.length() > 32_768 || command.indexOf('\n') >= 0 || command.indexOf('\r') >= 0) {
                    throw new IllegalArgumentException("trigger command must be a single line of at most 32768 characters");
                }
                enumeration(values.getOrDefault("showFeedback", "false"), "showFeedback",
                        Set.of("true", "false"));
            }
            case "sound" -> {
                decimalRange(values.getOrDefault("volume", "1"), "volume", 0, 1_000);
                decimalRange(values.getOrDefault("pitch", "1"), "pitch", 0, 2);
            }
            case "title" -> {
                integerRange(values.getOrDefault("numberPrecision", "2"),
                        "numberPrecision", 0, TriggerEvaluator.MAX_MESSAGE_FRACTION_DIGITS);
                integerRange(values.getOrDefault("fadeIn", "10"), "fadeIn", 0, 12_000);
                integerRange(values.getOrDefault("stay", "70"), "stay", 0, 12_000);
                integerRange(values.getOrDefault("fadeOut", "20"), "fadeOut", 0, 12_000);
            }
            case "teleport" -> {
                String destination = values.get("destination").strip();
                if (destination.indexOf('\n') >= 0 || destination.indexOf('\r') >= 0
                        || destination.length() > 256) {
                    throw new IllegalArgumentException("teleport destination must be a single line of at most 256 characters");
                }
            }
            case "give_item" -> integerRange(values.getOrDefault("count", "1"), "count", 1, 6_400);
            case "clear_inventory" -> {
                String item = values.getOrDefault("item", "").strip();
                if (!item.isEmpty()) resourceIdentifier(item, "item");
                String maximum = values.getOrDefault("maxCount", "").strip();
                if (!maximum.isEmpty()) integerRange(maximum, "maxCount", 0, Integer.MAX_VALUE);
            }
            case "set_gamemode" -> enumeration(values.get("gamemode"), "gamemode",
                    Set.of("survival", "creative", "adventure", "spectator"));
            case "add_effect" -> {
                integerRange(values.getOrDefault("duration", "30"), "duration", 1, 1_000_000);
                integerRange(values.getOrDefault("amplifier", "0"), "amplifier", 0, 255);
            }
            case "set_time" -> {
                String time = values.get("time").strip().toLowerCase(java.util.Locale.ROOT);
                if (!Set.of("day", "night", "noon", "midnight").contains(time)) {
                    integerRange(time, "time", 0, 24_000);
                }
            }
            case "set_weather" -> {
                enumeration(values.get("weather"), "weather", Set.of("clear", "rain", "thunder"));
                integerRange(values.getOrDefault("duration", "300"), "duration", 1, 1_000_000);
            }
            case "log" -> enumeration(values.getOrDefault("level", "info"), "level",
                    Set.of("debug", "info", "warn", "error"));
            case "variable" -> {
                String name = values.getOrDefault("name", "").strip();
                if (!name.matches("(?:global\\.)?[A-Za-z_][A-Za-z0-9_-]{0,47}")) {
                    throw new IllegalArgumentException("variable action requires a valid variable name");
                }
                enumeration(values.getOrDefault("operation", "set"), "variable operation", Set.of(
                        "set", "parse", "add", "subtract", "multiply", "divide", "modulo",
                        "increment", "decrement", "append", "prepend", "replace", "regex_replace",
                        "trim", "upper", "lower", "escape_json", "escape_command", "escape_regex",
                        "toggle", "list_add", "list_remove", "array_add", "array_insert",
                        "array_remove", "array_remove_at", "array_set", "dictionary_put",
                        "dictionary_remove", "dictionary_merge", "clear"));
            }
            case "wait" -> {
                String modeValue = values.getOrDefault("mode", "duration");
                enumeration(modeValue, "wait mode", Set.of("duration", "ticks", "game_time", "condition"));
                String mode = modeValue.strip().toLowerCase(java.util.Locale.ROOT);
                integerRange(values.getOrDefault("timeout", "0"), "wait timeout", 0, 86_400);
                integerRange(values.getOrDefault("pollTicks", "20"), "wait pollTicks", 1, 72_000);
                if (TriggerEvaluator.hasTemplateVariable(modeValue)) {
                    // Runtime validation selects the mode after template rendering.
                } else if (mode.equals("duration")) {
                    decimalRange(values.getOrDefault("value", "0"), "wait value", 0, 86_400_000);
                } else if (mode.equals("ticks")) {
                    integerRange(values.getOrDefault("value", "0"), "wait ticks", 0, 86_400_000);
                } else if (mode.equals("game_time")) {
                    integerRange(values.getOrDefault("value", "0"), "wait game time", 0, Integer.MAX_VALUE);
                } else {
                    TriggerDefinition.Condition condition = new TriggerDefinition.Condition(
                            values.get("field"), values.getOrDefault("operator", "eq"),
                            values.getOrDefault("expected", ""));
                    if (!TriggerEvaluator.hasTemplateVariable(condition.operator())
                            && !OPERATOR_SET.contains(condition.operator())) {
                        throw new IllegalArgumentException("unsupported wait operator: " + condition.operator());
                    }
                }
            }
            case "run_trigger" -> {
                if (!TriggerEvaluator.hasTemplateVariable(values.getOrDefault("triggerId", ""))) {
                    try { java.util.UUID.fromString(values.getOrDefault("triggerId", "").strip()); }
                    catch (IllegalArgumentException invalid) {
                        throw new IllegalArgumentException("run_trigger requires a trigger UUID", invalid);
                    }
                }
                enumeration(values.getOrDefault("waitForCompletion", "false"), "waitForCompletion",
                        Set.of("true", "false"));
            }
            case "open_menu" -> {
                if (!TriggerEvaluator.hasTemplateVariable(values.getOrDefault("menuId", ""))) {
                    try { java.util.UUID.fromString(values.getOrDefault("menuId", "").strip()); }
                    catch (IllegalArgumentException invalid) {
                        throw new IllegalArgumentException("open_menu requires a menu UUID", invalid);
                    }
                }
            }
            case "economy_deposit", "economy_withdraw", "economy_set_balance", "economy_transfer",
                    "economy_deposit_player", "economy_withdraw_player", "economy_set_player_balance",
                    "economy_transfer_players" -> {
                String currency = values.getOrDefault("currency", "");
                if (!TriggerEvaluator.hasTemplateVariable(currency)
                        && !currency.matches("(?:[a-z][a-z0-9_]{0,31}|[0-9a-fA-F-]{36})")) {
                    throw new IllegalArgumentException("economy currency must be a code or UUID");
                }
                String amount = values.getOrDefault("amount", "");
                if (!TriggerEvaluator.hasTemplateVariable(amount)) {
                    try {
                        java.math.BigDecimal parsed = new java.math.BigDecimal(amount);
                        if (!Set.of("economy_set_balance", "economy_set_player_balance").contains(action.type())
                                && parsed.signum() <= 0) {
                            throw new NumberFormatException();
                        }
                    } catch (RuntimeException invalid) {
                        throw new IllegalArgumentException("economy amount must be a decimal number", invalid);
                    }
                }
                String reason = values.getOrDefault("reason", "Trigger economy operation");
                if (reason.isBlank() || reason.length() > 512) {
                    throw new IllegalArgumentException("economy reason must contain 1-512 characters");
                }
                if (action.type().equals("economy_transfer")
                        && values.getOrDefault("target", "").isBlank()) {
                    throw new IllegalArgumentException("economy_transfer requires target");
                }
                if (Set.of("economy_deposit_player", "economy_withdraw_player",
                        "economy_set_player_balance").contains(action.type())) {
                    economyIdentity(values.get("player"), "player");
                }
                if (action.type().equals("economy_transfer_players")) {
                    economyIdentity(values.get("source"), "source");
                    economyIdentity(values.get("target"), "target");
                }
            }
            case "set_health" -> decimalRange(values.get("value"), "health", 0, 2_048);
            case "set_food" -> integerRange(values.get("value"), "food", 0, 20);
            case "set_experience" -> {
                integerRange(values.get("amount"), "experience", 0, Integer.MAX_VALUE);
                enumeration(values.getOrDefault("mode", "points"), "experience mode",
                        Set.of("points", "levels"));
            }
            case "set_spawnpoint" -> coordinateTriple(values.get("position"), "position");
            case "scoreboard_set", "scoreboard_add" -> {
                safeWord(values.get("objective"), "objective", 64);
                integerRange(values.getOrDefault("value", "0"), "score", Integer.MIN_VALUE, Integer.MAX_VALUE);
            }
            case "team_join" -> safeWord(values.get("team"), "team", 64);
            case "advancement_grant", "advancement_revoke" ->
                    resourceIdentifier(values.get("advancement"), "advancement");
            case "spawn_entity" -> {
                resourceIdentifier(values.get("entityType"), "entityType");
                coordinateTriple(values.getOrDefault("position", "~ ~ ~"), "position");
                boundedSingleLine(values.getOrDefault("nbt", ""), "nbt", 16_384, true);
            }
            case "damage_entity" -> {
                selector(values.get("selector"));
                decimalRange(values.getOrDefault("amount", "1"), "damage amount", 0, 1_000_000);
                String damageType = values.getOrDefault("damageType", "");
                if (!damageType.isBlank()) resourceIdentifier(damageType, "damageType");
            }
            case "teleport_entity" -> {
                selector(values.get("selector"));
                coordinateTriple(values.get("destination"), "destination");
            }
            case "remove_entity" -> selector(values.get("selector"));
            case "set_entity_attribute" -> {
                selector(values.get("selector"));
                resourceIdentifier(values.get("attribute"), "attribute");
                decimalRange(values.get("value"), "attribute value", -1_000_000, 1_000_000);
            }
            case "tag_entity" -> {
                selector(values.get("selector"));
                enumeration(values.getOrDefault("operation", "add"), "tag operation", Set.of("add", "remove"));
                safeWord(values.get("tag"), "tag", 128);
            }
            case "set_block" -> {
                resourceIdentifier(values.getOrDefault("dimension", "minecraft:overworld"), "dimension");
                coordinateTriple(values.get("position"), "position");
                boundedSingleLine(values.get("block"), "block", 1_024, false);
                enumeration(values.getOrDefault("mode", "replace"), "set block mode",
                        Set.of("replace", "destroy", "keep"));
            }
            case "fill_blocks" -> {
                resourceIdentifier(values.getOrDefault("dimension", "minecraft:overworld"), "dimension");
                coordinateTriple(values.get("from"), "from");
                coordinateTriple(values.get("to"), "to");
                boundedSingleLine(values.get("block"), "block", 1_024, false);
                enumeration(values.getOrDefault("mode", "replace"), "fill mode",
                        Set.of("replace", "destroy", "keep", "hollow", "outline"));
            }
            case "explosion" -> {
                resourceIdentifier(values.getOrDefault("dimension", "minecraft:overworld"), "dimension");
                coordinateTriple(values.get("position"), "position");
                decimalRange(values.getOrDefault("power", "4"), "explosion power", 0, 128);
                enumeration(values.getOrDefault("fire", "false"), "explosion fire", Set.of("true", "false"));
            }
            case "lightning" -> {
                resourceIdentifier(values.getOrDefault("dimension", "minecraft:overworld"), "dimension");
                coordinateTriple(values.get("position"), "position");
            }
            case "set_difficulty" -> enumeration(values.get("difficulty"), "difficulty",
                    Set.of("peaceful", "easy", "normal", "hard"));
            case "set_gamerule" -> {
                safeWord(values.get("rule"), "gamerule", 128);
                boundedSingleLine(values.getOrDefault("value", ""), "gamerule value", 128, false);
            }
            case "world_border" -> {
                enumeration(values.get("operation"), "world border operation",
                        Set.of("size", "add", "center", "damage_amount", "damage_buffer", "warning_distance", "warning_time"));
                boundedSingleLine(values.getOrDefault("value", ""), "world border value", 128, false);
                integerRange(values.getOrDefault("seconds", "0"), "world border seconds", 0, 86_400);
            }
            default -> { }
        }
    }

    private static void resourceIdentifier(String value, String name) {
        if (TriggerEvaluator.hasTemplateVariable(value)) return;
        if (value == null || !value.strip().matches("(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+")) {
            throw new IllegalArgumentException(name + " must be a valid resource identifier");
        }
    }

    private static void economyIdentity(String value, String name) {
        if (value == null || value.isBlank() || value.length() > 128
                || value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0) {
            throw new IllegalArgumentException("economy " + name
                    + " must be a player name, UUID, or template variable");
        }
    }

    private static void selector(String value) {
        if (TriggerEvaluator.hasTemplateVariable(value)) return;
        boundedSingleLine(value, "selector", 512, false);
        String normalized = value.strip();
        if (!normalized.matches("(?:@[aep][^\\r\\n ]{0,509}|[A-Za-z0-9_]{1,16}|[0-9a-fA-F-]{36})")) {
            throw new IllegalArgumentException("selector must be a player/entity selector, name, or UUID");
        }
    }

    private static void coordinateTriple(String value, String name) {
        if (TriggerEvaluator.hasTemplateVariable(value)) return;
        if (value == null || !value.strip().matches("(?:[~^](?:-?(?:\\d+(?:\\.\\d*)?|\\.\\d+))?|-?(?:\\d+(?:\\.\\d*)?|\\.\\d+))"
                + "(?:\\s+(?:[~^](?:-?(?:\\d+(?:\\.\\d*)?|\\.\\d+))?|-?(?:\\d+(?:\\.\\d*)?|\\.\\d+))){2}")) {
            throw new IllegalArgumentException(name + " must contain exactly three coordinates");
        }
    }

    private static void safeWord(String value, String name, int maximum) {
        if (TriggerEvaluator.hasTemplateVariable(value)) return;
        if (value == null || !value.strip().matches("[A-Za-z0-9_.:-]{1," + maximum + "}")) {
            throw new IllegalArgumentException(name + " contains unsupported characters");
        }
    }

    private static void boundedSingleLine(String value, String name, int maximum, boolean emptyAllowed) {
        if (TriggerEvaluator.hasTemplateVariable(value)) return;
        String normalized = value == null ? "" : value.strip();
        if ((!emptyAllowed && normalized.isEmpty()) || normalized.length() > maximum
                || normalized.indexOf('\r') >= 0 || normalized.indexOf('\n') >= 0) {
            throw new IllegalArgumentException(name + " must be a bounded single line");
        }
    }

    private static void enumeration(String value, String name, Set<String> accepted) {
        if (TriggerEvaluator.hasTemplateVariable(value)) return;
        if (value == null || !accepted.contains(value.strip().toLowerCase(java.util.Locale.ROOT))) {
            throw new IllegalArgumentException(name + " must be one of " + accepted);
        }
    }

    private static void integerRange(String value, String name, int minimum, int maximum) {
        if (TriggerEvaluator.hasTemplateVariable(value)) return;
        try {
            long parsed = Long.parseLong(value.strip());
            if (parsed < minimum || parsed > maximum) throw new NumberFormatException();
        } catch (RuntimeException exception) {
            throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum, exception);
        }
    }

    private static void decimalRange(String value, String name, double minimum, double maximum) {
        if (TriggerEvaluator.hasTemplateVariable(value)) return;
        double parsed = finiteNumber(value, name);
        if (parsed < minimum || parsed > maximum) {
            throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
        }
    }

    private static double finiteNumber(String value, String name) {
        try {
            double parsed = Double.parseDouble(value.strip());
            if (!Double.isFinite(parsed)) throw new NumberFormatException();
            return parsed;
        } catch (RuntimeException exception) {
            throw new IllegalArgumentException(name + " must be a finite number", exception);
        }
    }

    private static boolean providesPlayerContext(String event) {
        return event.startsWith("player.") || event.equals("block.break") || event.equals("block.place")
                || event.equals("block.tool_modify")
                || event.startsWith("menu.")
                || event.startsWith("economy.")
                || event.startsWith("region.")
                || event.startsWith("custom.");
    }

    private static void validateEventConfiguration(TriggerDefinition.TriggerEvent event) {
        if (event.type().equals(TriggerCommandConfiguration.EVENT_TYPE)) {
            TriggerCommandConfiguration.parse(event);
            return;
        }
        if (!event.arguments().isEmpty()) {
            throw new IllegalArgumentException("command arguments require the player.command_trigger event");
        }
        if (event.type().equals("schedule.daily")) {
            rejectUnknownEventParameters(event, Set.of("time", "timezone"));
            String time = event.configuration().getOrDefault("time", "");
            try {
                java.time.LocalTime.parse(time);
                java.time.ZoneId.of(event.configuration().getOrDefault("timezone", "UTC"));
            } catch (java.time.DateTimeException exception) {
                throw new IllegalArgumentException("daily schedule needs a valid time and timezone", exception);
            }
        }
        if (event.type().equals("schedule.interval")) {
            rejectUnknownEventParameters(event, Set.of("seconds"));
            TriggerIntervalSchedule.totalSeconds(event.configuration());
            return;
        }
        if (event.type().startsWith("protection.")) {
            validateProtectionEvent(event);
            return;
        }
        if (event.type().startsWith("region.")) {
            rejectUnknownEventParameters(event, Set.of("regionId"));
            String region = event.configuration().getOrDefault("regionId", "");
            if (!region.matches("[a-z][a-z0-9_.-]{1,63}")) {
                throw new IllegalArgumentException("region event requires a regionId");
            }
            return;
        }
        if (!event.type().equals("schedule.daily") && !event.configuration().isEmpty()) {
            throw new IllegalArgumentException(event.type() + " does not accept event configuration parameters");
        }
    }

    private static void validateProtectionEvent(TriggerDefinition.TriggerEvent event) {
        Set<String> accepted = switch (event.type()) {
            case "protection.item_overflow", "protection.mob_overflow", "protection.entity_overflow" ->
                    Set.of("threshold", "scope", "cooldownSeconds");
            case "protection.mod_entity_overflow" ->
                    Set.of("threshold", "namespace", "cooldownSeconds");
            case "protection.slow_tick" ->
                    Set.of("threshold", "consecutive", "cooldownSeconds");
            default -> Set.of("threshold", "cooldownSeconds");
        };
        rejectUnknownEventParameters(event, accepted);
        int maximum = event.type().equals("protection.memory_pressure") ? 99 : Integer.MAX_VALUE;
        int minimum = event.type().equals("protection.memory_pressure") ? 50
                : event.type().equals("protection.slow_tick") ? 50 : 1;
        integerRange(event.configuration().getOrDefault("threshold", ""),
                "protection threshold", minimum, maximum);
        integerRange(event.configuration().getOrDefault("cooldownSeconds", "60"),
                "protection cooldownSeconds", 1, 86_400);
        if (Set.of("protection.item_overflow", "protection.mob_overflow",
                "protection.entity_overflow").contains(event.type())) {
            enumeration(event.configuration().getOrDefault("scope", "dimension"),
                    "protection scope", Set.of("dimension", "chunk"));
        }
        if (event.type().equals("protection.mod_entity_overflow")) {
            String namespace = event.configuration().getOrDefault("namespace", "").strip();
            if (!namespace.matches("[a-z0-9_.-]{1,64}")) {
                throw new IllegalArgumentException("mod entity protection requires a namespace");
            }
        }
        if (event.type().equals("protection.slow_tick")) {
            integerRange(event.configuration().getOrDefault("consecutive", "1"),
                    "protection consecutive", 1, 1_200);
        }
    }

    private static void rejectUnknownEventParameters(
            TriggerDefinition.TriggerEvent event, Set<String> accepted) {
        for (String parameter : event.configuration().keySet()) {
            if (!accepted.contains(parameter)) {
                throw new IllegalArgumentException(event.type()
                        + " does not accept event configuration parameter " + parameter);
            }
        }
    }
}
