package de.avatic.lcc.e2e.pages;

import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.AriaRole;
import com.microsoft.playwright.options.WaitForSelectorState;
import de.avatic.lcc.e2e.testdata.DestinationExpected;
import de.avatic.lcc.e2e.testdata.TestCaseExpected;

import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * Page Object for the calculation results/report page.
 * Handles navigating to reports and reading calculation results.
 */
public class ResultsPage extends BasePage {

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

    // Report page selectors based on Report.vue structure
    private static final String REPORT_CONTAINER = ".report-container";
    private static final String CREATE_REPORT_BUTTON = "button:has-text('Create report')";
    private static final String REPORT_BOX = ".box"; // Reports are shown inside Box components

    public ResultsPage(Page page) {
        super(page);
    }

    /**
     * Navigates to the reports page and creates a report for the given material/supplier.
     */
    public void navigateToReports(String baseUrl, String partNumber, String supplierName) {
        // Navigate to reports page
        page.navigate(baseUrl + "/reports");
        page.waitForLoadState();
        logger.info("Navigated to reports page");

        // Click "Create report" button
        Locator createReportBtn = page.locator(CREATE_REPORT_BUTTON);
        createReportBtn.waitFor(new Locator.WaitForOptions()
            .setState(WaitForSelectorState.VISIBLE)
            .setTimeout(10000));
        createReportBtn.click();
        logger.info("Clicked Create report button");

        // Wait for the modal to fully open
        page.waitForTimeout(1000);

        // The modal has an autosuggest search bar with specific placeholder
        // Use the placeholder text to find the correct input inside the modal
        Locator searchInput = page.locator("input[placeholder='Select material for reporting']");
        searchInput.waitFor(new Locator.WaitForOptions()
            .setState(WaitForSelectorState.VISIBLE)
            .setTimeout(5000));
        searchInput.click();
        searchInput.fill(partNumber);
        logger.info("Entered part number in search: " + partNumber);
        page.waitForTimeout(1500);

        // Wait for and select the material from suggestions
        Locator suggestion = page.locator(".suggestion-item").first();
        try {
            suggestion.waitFor(new Locator.WaitForOptions()
                .setState(WaitForSelectorState.VISIBLE)
                .setTimeout(5000));
            suggestion.click();
            logger.info("Selected material from suggestions");
        } catch (Exception e) {
            logger.warning("Could not select material from suggestions: " + e.getMessage());
        }

        // Wait for suppliers list to load
        page.waitForTimeout(1500);

        // Select the supplier by clicking on its item-list-element
        // The supplier name is inside a supplier-item component
        try {
            Locator supplierElement = page.locator(".item-list-element")
                .filter(new Locator.FilterOptions().setHasText(supplierName))
                .first();

            if (supplierElement.count() > 0) {
                supplierElement.click();
                logger.info("Selected supplier: " + supplierName);
                page.waitForTimeout(500);
            } else {
                logger.warning("Supplier not found: " + supplierName);
            }
        } catch (Exception e) {
            logger.warning("Could not select supplier: " + e.getMessage());
        }

        // Click OK button inside the modal footer
        Locator okButton = page.locator(".footer button:has-text('OK')");
        try {
            okButton.waitFor(new Locator.WaitForOptions()
                .setState(WaitForSelectorState.VISIBLE)
                .setTimeout(5000));
            okButton.click();
            logger.info("Clicked OK button");
        } catch (Exception e) {
            // Fallback: try to find any OK button
            page.locator("button:has-text('OK')").first().click();
        }

        // Wait for the report to load
        waitForResults();
    }

    /**
     * Waits for the results to be loaded.
     */
    public void waitForResults() {
        // Wait for any "Prepare report" modal to disappear
        try {
            Locator prepareReportModal = page.locator(".modal-overlay, .modal-container, .modal-dialog");
            if (prepareReportModal.count() > 0 && prepareReportModal.first().isVisible()) {
                logger.info("Waiting for modal to close...");
                prepareReportModal.first().waitFor(new Locator.WaitForOptions()
                    .setState(WaitForSelectorState.HIDDEN)
                    .setTimeout(30000));
                logger.info("Modal closed");
            }
        } catch (Exception e) {
            // Modal might not be present or already closed
        }

        try {
            // Wait for report container or spinner to disappear
            page.locator(".report-spinner, .spinner").waitFor(new Locator.WaitForOptions()
                .setState(WaitForSelectorState.HIDDEN)
                .setTimeout(30000));
        } catch (Exception e) {
            // Spinner might not be present
        }

        try {
            page.locator(REPORT_CONTAINER).waitFor(new Locator.WaitForOptions()
                .setState(WaitForSelectorState.VISIBLE)
                .setTimeout(30000));
            page.waitForLoadState();
            logger.info("Results loaded");
        } catch (Exception e) {
            logger.warning("Results container not found, continuing...");
        }
    }

    /**
     * Expands all collapsible boxes on the report page.
     * The Vue CollapsibleBox component uses:
     * - .box-content.collapsed for hidden content
     * - .collapse-button in the header to toggle
     * - The outer box element gets class "collapsible" when collapsed and clickable
     */
    public void expandAllCollapsibleBoxes() {
        try {
            // Strategy: Keep clicking on collapsed boxes until none remain
            // After each click, re-query the DOM since it changes
            int maxIterations = 20; // Safety limit
            int totalExpanded = 0;

            for (int iteration = 0; iteration < maxIterations; iteration++) {
                // Find collapsed content sections
                Locator collapsedContent = page.locator(".box-content.collapsed");
                int collapsedCount = collapsedContent.count();

                if (collapsedCount == 0) {
                    break; // All expanded
                }

                final int iterNum = iteration + 1;
                final int remaining = collapsedCount;
                logger.info(() -> "Iteration " + iterNum + ": Found " + remaining + " collapsed boxes");

                // Try to expand the first collapsed box
                try {
                    Locator firstCollapsed = collapsedContent.first();

                    // Navigate up to find the clickable header span (the title)
                    // Structure: box > div > div.box-header > span (clickable)
                    Locator headerSpan = firstCollapsed.locator("xpath=preceding-sibling::div[contains(@class, 'box-header')]//span").first();

                    if (headerSpan.count() > 0 && headerSpan.isVisible()) {
                        headerSpan.click();
                        page.waitForTimeout(400); // Wait for animation
                        totalExpanded++;
                        logger.info(() -> "Expanded box via header span");
                        continue;
                    }

                    // Alternative: Try clicking the collapse button
                    Locator collapseButton = firstCollapsed.locator("xpath=preceding-sibling::div[contains(@class, 'box-header')]//button[contains(@class, 'collapse-button')]").first();

                    if (collapseButton.count() > 0 && collapseButton.isVisible()) {
                        collapseButton.click();
                        page.waitForTimeout(400);
                        totalExpanded++;
                        logger.info(() -> "Expanded box via collapse button");
                        continue;
                    }

                    // Alternative: Click on the parent box element which also has a click handler
                    Locator parentBox = firstCollapsed.locator("xpath=ancestor::*[contains(@class, 'collapsible')]").first();

                    if (parentBox.count() > 0 && parentBox.isVisible()) {
                        parentBox.click();
                        page.waitForTimeout(400);
                        totalExpanded++;
                        logger.info(() -> "Expanded box via parent collapsible element");
                        continue;
                    }

                    // If nothing worked, log and try next
                    logger.warning("Could not find clickable element for collapsed box");

                } catch (Exception e) {
                    final String errorMsg = e.getMessage();
                    logger.warning(() -> "Error expanding box: " + errorMsg);
                }
            }

            // Final check
            int finalCollapsed = page.locator(".box-content.collapsed").count();
            final int expanded = totalExpanded;
            final int stillCollapsedFinal = finalCollapsed;
            logger.info(() -> "Expanded " + expanded + " boxes, " + stillCollapsedFinal + " still collapsed");

            page.waitForTimeout(500); // Wait for all animations to complete
        } catch (Exception e) {
            logger.warning("Could not expand all boxes: " + e.getMessage());
        }
    }

    /**
     * Takes a full page screenshot with all content visible.
     * @param filename The filename without path (will be saved to target/screenshots/)
     */
    public void takeFullPageScreenshot(String filename) {
        try {
            // First expand all collapsible sections
            expandAllCollapsibleBoxes();

            // Wait a moment for any animations to complete
            page.waitForTimeout(500);

            // Take full page screenshot
            String path = "target/screenshots/" + filename + ".png";
            page.screenshot(new Page.ScreenshotOptions()
                .setPath(Paths.get(path))
                .setFullPage(true));

            logger.info(() -> "Full page screenshot saved: " + path);
        } catch (Exception e) {
            logger.warning("Could not take full page screenshot: " + e.getMessage());
        }
    }

    /**
     * Reads all result values from the page.
     * Based on Report.vue structure with .report-content-row elements.
     */
    public Map<String, Object> readResults() {
        waitForResults();

        // Expand all collapsible boxes to ensure all content is visible
        expandAllCollapsibleBoxes();

        Map<String, Object> results = new HashMap<>();

        // Read values from the "Summary" section (first 3-col grid)
        // Structure: <div class="report-content-row"><div>Label</div><div class="report-content-data-cell">Value €</div>...</div>
        results.put("mekA", readValueByLabel("MEK A"));
        results.put("logisticCost", readValueByLabel("Logistics cost"));
        results.put("mekB", readValueByLabel("MEK B"));

        // Read values from the "Weighted cost breakdown" section
        results.put("fcaFee", readValueByLabel("FCA fee"));
        results.put("transportation", readValueByLabel("Transportation costs"));
        results.put("d2d", readValueByLabel("Door 2 door costs"));
        results.put("airFreight", readValueByLabel("Air freight costs"));
        results.put("custom", readValueByLabel("Custom costs"));
        results.put("repackaging", readValueByLabel("Repackaging"));
        results.put("handling", readValueByLabel("Handling"));
        results.put("disposal", readValueByLabel("Disposal costs"));
        results.put("space", readValueByLabel("Space costs"));
        results.put("capital", readValueByLabel("Capital costs"));

        // Read safety stock from material section
        results.put("safetyStock", readIntValueByLabel("Safety stock"));

        // Read destination results
        results.put("destinations", readDestinationResults());

        return results;
    }

    /**
     * Reads a numeric value by finding the label in a report-content-row.
     * The structure is: label | value | percentage
     */
    private Double readValueByLabel(String label) {
        try {
            // Find the row containing the label, then get the first data cell
            String xpath = String.format(
                "//div[contains(@class, 'report-content-row')]/div[contains(text(), '%s')]/following-sibling::div[contains(@class, 'report-content-data-cell')][1]",
                label
            );
            Locator locator = page.locator("xpath=" + xpath).first();

            if (locator.count() == 0) {
                // Try alternative: text might be in a child element
                xpath = String.format(
                    "//div[contains(@class, 'report-content-row')]/div[contains(., '%s')]/following-sibling::div[contains(@class, 'report-content-data-cell')][1]",
                    label
                );
                locator = page.locator("xpath=" + xpath).first();
            }

            if (locator.count() == 0) {
                logger.warning(() -> "Field not found by label: " + label);
                return null;
            }

            String text = locator.textContent();
            if (text == null || text.isEmpty()) {
                return null;
            }

            // Remove currency symbols, percentage signs, and formatting
            // Handle German number format (1.234,56) vs English (1,234.56)
            text = text.replaceAll("[€$%\\s]", "").trim();

            // If contains comma as decimal separator (German format)
            if (text.contains(",") && !text.contains(".")) {
                text = text.replace(",", ".");
            } else if (text.contains(",") && text.contains(".")) {
                // 1.234,56 format - remove thousands separator, replace decimal
                text = text.replace(".", "").replace(",", ".");
            }

            return Double.parseDouble(text);
        } catch (Exception e) {
            logger.warning(() -> "Could not read numeric value for label: " + label + " - " + e.getMessage());
            return null;
        }
    }

    /**
     * Reads integer value by label.
     */
    private Integer readIntValueByLabel(String label) {
        Double value = readValueByLabel(label);
        return value != null ? value.intValue() : null;
    }

    /**
     * Reads results for all destinations from the report.
     * Destinations are in collapsible boxes with class containing destination info.
     */
    private List<Map<String, Object>> readDestinationResults() {
        List<Map<String, Object>> destinations = new ArrayList<>();

        try {
            // Each destination is in a collapsible-box with the destination name as title
            // Look for boxes that have destination-related content
            Locator destinationBoxes = page.locator(".box-gap:has(.report-content-container--2-col)");
            int count = destinationBoxes.count();

            logger.info(() -> "Found " + count + " potential destination boxes");

            // Skip the first few boxes which are summary, cost breakdown, and material sections
            // Destinations start after those
            for (int i = 0; i < count; i++) {
                Locator box = destinationBoxes.nth(i);

                // Check if this box has destination-specific content (Transit time [days], Container type)
                if (box.locator("div:has-text('Transit time')").count() > 0) {
                    Map<String, Object> destResult = new HashMap<>();

                    destResult.put("transitTime", readValueInBox(box, "Transit time [days]"));
                    destResult.put("stackedLayers", readValueInBox(box, "Stacked layers"));
                    destResult.put("containerUnitCount", readValueInBox(box, "Container unit count"));
                    destResult.put("containerType", readStringInBox(box, "Container type"));
                    destResult.put("limitingFactor", readStringInBox(box, "Limiting factor"));

                    destinations.add(destResult);
                    final int destCount = destinations.size();
                    logger.info(() -> "Read destination " + destCount + " results: " + destResult);
                }
            }
        } catch (Exception e) {
            logger.warning("Could not read destination results: " + e.getMessage());
        }

        return destinations;
    }

    private Double readValueInBox(Locator box, String label) {
        try {
            // Try exact text match first, then contains match
            Locator cell = box.locator(".report-content-row")
                .filter(new Locator.FilterOptions().setHasText(label))
                .locator(".report-content-data-cell")
                .first();

            if (cell.count() == 0) {
                logger.warning(() -> "Could not find cell for label: " + label);
                return null;
            }

            String text = cell.textContent().replaceAll("[^0-9.,\\-]", "").trim();
            final String logText = text;
            logger.info(() -> "Read value for '" + label + "': " + logText);

            if (text.isEmpty() || text.equals("-")) {
                return null;
            }
            // Handle German decimal format
            if (text.contains(",") && !text.contains(".")) {
                text = text.replace(",", ".");
            }
            return Double.parseDouble(text);
        } catch (Exception e) {
            logger.warning(() -> "Error reading value for label '" + label + "': " + e.getMessage());
            return null;
        }
    }

    private String readStringInBox(Locator box, String label) {
        try {
            Locator cell = box.locator(".report-content-row")
                .filter(new Locator.FilterOptions().setHasText(label))
                .locator(".report-content-data-cell")
                .first();

            if (cell.count() == 0) {
                logger.warning(() -> "Could not find string cell for label: " + label);
                return null;
            }

            String text = cell.textContent().trim();
            logger.info(() -> "Read string for '" + label + "': " + text);
            return text;
        } catch (Exception e) {
            logger.warning(() -> "Error reading string for label '" + label + "': " + e.getMessage());
            return null;
        }
    }

    /**
     * Verifies that results match expected values.
     */
    public void verifyResults(TestCaseExpected expected, double tolerance) {
        Map<String, Object> actual = readResults();

        // Log all actual values for debugging
        logger.info("======== ACTUAL VALUES FROM REPORT ========");
        logger.info(() -> "MEK A: " + actual.get("mekA"));
        logger.info(() -> "Logistics cost: " + actual.get("logisticCost"));
        logger.info(() -> "MEK B: " + actual.get("mekB"));
        logger.info(() -> "FCA fee: " + actual.get("fcaFee"));
        logger.info(() -> "Transportation: " + actual.get("transportation"));
        logger.info(() -> "D2D: " + actual.get("d2d"));
        logger.info(() -> "Air freight: " + actual.get("airFreight"));
        logger.info(() -> "Custom: " + actual.get("custom"));
        logger.info(() -> "Repackaging: " + actual.get("repackaging"));
        logger.info(() -> "Handling: " + actual.get("handling"));
        logger.info(() -> "Disposal: " + actual.get("disposal"));
        logger.info(() -> "Space: " + actual.get("space"));
        logger.info(() -> "Capital: " + actual.get("capital"));
        logger.info(() -> "Safety stock: " + actual.get("safetyStock"));
        logger.info("======== EXPECTED VALUES ========");
        logger.info(() -> "MEK A: " + expected.mekA());
        logger.info(() -> "Logistics cost: " + expected.logisticCost());
        logger.info(() -> "MEK B: " + expected.mekB());
        logger.info(() -> "FCA fee: " + expected.fcaFee());
        logger.info(() -> "Transportation: " + expected.transportation());
        logger.info(() -> "D2D: " + expected.d2d());
        logger.info(() -> "Air freight: " + expected.airFreight());
        logger.info(() -> "Custom: " + expected.custom());
        logger.info(() -> "Repackaging: " + expected.repackaging());
        logger.info(() -> "Handling: " + expected.handling());
        logger.info(() -> "Disposal: " + expected.disposal());
        logger.info(() -> "Space: " + expected.space());
        logger.info(() -> "Capital: " + expected.capital());
        logger.info(() -> "Safety stock: " + expected.safetyStock());
        logger.info("============================================");

        verifyNumericResult("MEK_A", expected.mekA(), (Double) actual.get("mekA"), tolerance);
        verifyNumericResult("LOGISTIC_COST", expected.logisticCost(), (Double) actual.get("logisticCost"), tolerance);
        verifyNumericResult("MEK_B", expected.mekB(), (Double) actual.get("mekB"), tolerance);
        verifyNumericResult("FCA_FEE", expected.fcaFee(), (Double) actual.get("fcaFee"), tolerance);
        verifyNumericResult("TRANSPORTATION", expected.transportation(), (Double) actual.get("transportation"), tolerance);
        verifyNumericResult("D2D", expected.d2d(), (Double) actual.get("d2d"), tolerance);
        verifyNumericResult("AIR_FREIGHT", expected.airFreight(), (Double) actual.get("airFreight"), tolerance);
        verifyNumericResult("CUSTOM", expected.custom(), (Double) actual.get("custom"), tolerance);
        verifyNumericResult("REPACKAGING", expected.repackaging(), (Double) actual.get("repackaging"), tolerance);
        verifyNumericResult("HANDLING", expected.handling(), (Double) actual.get("handling"), tolerance);
        verifyNumericResult("DISPOSAL", expected.disposal(), (Double) actual.get("disposal"), tolerance);
        verifyNumericResult("SPACE", expected.space(), (Double) actual.get("space"), tolerance);
        verifyNumericResult("CAPITAL", expected.capital(), (Double) actual.get("capital"), tolerance);

        // Verify destinations
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> actualDestinations = (List<Map<String, Object>>) actual.get("destinations");
        List<DestinationExpected> expectedDestinations = expected.destinations();

        if (expectedDestinations.size() != actualDestinations.size()) {
            throw new AssertionError(String.format(
                "Destination count mismatch: expected %d, got %d",
                expectedDestinations.size(), actualDestinations.size()
            ));
        }

        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) + " ";

            // Verify transit time (always expected to have a value)
            if (expDest.transitTime() != null) {
                verifyNumericResult(prefix + "TRANSIT_TIME",
                    expDest.transitTime().doubleValue(),
                    (Double) actDest.get("transitTime"), tolerance);
            }

            // Verify stacked layers (null expected = "-" in UI)
            verifyNullableNumericResult(prefix + "STACKED_LAYERS",
                expDest.stackedLayers(),
                (Double) actDest.get("stackedLayers"), tolerance);

            // Verify container unit count (null expected = "-" in UI)
            verifyNullableNumericResult(prefix + "CONTAINER_UNIT_COUNT",
                expDest.containerUnitCount(),
                (Double) actDest.get("containerUnitCount"), tolerance);

            // Verify container type (null or "-" expected = "-" in UI)
            String expContainerType = expDest.containerType();
            String actContainerType = (String) actDest.get("containerType");
            verifyStringResult(prefix + "CONTAINER_TYPE", expContainerType, actContainerType);

            // Verify limiting factor (null or "-" expected = "-" in UI)
            String expLimitingFactor = expDest.limitingFactor();
            String actLimitingFactor = (String) actDest.get("limitingFactor");
            verifyStringResult(prefix + "LIMITING_FACTOR", expLimitingFactor, actLimitingFactor);
        }

        logger.info("All results verified successfully");
    }

    private void verifyNumericResult(String fieldName, double expected, Double actual, double tolerance) {
        // If actual is null and expected is effectively zero, treat as pass
        // (some fields may not be displayed in the UI when their value is 0)
        if (actual == null) {
            if (Math.abs(expected) < 0.001) {
                logger.info(() -> "Field '" + fieldName + "': actual is null, expected ~0 - treating as pass");
                return;
            }
            throw new AssertionError(String.format(
                "Field '%s': actual value is null, expected %f",
                fieldName, expected
            ));
        }

        double diff = Math.abs(expected - actual);
        double relativeDiff = expected != 0 ? diff / Math.abs(expected) : diff;

        if (relativeDiff > tolerance) {
            throw new AssertionError(String.format(
                "Field '%s': expected %f, got %f (diff: %.4f, tolerance: %.2f%%)",
                fieldName, expected, actual, relativeDiff * 100, tolerance * 100
            ));
        }
    }

    /**
     * Verifies a nullable numeric result. If expected is null, actual should also be null.
     */
    private void verifyNullableNumericResult(String fieldName, Integer expected, Double actual, double tolerance) {
        if (expected == null) {
            // Expected null means UI shows "-"
            if (actual != null) {
                throw new AssertionError(String.format(
                    "Field '%s': expected null (UI shows '-'), got %f",
                    fieldName, actual
                ));
            }
            return;
        }

        // Expected has a value, verify it
        verifyNumericResult(fieldName, expected.doubleValue(), actual, tolerance);
    }

    /**
     * Verifies a string result. Handles null/"-" as equivalent.
     */
    private void verifyStringResult(String fieldName, String expected, String actual) {
        // Normalize "-" to null for comparison
        String normExpected = (expected == null || "-".equals(expected)) ? null : expected;
        String normActual = (actual == null || "-".equals(actual)) ? null : actual;

        if (normExpected == null && normActual == null) {
            return; // Both null/"-" = match
        }

        if (normExpected == null || normActual == null || !normExpected.equals(normActual)) {
            throw new AssertionError(String.format(
                "Field '%s': expected '%s', got '%s'",
                fieldName, expected, actual
            ));
        }
    }
}
