package com.r35157.jupiterperpsalarm.impl.ref;

import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class AlarmConfigurationParser {

    public static AlarmConfiguration parse(Path path) throws IOException {
        List<String> lines = Files.readAllLines(path);
        int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
                path,
                lines,
                SUPPORTED_FORMAT_VERSION
        );
        List<PriceAlarmDefinition> alarms = new ArrayList<>();
        Map<String, String> constants = new LinkedHashMap<>();

        for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
            if (lineNumber - 1 == formatVersionLineIndex) {
                continue;
            }

            String line = lines.get(lineNumber - 1);
            String trimmed = line.trim();

            if(trimmed.isEmpty() || trimmed.startsWith("#")) {
                continue;
            }

            try {
                if(isConstantDefinition(trimmed)) {
                    parseConstantDefinition(constants, line);
                } else {
                    alarms.add(parseLine(line));
                }
            } catch (RuntimeException exception) {
                throw new IllegalArgumentException(
                        path + ":" + lineNumber + ": " + exception.getMessage(),
                        exception
                );
            }
        }

        return new AlarmConfiguration(alarms, constants);
    }

    static PriceAlarmDefinition parseLine(String line) {
        Cursor cursor = new Cursor(line);

        int id = Integer.parseInt(cursor.nextToken("id"));

        JupiterPerpsAsset asset = JupiterPerpsAsset.valueOf(
                cursor.nextToken("asset").toUpperCase(Locale.ROOT)
        );

        String conditionExpression = parseConditionExpression(cursor);

        TriggerConfiguration triggerConfiguration = parseTrigger(
                cursor.nextToken("trigger")
        );

        validateConditionForTrigger(
                conditionExpression,
                triggerConfiguration.trigger()
        );

        cursor.skipWhitespace();
        if (!cursor.atEnd() && cursor.current() != '#') {
            throw new IllegalArgumentException(
                    "Unexpected text after trigger: " + cursor.remaining()
            );
        }

        return new PriceAlarmDefinition(
                id,
                asset,
                conditionExpression,
                triggerConfiguration.trigger(),
                triggerConfiguration.gracePeriod()
        );
    }

    private static String parseConditionExpression(Cursor cursor) {
        String conditionExpression = cursor.nextToken("condition");

        if (conditionExpression.equals("<")
                || conditionExpression.equals("<=")
                || conditionExpression.equals(">")
                || conditionExpression.equals(">=")) {
            throw new IllegalArgumentException(
                    "Condition must not contain whitespace: " + conditionExpression
            );
        }

        return conditionExpression;
    }

    private static void validateConditionForTrigger(
            String conditionExpression,
            AlarmTrigger trigger
    ) {
        if (trigger != AlarmTrigger.CROSSING) {
            if (conditionExpression.contains("/")) {
                throw new IllegalArgumentException(
                        "Hysteresis is only supported for CROSSING conditions: "
                                + conditionExpression
                );
            }
            return;
        }

        boolean directionalCondition =
                (conditionExpression.startsWith("<")
                        && !conditionExpression.startsWith("<="))
                || (conditionExpression.startsWith(">")
                        && !conditionExpression.startsWith(">="));

        if (!directionalCondition) {
            throw new IllegalArgumentException(
                    "CROSSING requires a '<' or '>' condition: " + conditionExpression
            );
        }
    }

    private static TriggerConfiguration parseTrigger(String triggerText) {
        String normalized = triggerText.toUpperCase(Locale.ROOT);

        if (normalized.equals("ONETIME")) {
            return new TriggerConfiguration(AlarmTrigger.ONETIME, 0L);
        }

        if (normalized.equals("CROSSING")) {
            return new TriggerConfiguration(AlarmTrigger.CROSSING, 0L);
        }

        if (normalized.equals("PERSISTENT")) {
            return new TriggerConfiguration(AlarmTrigger.PERSISTENT, 0L);
        }

        if (normalized.startsWith("PERSISTENT:")) {
            String graceText = triggerText.substring("PERSISTENT:".length());

            return new TriggerConfiguration(
                    AlarmTrigger.PERSISTENT,
                    parsePersistentGracePeriod(graceText)
            );
        }

        if (normalized.startsWith("ONETIME:")) {
            throw new IllegalArgumentException("ONETIME cannot have a grace period: " + triggerText);
        }

        if (normalized.startsWith("CROSSING:")) {
            throw new IllegalArgumentException("CROSSING cannot have a grace period: " + triggerText);
        }

        throw new IllegalArgumentException("Unknown trigger: " + triggerText);
    }

    private static ΩmilliSecondsΩ parsePersistentGracePeriod(String graceText) {
        Matcher matcher = PERSISTENT_GRACE_PERIOD_PATTERN.matcher(graceText);

        if (!matcher.matches()) {
            throw invalidPersistentGracePeriod(graceText);
        }

        try {
            long value = Long.parseLong(matcher.group(1));
            long millisecondsPerUnit = switch (matcher.group(2)) {
                case "ms" -> 1L;
                case "s" -> 1_000L;
                case "m" -> 60_000L;
                case "h" -> 3_600_000L;
                case "d" -> 86_400_000L;
                case "w" -> 604_800_000L;
                case "M" -> 2_592_000_000L;
                case "y" -> 31_536_000_000L;
                default -> throw new AssertionError("Unsupported grace period suffix");
            };

            return Math.multiplyExact(value, millisecondsPerUnit);
        } catch (NumberFormatException | ArithmeticException exception) {
            throw invalidPersistentGracePeriod(graceText, exception);
        }
    }

    private static IllegalArgumentException invalidPersistentGracePeriod(
            String graceText
    ) {
        return invalidPersistentGracePeriod(graceText, null);
    }

    private static IllegalArgumentException invalidPersistentGracePeriod(
            String graceText,
            Exception cause
    ) {
        return new IllegalArgumentException(
                "Invalid persistent grace period '" + graceText
                        + "'. Expected a non-negative whole number followed by one of: "
                        + "ms, s, m, h, d, w, M, y",
                cause
        );
    }

    private static final class Cursor {
        private Cursor(String line) {
            this.line = line;
        }

        private String nextToken(String fieldName) {
            skipWhitespace();
            if (atEnd()) {
                throw new IllegalArgumentException("Missing " + fieldName);
            }

            int start = position;
            while (!atEnd() && !Character.isWhitespace(current())) {
                position++;
            }
            return line.substring(start, position);
        }

        private void skipWhitespace() {
            while (!atEnd() && Character.isWhitespace(current())) {
                position++;
            }
        }

        private boolean atEnd() {
            return position >= line.length();
        }

        private char current() {
            return line.charAt(position);
        }

        private String remaining() {
            return line.substring(position);
        }

        private final String line;
        private int position;
    }

    private static boolean isConstantDefinition(String trimmedLine) {
        return trimmedLine.startsWith("{{");
    }

    private static void parseConstantDefinition(
            Map<String, String> constants,
            String line
    ) {
        Cursor cursor = new Cursor(line);

        String name = cursor.nextToken("constant name");
        String value = cursor.nextToken("constant value");

        cursor.skipWhitespace();
        if (!cursor.atEnd() && cursor.current() != '#') {
            throw new IllegalArgumentException(
                    "Unexpected text after constant value: " + cursor.remaining()
            );
        }

        validateConstantName(name);

        constants.put(parseVariableName(name), value);
    }

    private static String parseVariableName(String variableName) {
        if (!variableName.startsWith("{{") || !variableName.endsWith("}}")) {
            throw new IllegalArgumentException("Invalid variable name: " + variableName);
        }

        String parsedVariableName = variableName.substring(2, variableName.length() - 2);

        if (parsedVariableName.isBlank()) {
            throw new IllegalArgumentException("Variable name cannot be blank: " + variableName);
        }

        return parsedVariableName;
    }

    private static String replaceConstants(
            String line,
            Map<String, String> constants
    ) {
        String result = line;

        for (Map.Entry<String, String> entry : constants.entrySet()) {
            result = result.replace(entry.getKey(), entry.getValue());
        }

        if (result.contains("{{") || result.contains("}}")) {
            throw new IllegalArgumentException(
                    "Unknown or malformed constant in line: " + line
            );
        }

        return result;
    }

    private static void validateConstantName(String name) {
        if (!name.matches("\\{\\{[A-Z0-9_]+}}")) {
            throw new IllegalArgumentException("Invalid constant name: " + name);
        }
    }

    private record TriggerConfiguration(
            AlarmTrigger trigger,
            ΩmilliSecondsΩ gracePeriod
    ) {
    }

    private static final Pattern PERSISTENT_GRACE_PERIOD_PATTERN =
            Pattern.compile("(\\d+)(ms|s|m|h|d|w|M|y)");
    private static final int SUPPORTED_FORMAT_VERSION = 2;

    private AlarmConfigurationParser() {
    }
}
