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", "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");
    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")));
    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");
    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");
    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 "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");
                }
            }
            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 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("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().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);
            }
        }
    }
}
