package de.avatic.lcc.e2e.util;

import de.avatic.lcc.e2e.testdata.DestinationExpected;
import de.avatic.lcc.e2e.testdata.TestCaseExpected;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * Utility class for comparing actual results with expected values.
 * Supports tolerance-based comparison for numeric values.
 */
public final class ResultComparator {

    private static final Logger logger = Logger.getLogger(ResultComparator.class.getName());

    private ResultComparator() {
        // Utility class
    }

    /**
     * Asserts that actual results match expected values within the given tolerance.
     *
     * @param actualResults Map of actual result values from the UI
     * @param expected      Expected values from test case definition
     * @param tolerance     Relative tolerance for numeric comparisons (0.01 = 1%)
     * @throws AssertionError if any values don't match within tolerance
     */
    public static void assertResultsMatch(Map<String, Object> actualResults,
                                          TestCaseExpected expected,
                                          double tolerance) {
        List<String> failures = new ArrayList<>();

        // Compare main result fields
        compareNumeric(failures, "MEK_A", expected.mekA(), getDouble(actualResults, "mekA"), tolerance);
        compareNumeric(failures, "LOGISTIC_COST", expected.logisticCost(), getDouble(actualResults, "logisticCost"), tolerance);
        compareNumeric(failures, "MEK_B", expected.mekB(), getDouble(actualResults, "mekB"), tolerance);
        compareNumeric(failures, "FCA_FEE", expected.fcaFee(), getDouble(actualResults, "fcaFee"), tolerance);
        compareNumeric(failures, "TRANSPORTATION", expected.transportation(), getDouble(actualResults, "transportation"), tolerance);
        compareNumeric(failures, "D2D", expected.d2d(), getDouble(actualResults, "d2d"), tolerance);
        compareNumeric(failures, "AIR_FREIGHT", expected.airFreight(), getDouble(actualResults, "airFreight"), tolerance);
        compareNumeric(failures, "CUSTOM", expected.custom(), getDouble(actualResults, "custom"), tolerance);
        compareNumeric(failures, "REPACKAGING", expected.repackaging(), getDouble(actualResults, "repackaging"), tolerance);
        compareNumeric(failures, "HANDLING", expected.handling(), getDouble(actualResults, "handling"), tolerance);
        compareNumeric(failures, "DISPOSAL", expected.disposal(), getDouble(actualResults, "disposal"), tolerance);
        compareNumeric(failures, "SPACE", expected.space(), getDouble(actualResults, "space"), tolerance);
        compareNumeric(failures, "CAPITAL", expected.capital(), getDouble(actualResults, "capital"), tolerance);
        compareNumeric(failures, "SAFETY_STOCK", (double) expected.safetyStock(), getDouble(actualResults, "safetyStock"), tolerance);

        // Compare destination results
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> actualDestinations = (List<Map<String, Object>>) actualResults.get("destinations");
        List<DestinationExpected> expectedDestinations = expected.destinations();

        if (actualDestinations == null) {
            actualDestinations = List.of();
        }

        if (expectedDestinations.size() != actualDestinations.size()) {
            failures.add(String.format(
                "DESTINATION_COUNT: expected %d, got %d",
                expectedDestinations.size(), actualDestinations.size()
            ));
        } else {
            for (int i = 0; i < expectedDestinations.size(); i++) {
                DestinationExpected expDest = expectedDestinations.get(i);
                Map<String, Object> actDest = actualDestinations.get(i);
                String prefix = "DESTINATION_" + (i + 1) + "_";

                compareNumeric(failures, prefix + "TRANSIT_TIME",
                    (double) expDest.transitTime(),
                    getDouble(actDest, "transitTime"), tolerance);
                compareNumeric(failures, prefix + "STACKED_LAYERS",
                    (double) expDest.stackedLayers(),
                    getDouble(actDest, "stackedLayers"), tolerance);
                compareNumeric(failures, prefix + "CONTAINER_UNIT_COUNT",
                    (double) expDest.containerUnitCount(),
                    getDouble(actDest, "containerUnitCount"), tolerance);
                compareString(failures, prefix + "CONTAINER_TYPE",
                    expDest.containerType(),
                    getString(actDest, "containerType"));
                compareString(failures, prefix + "LIMITING_FACTOR",
                    expDest.limitingFactor(),
                    getString(actDest, "limitingFactor"));
            }
        }

        if (!failures.isEmpty()) {
            StringBuilder message = new StringBuilder("Result comparison failed:\n");
            for (String failure : failures) {
                message.append("  - ").append(failure).append("\n");
            }
            throw new AssertionError(message.toString());
        }

        logger.info("All results match within tolerance");
    }

    /**
     * Compares two numeric values with tolerance and adds failure message if they don't match.
     */
    private static void compareNumeric(List<String> failures, String fieldName,
                                        double expected, Double actual, double tolerance) {
        // Handle zero expected values - if expected is ~0 and actual is null, treat as pass
        // (some fields are not displayed in the UI when their value is 0)
        if (Math.abs(expected) < 1e-10) {
            if (actual == null) {
                // Expected ~0 and actual is null (field not shown) - this is acceptable
                return;
            }
            if (Math.abs(actual) > tolerance) {
                failures.add(String.format("%s: expected ~0, got %.6f", fieldName, actual));
            }
            return;
        }

        if (actual == null) {
            failures.add(String.format("%s: actual value is null, expected %.6f", fieldName, expected));
            return;
        }

        double relativeDiff = Math.abs(expected - actual) / Math.abs(expected);
        if (relativeDiff > tolerance) {
            failures.add(String.format(
                "%s: expected %.6f, got %.6f (diff: %.2f%%)",
                fieldName, expected, actual, relativeDiff * 100
            ));
        }
    }

    /**
     * Compares two string values and adds failure message if they don't match.
     */
    private static void compareString(List<String> failures, String fieldName,
                                       String expected, String actual) {
        if (expected == null && actual == null) {
            return;
        }
        if (expected == null || actual == null || !expected.equals(actual)) {
            failures.add(String.format("%s: expected '%s', got '%s'", fieldName, expected, actual));
        }
    }

    /**
     * Safely gets a Double value from a map.
     */
    private static Double getDouble(Map<String, Object> map, String key) {
        if (map == null) {
            return null;
        }
        Object value = map.get(key);
        if (value == null) {
            return null;
        }
        if (value instanceof Double) {
            return (Double) value;
        }
        if (value instanceof Number) {
            return ((Number) value).doubleValue();
        }
        try {
            return Double.parseDouble(value.toString().replaceAll("[€$,\\s]", "").replace(",", "."));
        } catch (NumberFormatException e) {
            return null;
        }
    }

    /**
     * Safely gets a String value from a map.
     */
    private static String getString(Map<String, Object> map, String key) {
        if (map == null) {
            return null;
        }
        Object value = map.get(key);
        return value != null ? value.toString() : null;
    }
}
