package de.votian.web.view;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.H4;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.tabs.Tab;
import com.vaadin.flow.component.tabs.Tabs;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.votian.web.service.RestClientService;
import org.springframework.core.ParameterizedTypeReference;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Route(value = "warehouse", layout = MainLayout.class)
@PageTitle("Votian - Lager")
public class WarehouseView extends VerticalLayout {

    private final RestClientService restClient;

    private final ComboBox<RootOption> rootStockField = new ComboBox<>("Hauptlager");
    private final Span selectedStockText = new Span("-");
    private final Span selectedCapacityText = new Span("-");
    private final Span accessText = new Span("-");

    private final Checkbox includeSubstocksField = new Checkbox("Unterlager einbeziehen", true);
    private final TextField inventorySearchField = new TextField("Bestandssuche");

    private final Grid<StockRow> stockGrid = new Grid<>(StockRow.class, false);
    private final Grid<InventoryRow> inventoryGrid = new Grid<>(InventoryRow.class, false);
    private final Grid<SerialRow> serialGrid = new Grid<>(SerialRow.class, false);
    private final Grid<JournalRow> journalGrid = new Grid<>(JournalRow.class, false);

    private final ComboBox<ArticleOption> movementArticleField = new ComboBox<>("Artikel");
    private final ComboBox<MovementStockOption> movementSourceField = new ComboBox<>("Quelle");
    private final ComboBox<MovementStockOption> movementTargetField = new ComboBox<>("Ziel");
    private final IntegerField movementQuantityField = new IntegerField("Menge");
    private final TextField movementSerialField = new TextField("Seriennummer");
    private final TextField movementTanField = new TextField("TAN");
    private final TextField movementJobField = new TextField("Auftrag");
    private final TextField movementRemarkField = new TextField("Bemerkung");
    private final Button moveButton = new Button("Buchen");

    private final DatePicker journalFromField = new DatePicker("Von");
    private final DatePicker journalToField = new DatePicker("Bis");
    private final ComboBox<MovementStockOption> journalSourceField = new ComboBox<>("Quelle");
    private final ComboBox<MovementStockOption> journalTargetField = new ComboBox<>("Ziel");
    private final TextField journalSearchField = new TextField("Journalfilter");

    private final List<RootOption> rootOptions = new ArrayList<>();
    private final List<StockRow> stockRows = new ArrayList<>();
    private final List<ArticleOption> articleOptions = new ArrayList<>();

    private boolean canMove;
    private StockRow selectedStock;
    private InventoryRow selectedInventory;
    private SerialRow selectedSerial;

    public WarehouseView(RestClientService restClient) {
        this.restClient = restClient;

        setSizeFull();
        setPadding(true);
        setSpacing(true);

        configureSelectors();
        configureGrids();
        configureMovementForm();
        configureJournalFilters();

        VerticalLayout inventoryContent = buildInventoryContent();
        VerticalLayout movementContent = buildMovementContent();
        VerticalLayout journalContent = buildJournalContent();
        movementContent.setVisible(false);
        journalContent.setVisible(false);

        Tab inventoryTab = new Tab("Bestand");
        Tab movementTab = new Tab("Bewegung");
        Tab journalTab = new Tab("Journal");
        Tabs tabs = new Tabs(inventoryTab, movementTab, journalTab);
        Map<Tab, Component> tabContent = Map.of(
                inventoryTab, inventoryContent,
                movementTab, movementContent,
                journalTab, journalContent
        );
        tabs.addSelectedChangeListener(event -> {
            for (Component content : tabContent.values()) {
                content.setVisible(false);
            }
            tabContent.get(event.getSelectedTab()).setVisible(true);
        });

        add(new H3("Lager"), buildTopBar(), buildSummary(), tabs, inventoryContent, movementContent, journalContent);
        expand(inventoryContent, movementContent, journalContent);

        loadBootstrap();
    }

    private void configureSelectors() {
        rootStockField.setItemLabelGenerator(RootOption::label);
        rootStockField.addValueChangeListener(event -> {
            if (event.getValue() != null && !event.isFromClient()) {
                return;
            }
            if (event.getValue() != null) {
                loadRootWorkspace();
            } else {
                clearWorkspace();
            }
        });
        inventorySearchField.setClearButtonVisible(true);
        journalSearchField.setClearButtonVisible(true);
    }

    private void configureGrids() {
        stockGrid.addColumn(StockRow::indentedName).setHeader("Lagerort").setFlexGrow(1);
        stockGrid.addColumn(StockRow::capacityLabel).setHeader("Kapazitaet").setAutoWidth(true);
        stockGrid.addColumn(row -> row.writeAllowed() ? "Ja" : "Nein").setHeader("Buchbar").setAutoWidth(true);
        stockGrid.addColumn(StockRow::addressLabel).setHeader("Adresse").setFlexGrow(1);
        stockGrid.setWidth("420px");
        stockGrid.setHeightFull();
        stockGrid.asSingleSelect().addValueChangeListener(event -> {
            selectedStock = event.getValue();
            selectedInventory = null;
            selectedSerial = null;
            updateSummary();
            refreshMovementStockDefaults();
            loadInventory();
            loadJournal();
        });

        inventoryGrid.addColumn(InventoryRow::articleCode).setHeader("Artikel").setAutoWidth(true).setFrozen(true);
        inventoryGrid.addColumn(InventoryRow::name).setHeader("Bezeichnung").setFlexGrow(1);
        inventoryGrid.addColumn(InventoryRow::match).setHeader("Zusatz").setFlexGrow(1);
        inventoryGrid.addColumn(InventoryRow::itemQuantity).setHeader("Menge").setAutoWidth(true);
        inventoryGrid.addColumn(InventoryRow::areaQuantity).setHeader("Plaetze").setAutoWidth(true);
        inventoryGrid.addColumn(InventoryRow::stockCount).setHeader("Lagerorte").setAutoWidth(true);
        inventoryGrid.addColumn(row -> row.serialTracked() ? "Ja" : "Nein").setHeader("Seriennr.").setAutoWidth(true);
        inventoryGrid.setSizeFull();
        inventoryGrid.asSingleSelect().addValueChangeListener(event -> {
            selectedInventory = event.getValue();
            if (selectedInventory != null) {
                selectMovementArticle(selectedInventory.articleId());
                movementQuantityField.setValue(selectedInventory.serialTracked() ? 1 : movementQuantityField.getValue());
                if (selectedStock != null && selectedStock.writeAllowed()) {
                    movementSourceField.setValue(findMovementSourceOption(selectedStock.id()));
                }
            }
            loadSerials();
        });

        serialGrid.addColumn(SerialRow::stockName).setHeader("Lagerort").setFlexGrow(1);
        serialGrid.addColumn(SerialRow::serialNo).setHeader("Seriennummer").setAutoWidth(true);
        serialGrid.addColumn(SerialRow::dataPreview).setHeader("Daten").setFlexGrow(1);
        serialGrid.setWidth("420px");
        serialGrid.setHeightFull();
        serialGrid.asSingleSelect().addValueChangeListener(event -> {
            selectedSerial = event.getValue();
            if (selectedSerial != null) {
                movementSerialField.setValue(selectedSerial.serialNo());
                movementQuantityField.setValue(1);
                movementSourceField.setValue(findMovementSourceOption(selectedSerial.stockId()));
            }
        });

        journalGrid.addColumn(JournalRow::bookingTime).setHeader("Zeit").setAutoWidth(true).setFrozen(true);
        journalGrid.addColumn(JournalRow::sourceStock).setHeader("Quelle").setFlexGrow(1);
        journalGrid.addColumn(JournalRow::targetStock).setHeader("Ziel").setFlexGrow(1);
        journalGrid.addColumn(JournalRow::articleCode).setHeader("Artikel").setAutoWidth(true);
        journalGrid.addColumn(JournalRow::articleName).setHeader("Bezeichnung").setFlexGrow(1);
        journalGrid.addColumn(JournalRow::itemQuantity).setHeader("Menge").setAutoWidth(true);
        journalGrid.addColumn(JournalRow::serialNo).setHeader("Seriennummer").setAutoWidth(true);
        journalGrid.addColumn(JournalRow::tan).setHeader("TAN").setAutoWidth(true);
        journalGrid.addColumn(JournalRow::remark).setHeader("Bemerkung").setFlexGrow(1);
        journalGrid.addColumn(row -> row.jobId() != null && row.jobId() > 0 ? row.jobId() : null).setHeader("Auftrag").setAutoWidth(true);
        journalGrid.setSizeFull();
    }

    private void configureMovementForm() {
        movementArticleField.setItemLabelGenerator(ArticleOption::label);
        movementSourceField.setItemLabelGenerator(MovementStockOption::label);
        movementTargetField.setItemLabelGenerator(MovementStockOption::label);
        movementQuantityField.setMin(1);
        movementQuantityField.setValue(1);
        movementArticleField.addValueChangeListener(event -> updateMovementSerialState());
        moveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        moveButton.addClickListener(event -> moveArticle());
    }

    private void configureJournalFilters() {
        journalFromField.setValue(LocalDate.now().minusDays(7));
        journalToField.setValue(LocalDate.now());
        journalSourceField.setItemLabelGenerator(MovementStockOption::label);
        journalTargetField.setItemLabelGenerator(MovementStockOption::label);
    }

    private HorizontalLayout buildTopBar() {
        Button reloadButton = new Button("Neu laden", event -> loadRootWorkspace());
        reloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        HorizontalLayout bar = new HorizontalLayout(rootStockField, reloadButton);
        bar.setAlignItems(Alignment.BASELINE);
        return bar;
    }

    private HorizontalLayout buildSummary() {
        HorizontalLayout layout = new HorizontalLayout(
                detailLine("Lagerort", selectedStockText),
                detailLine("Kapazitaet", selectedCapacityText),
                detailLine("Zugriff", accessText)
        );
        layout.setWidthFull();
        layout.setAlignItems(Alignment.BASELINE);
        layout.getStyle().set("flex-wrap", "wrap");
        return layout;
    }

    private VerticalLayout buildInventoryContent() {
        Button loadInventoryButton = new Button("Bestand laden", event -> loadInventory());
        HorizontalLayout filterBar = new HorizontalLayout(includeSubstocksField, inventorySearchField, loadInventoryButton);
        filterBar.setAlignItems(Alignment.BASELINE);

        VerticalLayout stockPanel = new VerticalLayout(new H4("Lagerbaum"), stockGrid);
        stockPanel.setPadding(false);
        stockPanel.setSpacing(true);
        stockPanel.setWidth("420px");
        stockPanel.setHeightFull();
        stockPanel.expand(stockGrid);

        VerticalLayout inventoryPanel = new VerticalLayout(new H4("Bestand"), filterBar, inventoryGrid);
        inventoryPanel.setPadding(false);
        inventoryPanel.setSpacing(true);
        inventoryPanel.setSizeFull();
        inventoryPanel.expand(inventoryGrid);

        VerticalLayout serialPanel = new VerticalLayout(new H4("Seriennummern"), serialGrid);
        serialPanel.setPadding(false);
        serialPanel.setSpacing(true);
        serialPanel.setWidth("420px");
        serialPanel.setHeightFull();
        serialPanel.expand(serialGrid);

        HorizontalLayout workspace = new HorizontalLayout(stockPanel, inventoryPanel, serialPanel);
        workspace.setSizeFull();
        workspace.expand(inventoryPanel);

        VerticalLayout content = new VerticalLayout(workspace);
        content.setPadding(false);
        content.setSpacing(false);
        content.setSizeFull();
        content.expand(workspace);
        return content;
    }

    private VerticalLayout buildMovementContent() {
        FormLayout form = new FormLayout();
        form.setResponsiveSteps(
                new FormLayout.ResponsiveStep("0", 1),
                new FormLayout.ResponsiveStep("720px", 2)
        );
        form.add(movementArticleField, movementQuantityField, movementSourceField, movementTargetField,
                movementSerialField, movementTanField, movementJobField, movementRemarkField);

        Paragraph hint = new Paragraph("Quelle `Eingangslieferung` bucht Ware ins Lager. Ziel `Ausgangslager` bucht Ware aus dem Lagerbestand aus.");
        hint.getStyle().set("margin", "0");

        VerticalLayout content = new VerticalLayout(new H4("Artikelbewegung"), hint, form, moveButton);
        content.setPadding(false);
        content.setSpacing(true);
        content.setWidthFull();
        return content;
    }

    private VerticalLayout buildJournalContent() {
        Button loadJournalButton = new Button("Journal laden", event -> loadJournal());
        HorizontalLayout filters = new HorizontalLayout(journalFromField, journalToField, journalSourceField, journalTargetField,
                journalSearchField, loadJournalButton);
        filters.setAlignItems(Alignment.BASELINE);
        filters.getStyle().set("flex-wrap", "wrap");

        VerticalLayout content = new VerticalLayout(new H4("Lagerjournal"), filters, journalGrid);
        content.setPadding(false);
        content.setSpacing(true);
        content.setSizeFull();
        content.expand(journalGrid);
        return content;
    }

    private HorizontalLayout detailLine(String label, Span value) {
        Span title = new Span(label + ":");
        title.getStyle().set("font-weight", "600");
        return new HorizontalLayout(title, value);
    }

    private void loadBootstrap() {
        try {
            Map<String, Object> data = restClient.getMap("/warehouse/bootstrap");
            rootOptions.clear();
            canMove = bool(data.get("canMove"));

            for (Object row : asList(data.get("roots"))) {
                if (row instanceof Map<?, ?> map) {
                    Long id = toLong(map.get("id"));
                    if (id != null) {
                        rootOptions.add(new RootOption(id, str(map.get("name"))));
                    }
                }
            }
            rootStockField.setItems(rootOptions);
            moveButton.setEnabled(canMove);

            Long defaultRootStockId = toLong(data.get("defaultRootStockId"));
            RootOption defaultRoot = rootOptions.stream()
                    .filter(option -> option.id().equals(defaultRootStockId))
                    .findFirst()
                    .orElse(rootOptions.isEmpty() ? null : rootOptions.get(0));

            if (defaultRoot != null) {
                rootStockField.setValue(defaultRoot);
                loadRootWorkspace();
            } else {
                clearWorkspace();
            }
        } catch (Exception e) {
            showError("Lagermodul konnte nicht geladen werden: " + e.getMessage());
        }
    }

    private void loadRootWorkspace() {
        RootOption root = rootStockField.getValue();
        if (root == null) {
            clearWorkspace();
            return;
        }

        try {
            loadStocks(root.id());
            loadArticleOptions(root.id());
            refreshStockSelectors();
            selectInitialStock(root.id());
            loadInventory();
            loadJournal();
        } catch (Exception e) {
            showError("Lagerdaten konnten nicht geladen werden: " + e.getMessage());
        }
    }

    private void loadStocks(Long rootStockId) {
        List<LinkedHashMap<String, Object>> data = restClient.getList(
                "/warehouse/stocks?rootStockId=" + rootStockId,
                new ParameterizedTypeReference<>() {}
        );
        stockRows.clear();
        for (Map<String, Object> row : data) {
            stockRows.add(new StockRow(
                    toLong(row.get("id")),
                    toLong(row.get("parentId")),
                    toInt(row.get("level")),
                    str(row.get("name")),
                    bool(row.get("planning")),
                    toInt(row.get("maxQuantity")),
                    toInt(row.get("usedCapacity")),
                    toInt(row.get("reservedCapacity")),
                    toInt(row.get("freeCapacity")),
                    str(row.get("barcode")),
                    str(row.get("addressLabel")),
                    bool(row.get("writeAllowed"))
            ));
        }
        stockGrid.setItems(stockRows);
    }

    private void loadArticleOptions(Long rootStockId) {
        List<LinkedHashMap<String, Object>> data = restClient.getList(
                "/warehouse/articles?rootStockId=" + rootStockId,
                new ParameterizedTypeReference<>() {}
        );
        articleOptions.clear();
        for (Map<String, Object> row : data) {
            Long articleId = toLong(row.get("articleId"));
            if (articleId != null) {
                articleOptions.add(new ArticleOption(
                        articleId,
                        str(row.get("articleCode")),
                        str(row.get("name")),
                        str(row.get("match")),
                        bool(row.get("serialTracked"))
                ));
            }
        }
        movementArticleField.setItems(articleOptions);
    }

    private void refreshStockSelectors() {
        List<MovementStockOption> writableStocks = stockRows.stream()
                .filter(StockRow::writeAllowed)
                .map(row -> new MovementStockOption(row.id(), row.indentedName(), true))
                .toList();

        List<MovementStockOption> movementSourceOptions = new ArrayList<>();
        movementSourceOptions.add(new MovementStockOption(0L, "Eingangslieferung", canMove));
        movementSourceOptions.addAll(writableStocks);

        List<MovementStockOption> movementTargetOptions = new ArrayList<>(writableStocks);
        movementTargetOptions.add(new MovementStockOption(0L, "Ausgangslager", canMove));

        movementSourceField.setItems(movementSourceOptions);
        movementTargetField.setItems(movementTargetOptions);
        journalSourceField.setItems(stockRows.stream()
                .map(row -> new MovementStockOption(row.id(), row.indentedName(), row.writeAllowed()))
                .toList());
        journalTargetField.setItems(stockRows.stream()
                .map(row -> new MovementStockOption(row.id(), row.indentedName(), row.writeAllowed()))
                .toList());
    }

    private void selectInitialStock(Long rootStockId) {
        StockRow preferred = selectedStock != null
                ? stockRows.stream().filter(row -> row.id().equals(selectedStock.id())).findFirst().orElse(null)
                : null;
        if (preferred == null) {
            preferred = stockRows.stream().filter(row -> row.id().equals(rootStockId)).findFirst().orElse(null);
        }
        if (preferred == null && !stockRows.isEmpty()) {
            preferred = stockRows.get(0);
        }
        selectedStock = preferred;
        if (preferred != null) {
            stockGrid.select(preferred);
        } else {
            stockGrid.deselectAll();
            updateSummary();
        }
    }

    private void loadInventory() {
        if (selectedStock == null) {
            inventoryGrid.setItems(List.of());
            serialGrid.setItems(List.of());
            return;
        }

        try {
            List<LinkedHashMap<String, Object>> data = restClient.getList(
                    "/warehouse/inventory?stockId=" + selectedStock.id()
                            + "&includeSubstocks=" + includeSubstocksField.getValue()
                            + "&search=" + encode(inventorySearchField.getValue()),
                    new ParameterizedTypeReference<>() {}
            );
            List<InventoryRow> rows = new ArrayList<>();
            for (Map<String, Object> row : data) {
                rows.add(new InventoryRow(
                        toLong(row.get("articleId")),
                        str(row.get("articleCode")),
                        str(row.get("name")),
                        str(row.get("match")),
                        str(row.get("barcode")),
                        bool(row.get("serialTracked")),
                        toInt(row.get("itemQuantity")),
                        toInt(row.get("areaQuantity")),
                        toInt(row.get("stockCount"))
                ));
            }
            inventoryGrid.setItems(rows);
            InventoryRow updatedSelection = selectedInventory != null
                    ? rows.stream().filter(row -> row.articleId().equals(selectedInventory.articleId())).findFirst().orElse(null)
                    : null;
            selectedInventory = updatedSelection;
            if (updatedSelection != null) {
                inventoryGrid.select(updatedSelection);
            } else {
                inventoryGrid.deselectAll();
                loadSerials();
            }
        } catch (Exception e) {
            showError("Bestand konnte nicht geladen werden: " + e.getMessage());
        }
    }

    private void loadSerials() {
        if (selectedStock == null || selectedInventory == null || !selectedInventory.serialTracked()) {
            serialGrid.setItems(List.of());
            return;
        }

        try {
            List<LinkedHashMap<String, Object>> data = restClient.getList(
                    "/warehouse/serials?stockId=" + selectedStock.id()
                            + "&includeSubstocks=" + includeSubstocksField.getValue()
                            + "&articleId=" + selectedInventory.articleId(),
                    new ParameterizedTypeReference<>() {}
            );
            List<SerialRow> rows = new ArrayList<>();
            for (Map<String, Object> row : data) {
                rows.add(new SerialRow(
                        toLong(row.get("id")),
                        toLong(row.get("stockId")),
                        str(row.get("stockName")),
                        str(row.get("serialNo")),
                        extractStringList(row.get("dataFields"))
                ));
            }
            serialGrid.setItems(rows);
        } catch (Exception e) {
            showError("Seriennummern konnten nicht geladen werden: " + e.getMessage());
        }
    }

    private void loadJournal() {
        RootOption root = rootStockField.getValue();
        if (root == null) {
            journalGrid.setItems(List.of());
            return;
        }

        try {
            String path = "/warehouse/journal?rootStockId=" + root.id()
                    + optionalLongParam("stockFromId", journalSourceField.getValue() != null ? journalSourceField.getValue().stockId() : null)
                    + optionalLongParam("stockToId", journalTargetField.getValue() != null ? journalTargetField.getValue().stockId() : null)
                    + "&from=" + journalFromField.getValue()
                    + "&to=" + journalToField.getValue()
                    + "&search=" + encode(journalSearchField.getValue());

            List<LinkedHashMap<String, Object>> data = restClient.getList(path, new ParameterizedTypeReference<>() {});
            List<JournalRow> rows = new ArrayList<>();
            for (Map<String, Object> row : data) {
                rows.add(new JournalRow(
                        toLong(row.get("id")),
                        str(row.get("bookingTime")),
                        str(row.get("sourceStock")),
                        str(row.get("targetStock")),
                        str(row.get("articleCode")),
                        str(row.get("articleName")),
                        toInt(row.get("itemQuantity")),
                        str(row.get("serialNo")),
                        str(row.get("tan")),
                        str(row.get("remark")),
                        toLong(row.get("jobId"))
                ));
            }
            journalGrid.setItems(rows);
        } catch (Exception e) {
            showError("Journal konnte nicht geladen werden: " + e.getMessage());
        }
    }

    private void moveArticle() {
        if (!canMove) {
            showError("Lagerbuchungen sind fuer den angemeldeten Benutzer gesperrt.");
            return;
        }
        if (movementArticleField.getValue() == null || movementSourceField.getValue() == null || movementTargetField.getValue() == null) {
            showError("Bitte Artikel, Quelle und Ziel waehlen.");
            return;
        }
        if (movementArticleField.getValue().serialTracked() && movementSerialField.getValue().isBlank()) {
            showError("Fuer diesen Artikel ist eine Seriennummer erforderlich.");
            return;
        }

        try {
            Map<String, Object> request = new LinkedHashMap<>();
            request.put("articleId", movementArticleField.getValue().articleId());
            request.put("sourceStockId", movementSourceField.getValue().stockId());
            request.put("targetStockId", movementTargetField.getValue().stockId());
            request.put("itemQuantity", movementQuantityField.getValue());
            request.put("serialNo", movementSerialField.getValue());
            request.put("tan", movementTanField.getValue());
            request.put("remark", movementRemarkField.getValue());
            request.put("jobId", parseLongOrNull(movementJobField.getValue()));
            restClient.post("/warehouse/moves", request, LinkedHashMap.class);
            Notification.show("Lagerbewegung gespeichert.", 3000, Notification.Position.MIDDLE)
                    .addThemeVariants(NotificationVariant.LUMO_SUCCESS);

            movementRemarkField.clear();
            movementTanField.clear();
            movementSerialField.clear();
            movementJobField.clear();
            movementQuantityField.setValue(1);

            loadRootWorkspace();
        } catch (Exception e) {
            showError("Lagerbewegung konnte nicht gespeichert werden: " + e.getMessage());
        }
    }

    private void updateSummary() {
        if (selectedStock == null) {
            selectedStockText.setText("-");
            selectedCapacityText.setText("-");
            accessText.setText(canMove ? "Buchbar" : "Nur Lesen");
            return;
        }
        selectedStockText.setText(selectedStock.name());
        if (selectedStock.planning()) {
            selectedCapacityText.setText(selectedStock.usedCapacity() + " belegt / "
                    + selectedStock.reservedCapacity() + " reserviert / "
                    + selectedStock.freeCapacity() + " frei");
        } else {
            selectedCapacityText.setText("Kapazitaetsplanung deaktiviert");
        }
        accessText.setText(selectedStock.writeAllowed() && canMove ? "Buchbar" : "Nur Lesen");
    }

    private void updateMovementSerialState() {
        ArticleOption article = movementArticleField.getValue();
        boolean serialTracked = article != null && article.serialTracked();
        movementSerialField.setRequired(serialTracked);
        if (serialTracked && (movementQuantityField.getValue() == null || movementQuantityField.getValue() != 1)) {
            movementQuantityField.setValue(1);
        }
    }

    private void refreshMovementStockDefaults() {
        updateSummary();
        if (selectedStock != null && selectedStock.writeAllowed()) {
            movementSourceField.setValue(findMovementSourceOption(selectedStock.id()));
        } else {
            movementSourceField.clear();
        }
        movementTargetField.clear();
    }

    private void selectMovementArticle(Long articleId) {
        if (articleId == null) {
            return;
        }
        articleOptions.stream()
                .filter(option -> option.articleId().equals(articleId))
                .findFirst()
                .ifPresent(movementArticleField::setValue);
    }

    private MovementStockOption findMovementSourceOption(Long stockId) {
        return movementSourceField.getListDataView().getItems()
                .filter(option -> option.stockId().equals(stockId))
                .findFirst()
                .orElse(null);
    }

    private void clearWorkspace() {
        stockRows.clear();
        articleOptions.clear();
        selectedStock = null;
        selectedInventory = null;
        selectedSerial = null;
        stockGrid.setItems(List.of());
        inventoryGrid.setItems(List.of());
        serialGrid.setItems(List.of());
        journalGrid.setItems(List.of());
        updateSummary();
    }

    private void showError(String message) {
        Notification.show(message, 5000, Notification.Position.MIDDLE)
                .addThemeVariants(NotificationVariant.LUMO_ERROR);
    }

    private String optionalLongParam(String key, Long value) {
        return value != null ? "&" + key + "=" + value : "";
    }

    private String encode(String value) {
        return value != null ? URLEncoder.encode(value, StandardCharsets.UTF_8) : "";
    }

    private List<?> asList(Object value) {
        if (value instanceof List<?> list) {
            return list;
        }
        return List.of();
    }

    private List<String> extractStringList(Object value) {
        List<String> result = new ArrayList<>();
        if (value instanceof Iterable<?> iterable) {
            for (Object item : iterable) {
                result.add(str(item));
            }
        }
        return result;
    }

    private Long parseLongOrNull(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return Long.parseLong(value.trim());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private Long toLong(Object value) {
        if (value instanceof Number number) {
            return number.longValue();
        }
        if (value == null || value.toString().isBlank()) {
            return null;
        }
        try {
            return Long.parseLong(value.toString());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private int toInt(Object value) {
        if (value instanceof Number number) {
            return number.intValue();
        }
        if (value == null || value.toString().isBlank()) {
            return 0;
        }
        try {
            return Integer.parseInt(value.toString());
        } catch (NumberFormatException ignored) {
            return 0;
        }
    }

    private boolean bool(Object value) {
        if (value instanceof Boolean bool) {
            return bool;
        }
        return "true".equalsIgnoreCase(str(value)) || "1".equals(str(value));
    }

    private String str(Object value) {
        return value != null ? value.toString() : "";
    }

    private record RootOption(Long id, String name) {
        private String label() {
            return name + " (" + id + ")";
        }
    }

    private record StockRow(
            Long id,
            Long parentId,
            int level,
            String name,
            boolean planning,
            int maxQuantity,
            int usedCapacity,
            int reservedCapacity,
            int freeCapacity,
            String barcode,
            String addressLabel,
            boolean writeAllowed
    ) {
        private String indentedName() {
            return "  ".repeat(Math.max(0, level)) + name;
        }

        private String capacityLabel() {
            if (!planning) {
                return "-";
            }
            return maxQuantity + " / " + freeCapacity + " frei";
        }
    }

    private record InventoryRow(
            Long articleId,
            String articleCode,
            String name,
            String match,
            String barcode,
            boolean serialTracked,
            int itemQuantity,
            int areaQuantity,
            int stockCount
    ) {
    }

    private record SerialRow(Long id, Long stockId, String stockName, String serialNo, List<String> dataFields) {
        private String dataPreview() {
            return dataFields.stream()
                    .filter(value -> value != null && !value.isBlank())
                    .limit(3)
                    .collect(Collectors.joining(" | "));
        }
    }

    private record JournalRow(
            Long id,
            String bookingTime,
            String sourceStock,
            String targetStock,
            String articleCode,
            String articleName,
            int itemQuantity,
            String serialNo,
            String tan,
            String remark,
            Long jobId
    ) {
    }

    private record ArticleOption(Long articleId, String articleCode, String name, String match, boolean serialTracked) {
        private String label() {
            StringBuilder builder = new StringBuilder();
            if (articleCode != null && !articleCode.isBlank()) {
                builder.append(articleCode).append(" - ");
            }
            builder.append(name);
            if (match != null && !match.isBlank()) {
                builder.append(" (").append(match).append(")");
            }
            if (serialTracked) {
                builder.append(" [SN]");
            }
            return builder.toString();
        }
    }

    private record MovementStockOption(Long stockId, String label, boolean writeAllowed) {
    }
}
