package com.xfestudio.xfeservermanager.core.trigger;

import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Compiler for the intentionally small, auditable XFE Script trigger DSL. */
public final class TriggerScriptCompiler {
    private static final String CONDITION_BODY = "([A-Za-z0-9_.-]+)\\s+("
            + TriggerCatalog.OPERATORS.stream().map(Pattern::quote)
                    .collect(java.util.stream.Collectors.joining("|"))
            + ")(?:\\s+(.+))?";
    private static final Pattern CONDITION = conditionPattern("when");
    private static final Pattern ACTION_CONDITION = conditionPattern("if");
    private static final Pattern PARAMETER = Pattern.compile(
            "([A-Za-z][A-Za-z0-9_-]*)=(?:\\\"((?:\\\\.|[^\\\"])*)\\\"|'((?:\\\\.|[^'])*)'|([^\\s]+))");

    private TriggerScriptCompiler() { }

    public static TriggerDefinition.Program compile(String script) {
        if (script == null || script.isBlank()) throw new IllegalArgumentException("script is empty");
        if (script.length() > TriggerDefinition.MAX_SCRIPT_CHARACTERS) {
            throw new IllegalArgumentException("script cannot exceed 65536 characters");
        }
        TriggerDefinition.TriggerEvent event = null;
        TriggerDefinition.MatchMode mode = TriggerDefinition.MatchMode.ALL;
        List<TriggerDefinition.Condition> conditions = new ArrayList<>();
        List<TriggerDefinition.Action> actions = new ArrayList<>();
        List<TriggerDefinition.VariableDefinition> variables = new ArrayList<>();
        List<ArgumentDeclaration> argumentDeclarations = new ArrayList<>();
        Deque<ActionBlock> actionBlocks = new ArrayDeque<>();
        Map<String, String> eventConfiguration = new LinkedHashMap<>();
        int lineNumber = 0;
        for (String raw : script.lines().toList()) {
            lineNumber++;
            String line = raw.strip();
            if (line.isEmpty() || line.startsWith("#") || line.startsWith("//")) continue;
            try {
                if (line.equalsIgnoreCase("end")) {
                    if (actionBlocks.isEmpty()) {
                        throw new IllegalArgumentException("end has no matching if");
                    }
                    ActionBlock block = actionBlocks.pop();
                    if (block.actions().isEmpty()) {
                        throw new IllegalArgumentException(
                                "if block from line " + block.lineNumber() + " needs at least one action");
                    }
                    addAction(actions, actionBlocks, new TriggerDefinition.Action(
                            TriggerActionTree.CONDITION_TYPE,
                            conditionParameters(block.condition()), block.actions()));
                } else if (line.regionMatches(true, 0, "if ", 0, 3)) {
                    Matcher matcher = ACTION_CONDITION.matcher(line);
                    if (!matcher.matches()) throw new IllegalArgumentException("invalid if statement");
                    actionBlocks.push(new ActionBlock(condition(matcher), new ArrayList<>(), lineNumber));
                } else if (line.regionMatches(true, 0, "on ", 0, 3)) {
                    requireTopLevel(actionBlocks, "on");
                    if (event != null) throw new IllegalArgumentException("only one on statement is allowed");
                    event = new TriggerDefinition.TriggerEvent(line.substring(3).strip(), Map.of());
                } else if (line.regionMatches(true, 0, "set ", 0, 4)) {
                    requireTopLevel(actionBlocks, "set");
                    String[] pair = line.substring(4).strip().split("\\s*=\\s*", 2);
                    if (pair.length != 2) throw new IllegalArgumentException("set expects name=value");
                    eventConfiguration.put(pair[0].strip(), unquote(pair[1].strip()));
                } else if (line.regionMatches(true, 0, "var ", 0, 4)) {
                    requireTopLevel(actionBlocks, "var");
                    String declaration = line.substring(4).strip();
                    int first = declaration.indexOf(' ');
                    int second = first < 0 ? -1 : declaration.indexOf(' ', first + 1);
                    if (first < 1) throw new IllegalArgumentException("var expects name type initialValue");
                    String name = declaration.substring(0, first).strip();
                    String type = second < 0 ? declaration.substring(first + 1).strip()
                            : declaration.substring(first + 1, second).strip();
                    String tail = second < 0 ? "" : declaration.substring(second + 1).strip();
                    if (tail.matches("(?is)^(?:initial|visibility|storage|lifetime|ttlSeconds)\\s*=.*")) {
                        Map<String, String> fields = parameters(tail);
                        for (String field : fields.keySet()) {
                            if (!Set.of("initial", "visibility", "storage", "lifetime", "ttlSeconds").contains(field)) {
                                throw new IllegalArgumentException("unsupported var field " + field);
                            }
                        }
                        Long ttlSeconds = fields.containsKey("ttlSeconds")
                                ? Long.parseLong(fields.get("ttlSeconds")) : null;
                        variables.add(new TriggerDefinition.VariableDefinition(name, type,
                                fields.getOrDefault("initial", TriggerValueTypes.defaultVariableValue(type)),
                                fields.getOrDefault("visibility", "trigger"),
                                fields.getOrDefault("storage", "trigger"),
                                fields.getOrDefault("lifetime", "session"), ttlSeconds, 0L));
                    } else {
                        // v1 syntax: var <name> <type> <initialValue>
                        variables.add(new TriggerDefinition.VariableDefinition(name, type,
                                tail.isEmpty() ? "" : unquote(tail)));
                    }
                } else if (line.regionMatches(true, 0, "arg ", 0, 4)) {
                    requireTopLevel(actionBlocks, "arg");
                    String declaration = line.substring(4).strip();
                    int first = declaration.indexOf(' ');
                    int second = first < 0 ? -1 : declaration.indexOf(' ', first + 1);
                    if (first < 1) throw new IllegalArgumentException("arg expects name type and optional fields");
                    String name = declaration.substring(0, first).strip();
                    String type = second < 0 ? declaration.substring(first + 1).strip()
                            : declaration.substring(first + 1, second).strip();
                    Map<String, String> fields = parameters(second < 0 ? "" : declaration.substring(second + 1));
                    argumentDeclarations.add(new ArgumentDeclaration(name, type, fields));
                } else if (line.regionMatches(true, 0, "match ", 0, 6)) {
                    requireTopLevel(actionBlocks, "match");
                    mode = TriggerDefinition.MatchMode.valueOf(line.substring(6).strip().toUpperCase(java.util.Locale.ROOT));
                } else if (line.regionMatches(true, 0, "when ", 0, 5)) {
                    requireTopLevel(actionBlocks, "when");
                    Matcher matcher = CONDITION.matcher(line);
                    if (!matcher.matches()) throw new IllegalArgumentException("invalid when statement");
                    conditions.add(condition(matcher));
                } else if (line.regionMatches(true, 0, "do ", 0, 3)) {
                    String body = line.substring(3).strip();
                    int space = body.indexOf(' ');
                    String type = space < 0 ? body : body.substring(0, space);
                    String rawParameters = space < 0 ? "" : body.substring(space + 1);
                    addAction(actions, actionBlocks,
                            new TriggerDefinition.Action(type, parameters(rawParameters)));
                } else {
                    throw new IllegalArgumentException("expected on, set, var, arg, match, when, if, do, or end");
                }
            } catch (RuntimeException exception) {
                throw new IllegalArgumentException("script line " + lineNumber + ": " + exception.getMessage(), exception);
            }
        }
        if (!actionBlocks.isEmpty()) {
            ActionBlock block = actionBlocks.peek();
            throw new IllegalArgumentException(
                    "script line " + block.lineNumber() + ": if block is not closed; expected end");
        }
        if (event == null) throw new IllegalArgumentException("script needs an on statement");
        List<TriggerDefinition.CommandArgument> argumentTree = arguments(argumentDeclarations);
        TriggerDefinition.Program program = new TriggerDefinition.Program(
                new TriggerDefinition.TriggerEvent(event.type(), eventConfiguration, argumentTree, variables),
                mode, conditions, actions);
        TriggerActionTree.validateProgram(program);
        return program;
    }

    private static Pattern conditionPattern(String keyword) {
        return Pattern.compile("^" + keyword + "\\s+" + CONDITION_BODY + "$", Pattern.CASE_INSENSITIVE);
    }

    private static TriggerDefinition.Condition condition(Matcher matcher) {
        return new TriggerDefinition.Condition(matcher.group(1), matcher.group(2),
                matcher.group(3) == null ? "" : unquote(matcher.group(3).strip()));
    }

    private static Map<String, String> conditionParameters(TriggerDefinition.Condition condition) {
        Map<String, String> result = new LinkedHashMap<>();
        result.put("field", condition.field());
        result.put("operator", condition.operator());
        result.put("value", condition.value());
        return result;
    }

    private static void addAction(
            List<TriggerDefinition.Action> root,
            Deque<ActionBlock> actionBlocks,
            TriggerDefinition.Action action) {
        if (actionBlocks.isEmpty()) root.add(action);
        else actionBlocks.peek().actions().add(action);
    }

    private static void requireTopLevel(Deque<ActionBlock> actionBlocks, String statement) {
        if (!actionBlocks.isEmpty()) {
            throw new IllegalArgumentException(statement + " is not allowed inside an if block");
        }
    }

    private static Map<String, String> parameters(String value) {
        Map<String, String> result = new LinkedHashMap<>();
        Matcher matcher = PARAMETER.matcher(value);
        int end = 0;
        while (matcher.find()) {
            if (!value.substring(end, matcher.start()).isBlank()) {
                throw new IllegalArgumentException("invalid action parameters");
            }
            String parameter = matcher.group(2) != null ? matcher.group(2)
                    : matcher.group(3) != null ? matcher.group(3) : matcher.group(4);
            result.put(matcher.group(1), unescape(parameter));
            end = matcher.end();
        }
        if (!value.substring(end).isBlank()) throw new IllegalArgumentException("invalid action parameters");
        return result;
    }

    private static String unquote(String value) {
        if (value.length() >= 2 && ((value.startsWith("\"") && value.endsWith("\""))
                || (value.startsWith("'") && value.endsWith("'")))) {
            return unescape(value.substring(1, value.length() - 1));
        }
        return value;
    }

    private static String unescape(String value) {
        StringBuilder result = new StringBuilder(value.length());
        for (int index = 0; index < value.length(); index++) {
            char current = value.charAt(index);
            if (current != '\\' || index + 1 >= value.length()) {
                result.append(current);
                continue;
            }
            char escaped = value.charAt(++index);
            switch (escaped) {
                case 'n' -> result.append('\n');
                case 'r' -> result.append('\r');
                case '\"' -> result.append('\"');
                case '\'' -> result.append('\'');
                case '\\' -> result.append('\\');
                default -> result.append('\\').append(escaped);
            }
        }
        return result.toString();
    }

    private static List<TriggerDefinition.CommandArgument> arguments(List<ArgumentDeclaration> declarations) {
        Map<String, ArgumentDeclaration> byName = new LinkedHashMap<>();
        for (ArgumentDeclaration declaration : declarations) {
            if (byName.putIfAbsent(declaration.name(), declaration) != null) {
                throw new IllegalArgumentException("duplicate arg declaration " + declaration.name());
            }
        }
        List<TriggerDefinition.CommandArgument> roots = new ArrayList<>();
        for (ArgumentDeclaration declaration : declarations) {
            if (declaration.fields().getOrDefault("parent", "").isBlank()) {
                roots.add(argument(declaration, declarations, new java.util.LinkedHashSet<>()));
            }
        }
        if (roots.isEmpty() && !declarations.isEmpty()) {
            throw new IllegalArgumentException("arg declarations contain a parent cycle");
        }
        java.util.Set<String> reached = new java.util.LinkedHashSet<>();
        collectArgumentNames(roots, reached);
        if (reached.size() != declarations.size()) {
            throw new IllegalArgumentException("arg parent is missing or cyclic");
        }
        return List.copyOf(roots);
    }

    private static TriggerDefinition.CommandArgument argument(
            ArgumentDeclaration declaration, List<ArgumentDeclaration> declarations,
            java.util.Set<String> ancestors) {
        if (!ancestors.add(declaration.name())) throw new IllegalArgumentException("arg parent cycle");
        List<TriggerDefinition.CommandArgument> children = declarations.stream()
                .filter(value -> value.fields().getOrDefault("parent", "").equals(declaration.name()))
                .map(value -> argument(value, declarations, new java.util.LinkedHashSet<>(ancestors))).toList();
        Map<String, String> fields = declaration.fields();
        java.util.Set<String> accepted = java.util.Set.of(
                "parent", "literal", "optional", "error", "min", "max", "suggestions");
        if (fields.keySet().stream().anyMatch(key -> !accepted.contains(key))) {
            throw new IllegalArgumentException("arg contains an unsupported field");
        }
        List<String> suggestions = java.util.Arrays.stream(fields.getOrDefault("suggestions", "").split(","))
                .map(String::strip).filter(value -> !value.isEmpty()).toList();
        String optional = fields.getOrDefault("optional", "false").strip().toLowerCase(java.util.Locale.ROOT);
        if (!java.util.Set.of("true", "false").contains(optional)) {
            throw new IllegalArgumentException("arg optional must be true or false");
        }
        return new TriggerDefinition.CommandArgument(declaration.name(), declaration.type(),
                fields.getOrDefault("literal", ""),
                Boolean.parseBoolean(optional),
                fields.getOrDefault("error", ""), fields.getOrDefault("min", ""),
                fields.getOrDefault("max", ""), suggestions, children);
    }

    private static void collectArgumentNames(
            List<TriggerDefinition.CommandArgument> arguments, java.util.Set<String> result) {
        for (TriggerDefinition.CommandArgument argument : arguments) {
            result.add(argument.name());
            collectArgumentNames(argument.children(), result);
        }
    }

    private record ActionBlock(
            TriggerDefinition.Condition condition,
            List<TriggerDefinition.Action> actions,
            int lineNumber) { }

    private record ArgumentDeclaration(String name, String type, Map<String, String> fields) { }
}
