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.combobox.ComboBox;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.Div;
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.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.model.SessionData;
import de.votian.web.service.RestClientService;
import org.springframework.core.ParameterizedTypeReference;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

@Route(value = "geo-operations", layout = MainLayout.class)
@PageTitle("Votian - Karten und Adressen")
public class GeoOperationsWorkspaceView extends VerticalLayout {

    private final RestClientService restClient;
    private final SessionData sessionData;

    private final Paragraph intro = new Paragraph();

    private final Span mapInfo = new Span("-");
    private final Div mapView = new Div();
    private final Button mapReloadButton = new Button("Karte neu laden");
    private final Grid<MapCourierRow> mapGrid = new Grid<>(MapCourierRow.class, false);

    private final TextField addressStreetField = new TextField("Strasse");
    private final TextField addressZipcodeField = new TextField("PLZ");
    private final TextField addressCityField = new TextField("Ort");
    private final TextField addressCountryField = new TextField("Land");
    private final Button addressSearchButton = new Button("Adressen suchen");
    private final Grid<AddressRow> addressGrid = new Grid<>(AddressRow.class, false);
    private final Grid<ValidatedStreetRow> validatedStreetGrid = new Grid<>(ValidatedStreetRow.class, false);
    private final TextField editStreetField = new TextField("Strasse");
    private final TextField editZipcodeField = new TextField("PLZ");
    private final TextField editCityField = new TextField("Ort");
    private final TextField editCountryField = new TextField("Land");
    private final Button saveAddressButton = new Button("Adresse speichern");
    private final TextField validatedStreetField = new TextField("Strasse");
    private final TextField validatedZipcodeField = new TextField("PLZ");
    private final TextField validatedCityField = new TextField("Ort");
    private final TextField validatedCountryField = new TextField("Land");
    private final Button createValidatedButton = new Button("Validierungsadresse anlegen");

    private final ComboBox<SearchTypeOption> similarityTypeField = new ComboBox<>("Bereich");
    private final TextField similarityPhraseField = new TextField("Phrase");
    private final Button similaritySearchButton = new Button("Aehnlich suchen");
    private final Button similarityOpenButton = new Button("Objekt oeffnen");
    private final Grid<SimilarityRow> similarityGrid = new Grid<>(SimilarityRow.class, false);

    private final ComboBox<TravelModeOption> travelModeField = new ComboBox<>("Modus");
    private final TextField travelFromField = new TextField("Von");
    private final TextField travelToField = new TextField("Bis");
    private final Button travelLoadButton = new Button("Liste aufbauen");
    private final Button travelSaveButton = new Button("Anfahrtszeiten speichern");
    private final Span travelInfo = new Span("-");
    private final Grid<MutableTravelTimeRow> travelGrid = new Grid<>(MutableTravelTimeRow.class, false);

    private final List<MapCourierRow> mapRows = new ArrayList<>();
    private final List<AddressRow> addressRows = new ArrayList<>();
    private final List<ValidatedStreetRow> validatedRows = new ArrayList<>();
    private final List<SimilarityRow> similarityRows = new ArrayList<>();
    private final List<MutableTravelTimeRow> travelRows = new ArrayList<>();

    private MapHeadquarters headquarters;
    private MapCourierRow selectedMapCourier;
    private AddressRow selectedAddress;
    private SimilarityRow selectedSimilarity;

    private boolean mapVisible;
    private boolean addressAdminVisible;
    private boolean customerSearchVisible;
    private boolean courierSearchVisible;
    private boolean travelTimeAdminVisible;
    private String defaultCity = "";
    private int defaultTravelTimeMinutes = 30;

    public GeoOperationsWorkspaceView(RestClientService restClient, SessionData sessionData) {
        this.restClient = restClient;
        this.sessionData = sessionData;

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

        configureMap();
        configureAddressAdmin();
        configureSimilaritySearch();
        configureTravelTimes();

        add(new H3("Karten, Adressen und Suche"));
        loadBootstrap();
    }

    private void configureMap() {
        mapReloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        mapReloadButton.addClickListener(event -> loadMap());

        mapView.setWidthFull();
        mapView.setHeight("380px");
        mapView.getStyle()
                .set("border", "1px solid var(--lumo-contrast-10pct)")
                .set("border-radius", "var(--lumo-border-radius-m)")
                .set("overflow", "hidden")
                .set("background", "linear-gradient(180deg, #edf6ff 0%, #f7f9fc 100%)");

        mapGrid.addColumn(MapCourierRow::courierCode).setHeader("EID").setAutoWidth(true).setFrozen(true);
        mapGrid.addColumn(MapCourierRow::sid).setHeader("SID").setAutoWidth(true);
        mapGrid.addColumn(MapCourierRow::companyName).setHeader("Firma").setFlexGrow(1);
        mapGrid.addColumn(MapCourierRow::vehicleType).setHeader("Fahrzeug").setAutoWidth(true);
        mapGrid.addColumn(row -> row.available() ? "Ja" : "Nein").setHeader("Frei").setAutoWidth(true);
        mapGrid.addColumn(MapCourierRow::locationZipcode).setHeader("PLZ").setAutoWidth(true);
        mapGrid.addColumn(MapCourierRow::gpsTypeLabel).setHeader("Ortung").setAutoWidth(true);
        mapGrid.addColumn(MapCourierRow::gpsTime).setHeader("Zeit").setAutoWidth(true);
        mapGrid.addComponentColumn(this::mapLink).setHeader("Google Maps").setAutoWidth(true);
        mapGrid.setSizeFull();
        mapGrid.asSingleSelect().addValueChangeListener(event -> {
            selectedMapCourier = event.getValue();
            updateMap();
        });
    }

    private void configureAddressAdmin() {
        addressCountryField.setValue("DE");
        addressCountryField.setWidth("100px");
        addressSearchButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        addressSearchButton.addClickListener(event -> searchAddresses());

        addressGrid.addColumn(AddressRow::street).setHeader("Strasse").setFlexGrow(1).setFrozen(true);
        addressGrid.addColumn(AddressRow::zipcode).setHeader("PLZ").setAutoWidth(true);
        addressGrid.addColumn(AddressRow::city).setHeader("Ort").setFlexGrow(1);
        addressGrid.addColumn(AddressRow::country).setHeader("Land").setAutoWidth(true);
        addressGrid.setHeight("220px");
        addressGrid.asSingleSelect().addValueChangeListener(event -> {
            selectedAddress = event.getValue();
            fillAddressEditor();
        });

        validatedStreetGrid.addColumn(ValidatedStreetRow::street).setHeader("Strasse").setFlexGrow(1).setFrozen(true);
        validatedStreetGrid.addColumn(ValidatedStreetRow::zipcode).setHeader("PLZ").setAutoWidth(true);
        validatedStreetGrid.addColumn(ValidatedStreetRow::city).setHeader("Ort").setFlexGrow(1);
        validatedStreetGrid.addColumn(ValidatedStreetRow::country).setHeader("Land").setAutoWidth(true);
        validatedStreetGrid.addColumn(ValidatedStreetRow::district).setHeader("Bezirk").setAutoWidth(true);
        validatedStreetGrid.addComponentColumn(row -> {
            Button delete = new Button("Loeschen");
            delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR);
            delete.addClickListener(event -> deleteValidatedStreet(row));
            return delete;
        }).setHeader("Aktion").setAutoWidth(true);
        validatedStreetGrid.setHeight("220px");
        validatedStreetGrid.asSingleSelect().addValueChangeListener(event -> fillValidatedEditor(event.getValue()));

        saveAddressButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        saveAddressButton.addClickListener(event -> saveAddress());

        validatedCountryField.setValue("DE");
        createValidatedButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        createValidatedButton.addClickListener(event -> createValidatedStreet());
    }

    private void configureSimilaritySearch() {
        similarityTypeField.setItemLabelGenerator(SearchTypeOption::label);
        similarityTypeField.setWidth("220px");
        similarityPhraseField.setWidth("420px");
        similaritySearchButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        similaritySearchButton.addClickListener(event -> runSimilaritySearch());
        similarityOpenButton.addClickListener(event -> openSimilarityResult(selectedSimilarity));

        similarityGrid.addColumn(SimilarityRow::entityCode).setHeader("Code").setAutoWidth(true).setFrozen(true);
        similarityGrid.addColumn(SimilarityRow::companyName).setHeader("Firma").setFlexGrow(1);
        similarityGrid.addColumn(SimilarityRow::companyName2).setHeader("Firma 2").setFlexGrow(1);
        similarityGrid.addColumn(SimilarityRow::headquartersMnemonic).setHeader("HQ").setAutoWidth(true);
        similarityGrid.addColumn(SimilarityRow::score).setHeader("Score").setAutoWidth(true);
        similarityGrid.addComponentColumn(row -> {
            Button open = new Button("Oeffnen");
            open.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
            open.addClickListener(event -> openSimilarityResult(row));
            return open;
        }).setHeader("Aktion").setAutoWidth(true);
        similarityGrid.setSizeFull();
        similarityGrid.asSingleSelect().addValueChangeListener(event -> {
            selectedSimilarity = event.getValue();
            similarityOpenButton.setEnabled(selectedSimilarity != null);
        });
        similarityOpenButton.setEnabled(false);
    }

    private void configureTravelTimes() {
        List<TravelModeOption> options = List.of(
                new TravelModeOption("classic", "CLASSIC"),
                new TravelModeOption("bwv", "BWV")
        );
        travelModeField.setItems(options);
        travelModeField.setItemLabelGenerator(TravelModeOption::label);
        travelModeField.setValue(options.get(0));

        travelLoadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        travelLoadButton.addClickListener(event -> loadTravelTimes());
        travelSaveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        travelSaveButton.addClickListener(event -> saveTravelTimes());

        travelGrid.addColumn(MutableTravelTimeRow::zipcode).setHeader("PLZ").setAutoWidth(true).setFrozen(true);
        travelGrid.addComponentColumn(row -> {
            IntegerField field = new IntegerField();
            field.setWidth("140px");
            field.setMin(0);
            field.setStepButtonsVisible(true);
            field.setValue(row.travelTime());
            field.setPlaceholder(String.valueOf(defaultTravelTimeMinutes));
            field.addValueChangeListener(event -> row.setTravelTime(event.getValue()));
            return field;
        }).setHeader("Zeit in Minuten").setAutoWidth(true);
        travelGrid.setSizeFull();
    }

    private void loadBootstrap() {
        Long headquartersId = sessionData.getHeadquartersId();
        if (headquartersId == null) {
            showError("Keine Niederlassung in der Session gefunden.");
            return;
        }
        try {
            Map<String, Object> bootstrap = restClient.getMap("/geo-operations/bootstrap?hqId=" + headquartersId);
            mapVisible = bool(bootstrap.get("mapVisible"));
            addressAdminVisible = bool(bootstrap.get("addressAdminVisible"));
            customerSearchVisible = bool(bootstrap.get("customerSearchVisible"));
            courierSearchVisible = bool(bootstrap.get("courierSearchVisible"));
            travelTimeAdminVisible = bool(bootstrap.get("travelTimeAdminVisible"));
            defaultCity = str(bootstrap.get("defaultCity"));
            defaultTravelTimeMinutes = toInt(bootstrap.get("defaultTravelTimeMinutes"), 30);

            if (addressCityField.isEmpty()) {
                addressCityField.setValue(defaultCity);
            }
            if (validatedCityField.isEmpty()) {
                validatedCityField.setValue(defaultCity);
            }

            configureSimilarityTypes();
            rebuildWorkspace();

            if (mapVisible) {
                loadMap();
            }
            if (addressAdminVisible) {
                searchAddresses();
            }
        } catch (Exception e) {
            showError("Geo-Workspace konnte nicht initialisiert werden: " + e.getMessage());
        }
    }

    private void rebuildWorkspace() {
        removeAll();
        add(new H3("Karten, Adressen und Suche"));

        intro.setText("Der NG-Workspace konsolidiert die bisher verteilten Legacy-Seiten fuer Karten, Adressen, Similarity Search und PLZ-Anfahrtszeiten in einem Bereich.");
        add(intro);

        List<TabBinding> bindings = new ArrayList<>();
        if (mapVisible) {
            bindings.add(new TabBinding(new Tab("Karte"), buildMapContent()));
        }
        if (addressAdminVisible) {
            bindings.add(new TabBinding(new Tab("Adressen"), buildAddressContent()));
        }
        if (customerSearchVisible || courierSearchVisible) {
            bindings.add(new TabBinding(new Tab("Aehnlichkeitssuche"), buildSimilarityContent()));
        }
        if (travelTimeAdminVisible) {
            bindings.add(new TabBinding(new Tab("Anfahrtszeiten"), buildTravelTimeContent()));
        }

        if (bindings.isEmpty()) {
            add(new Paragraph("Fuer diesen Benutzer sind in diesem Workspace keine Teilbereiche freigegeben."));
            return;
        }

        Tabs tabs = new Tabs();
        bindings.forEach(binding -> tabs.add(binding.tab()));
        bindings.forEach(binding -> binding.content().setVisible(false));
        bindings.get(0).content().setVisible(true);
        tabs.addSelectedChangeListener(event -> bindings.forEach(binding -> binding.content().setVisible(binding.tab() == event.getSelectedTab())));

        add(tabs);
        bindings.forEach(binding -> add(binding.content()));

        bindings.stream()
                .map(TabBinding::content)
                .filter(component -> component instanceof VerticalLayout)
                .forEach(component -> ((VerticalLayout) component).setWidthFull());
    }

    private Component buildMapContent() {
        VerticalLayout panel = new VerticalLayout(
                mapInfo,
                new HorizontalLayout(mapReloadButton),
                mapView,
                new H4("Ortbare Kuriere"),
                mapGrid
        );
        panel.setPadding(false);
        panel.setSpacing(true);
        panel.setSizeFull();
        panel.expand(mapGrid);
        return panel;
    }

    private Component buildAddressContent() {
        HorizontalLayout filterBar = new HorizontalLayout(addressStreetField, addressZipcodeField, addressCityField, addressCountryField, addressSearchButton);
        filterBar.setAlignItems(Alignment.BASELINE);
        filterBar.getStyle().set("flex-wrap", "wrap");

        FormLayout addressEditor = new FormLayout();
        addressEditor.setResponsiveSteps(
                new FormLayout.ResponsiveStep("0", 1),
                new FormLayout.ResponsiveStep("800px", 4)
        );
        addressEditor.add(editStreetField, editZipcodeField, editCityField, editCountryField, saveAddressButton);
        addressEditor.setColspan(saveAddressButton, 4);

        FormLayout validationEditor = new FormLayout();
        validationEditor.setResponsiveSteps(
                new FormLayout.ResponsiveStep("0", 1),
                new FormLayout.ResponsiveStep("800px", 4)
        );
        validationEditor.add(validatedStreetField, validatedZipcodeField, validatedCityField, validatedCountryField, createValidatedButton);
        validationEditor.setColspan(createValidatedButton, 4);

        VerticalLayout panel = new VerticalLayout(
                new Paragraph("Die Adresssuche arbeitet wie im Legacy mit Praefixfiltern auf `address` und `phoenix_special.street`. Die untere Pflege schreibt weiterhin direkt in beide Tabellen."),
                filterBar,
                new H4("Adressen"),
                addressGrid,
                new H4("Adresse bearbeiten"),
                addressEditor,
                new H4("Validierungsstrassen"),
                validatedStreetGrid,
                new H4("Validierungsadresse anlegen"),
                validationEditor
        );
        panel.setPadding(false);
        panel.setSpacing(true);
        panel.setSizeFull();
        return panel;
    }

    private Component buildSimilarityContent() {
        HorizontalLayout toolbar = new HorizontalLayout(similarityTypeField, similarityPhraseField, similaritySearchButton, similarityOpenButton);
        toolbar.setAlignItems(Alignment.BASELINE);
        toolbar.getStyle().set("flex-wrap", "wrap");

        VerticalLayout panel = new VerticalLayout(
                new Paragraph("Die Suche bewertet exakte Treffer, Praefixe und fuzzy Varianten analog zum Legacy-Popup aus Kunden- und Kurierpflege."),
                toolbar,
                similarityGrid
        );
        panel.setPadding(false);
        panel.setSpacing(true);
        panel.setSizeFull();
        panel.expand(similarityGrid);
        return panel;
    }

    private Component buildTravelTimeContent() {
        HorizontalLayout toolbar = new HorizontalLayout(travelModeField, travelFromField, travelToField, travelLoadButton, travelSaveButton);
        toolbar.setAlignItems(Alignment.BASELINE);
        toolbar.getStyle().set("flex-wrap", "wrap");

        VerticalLayout panel = new VerticalLayout(
                new Paragraph("Leere oder `0`-Werte werden beim Speichern wie im Legacy auf den Standardwert gesetzt."),
                toolbar,
                travelInfo,
                travelGrid
        );
        panel.setPadding(false);
        panel.setSpacing(true);
        panel.setSizeFull();
        panel.expand(travelGrid);
        return panel;
    }

    private void configureSimilarityTypes() {
        List<SearchTypeOption> options = new ArrayList<>();
        if (customerSearchVisible) {
            options.add(new SearchTypeOption("customer", "Kunden"));
        }
        if (courierSearchVisible) {
            options.add(new SearchTypeOption("courier", "Kuriere"));
        }
        similarityTypeField.setItems(options);
        if (!options.isEmpty()) {
            similarityTypeField.setValue(options.get(0));
        }
    }

    private void loadMap() {
        if (!mapVisible || sessionData.getHeadquartersId() == null) {
            return;
        }
        try {
            Map<String, Object> map = restClient.getMap("/geo-operations/map?hqId=" + sessionData.getHeadquartersId());
            headquarters = parseHeadquarters(map.get("headquarters"));
            mapRows.clear();
            Object rawRows = map.get("couriers");
            if (rawRows instanceof List<?> list) {
                for (Object rawRow : list) {
                    if (rawRow instanceof Map<?, ?> row) {
                        mapRows.add(new MapCourierRow(
                                toLong(row.get("courierId")),
                                str(row.get("courierCode")),
                                str(row.get("sid")),
                                str(row.get("companyName")),
                                str(row.get("vehicleType")),
                                bool(row.get("available")),
                                str(row.get("locationZipcode")),
                                str(row.get("availableTime")),
                                str(row.get("gpsTime")),
                                str(row.get("gpsTypeLabel")),
                                str(row.get("mobilePda")),
                                str(row.get("phone")),
                                str(row.get("phone2")),
                                toDouble(row.get("latitude")),
                                toDouble(row.get("longitude")),
                                bool(row.get("gpsBased")),
                                str(row.get("googleMapsUrl"))
                        ));
                    }
                }
            }
            mapRows.sort(Comparator.comparing(MapCourierRow::sid, String.CASE_INSENSITIVE_ORDER));
            mapGrid.setItems(mapRows);
            mapInfo.setText(buildMapInfo(map));
            updateMap();
        } catch (Exception e) {
            showError("Kartenuebersicht konnte nicht geladen werden: " + e.getMessage());
        }
    }

    private void searchAddresses() {
        if (!addressAdminVisible || sessionData.getHeadquartersId() == null) {
            return;
        }
        try {
            String path = "/geo-operations/addresses/search?hqId=" + sessionData.getHeadquartersId()
                    + "&street=" + encode(addressStreetField.getValue())
                    + "&zipcode=" + encode(addressZipcodeField.getValue())
                    + "&city=" + encode(addressCityField.getValue())
                    + "&country=" + encode(addressCountryField.getValue());
            Map<String, Object> result = restClient.getMap(path);

            addressRows.clear();
            Object rawAddresses = result.get("addresses");
            if (rawAddresses instanceof List<?> list) {
                for (Object item : list) {
                    if (item instanceof Map<?, ?> row) {
                        Long id = toLong(row.get("id"));
                        if (id != null) {
                            addressRows.add(new AddressRow(
                                    id,
                                    str(row.get("street")),
                                    str(row.get("zipcode")),
                                    str(row.get("city")),
                                    str(row.get("country"))
                            ));
                        }
                    }
                }
            }

            validatedRows.clear();
            Object rawValidated = result.get("validatedStreets");
            if (rawValidated instanceof List<?> list) {
                for (Object item : list) {
                    if (item instanceof Map<?, ?> row) {
                        Long id = toLong(row.get("id"));
                        if (id != null) {
                            validatedRows.add(new ValidatedStreetRow(
                                    id,
                                    str(row.get("district")),
                                    str(row.get("street")),
                                    str(row.get("zipcode")),
                                    str(row.get("city")),
                                    str(row.get("country"))
                            ));
                        }
                    }
                }
            }

            addressGrid.setItems(addressRows);
            validatedStreetGrid.setItems(validatedRows);
            showInfo(addressRows.size() + " Adressen, " + validatedRows.size() + " Validierungsstrassen geladen.");
        } catch (Exception e) {
            showError("Adresssuche fehlgeschlagen: " + e.getMessage());
        }
    }

    private void fillAddressEditor() {
        if (selectedAddress == null) {
            editStreetField.clear();
            editZipcodeField.clear();
            editCityField.clear();
            editCountryField.setValue("DE");
            return;
        }
        editStreetField.setValue(selectedAddress.street());
        editZipcodeField.setValue(selectedAddress.zipcode());
        editCityField.setValue(selectedAddress.city());
        editCountryField.setValue(selectedAddress.country().isBlank() ? "DE" : selectedAddress.country());
    }

    private void fillValidatedEditor(ValidatedStreetRow row) {
        if (row == null) {
            return;
        }
        validatedStreetField.setValue(row.street());
        validatedZipcodeField.setValue(row.zipcode());
        validatedCityField.setValue(row.city());
        validatedCountryField.setValue(row.country().isBlank() ? "DE" : row.country());
    }

    private void saveAddress() {
        if (selectedAddress == null || sessionData.getHeadquartersId() == null) {
            showError("Bitte zuerst eine Adresse auswaehlen.");
            return;
        }
        try {
            Map<String, Object> body = new LinkedHashMap<>();
            body.put("hqId", sessionData.getHeadquartersId());
            body.put("street", editStreetField.getValue());
            body.put("zipcode", editZipcodeField.getValue());
            body.put("city", editCityField.getValue());
            body.put("country", editCountryField.getValue());
            restClient.put("/geo-operations/addresses/" + selectedAddress.id(), body);
            showInfo("Adresse aktualisiert.");
            searchAddresses();
        } catch (Exception e) {
            showError("Adresse konnte nicht gespeichert werden: " + e.getMessage());
        }
    }

    private void createValidatedStreet() {
        if (sessionData.getHeadquartersId() == null) {
            return;
        }
        try {
            Map<String, Object> body = new LinkedHashMap<>();
            body.put("hqId", sessionData.getHeadquartersId());
            body.put("street", validatedStreetField.getValue());
            body.put("zipcode", validatedZipcodeField.getValue());
            body.put("city", validatedCityField.getValue());
            body.put("country", validatedCountryField.getValue());
            restClient.post("/geo-operations/addresses/validated", body, LinkedHashMap.class);
            showInfo("Validierungsadresse angelegt.");
            searchAddresses();
        } catch (Exception e) {
            showError("Validierungsadresse konnte nicht angelegt werden: " + e.getMessage());
        }
    }

    private void deleteValidatedStreet(ValidatedStreetRow row) {
        if (sessionData.getHeadquartersId() == null || row == null) {
            return;
        }
        try {
            restClient.delete("/geo-operations/addresses/validated/" + row.id() + "?hqId=" + sessionData.getHeadquartersId());
            showInfo("Validierungsadresse geloescht.");
            searchAddresses();
        } catch (Exception e) {
            showError("Validierungsadresse konnte nicht geloescht werden: " + e.getMessage());
        }
    }

    private void runSimilaritySearch() {
        if (sessionData.getHeadquartersId() == null || similarityTypeField.getValue() == null) {
            return;
        }
        try {
            String path = "/geo-operations/similarity?hqId=" + sessionData.getHeadquartersId()
                    + "&type=" + encode(similarityTypeField.getValue().code())
                    + "&phrase=" + encode(similarityPhraseField.getValue());
            List<LinkedHashMap<String, Object>> rows = restClient.getList(path, new ParameterizedTypeReference<>() {});
            similarityRows.clear();
            for (Map<String, Object> row : rows) {
                Long entityId = toLong(row.get("entityId"));
                if (entityId == null) {
                    continue;
                }
                similarityRows.add(new SimilarityRow(
                        str(row.get("type")),
                        entityId,
                        str(row.get("entityCode")),
                        str(row.get("companyName")),
                        str(row.get("companyName2")),
                        str(row.get("headquartersMnemonic")),
                        toInt(row.get("score"), 0)
                ));
            }
            similarityGrid.setItems(similarityRows);
            similarityOpenButton.setEnabled(false);
            showInfo(similarityRows.size() + " Treffer geladen.");
        } catch (Exception e) {
            showError("Aehnlichkeitssuche fehlgeschlagen: " + e.getMessage());
        }
    }

    private void openSimilarityResult(SimilarityRow row) {
        if (row == null) {
            return;
        }
        String route = "courier".equals(row.type()) ? "couriers/" + row.entityId() : "customers/" + row.entityId();
        getUI().ifPresent(ui -> ui.navigate(route));
    }

    private void loadTravelTimes() {
        if (!travelTimeAdminVisible || sessionData.getHeadquartersId() == null || travelModeField.getValue() == null) {
            return;
        }
        try {
            String path = "/geo-operations/travel-times?hqId=" + sessionData.getHeadquartersId()
                    + "&mode=" + encode(travelModeField.getValue().code())
                    + "&zipcodeFrom=" + encode(travelFromField.getValue())
                    + "&zipcodeTo=" + encode(travelToField.getValue());
            Map<String, Object> response = restClient.getMap(path);
            travelRows.clear();
            Object rawRows = response.get("rows");
            if (rawRows instanceof List<?> list) {
                for (Object item : list) {
                    if (item instanceof Map<?, ?> row) {
                        Long serviceZipId = toLong(row.get("serviceZipId"));
                        if (serviceZipId != null) {
                            travelRows.add(new MutableTravelTimeRow(
                                    serviceZipId,
                                    str(row.get("zipcode")),
                                    toNullableInteger(row.get("travelTime"))
                            ));
                        }
                    }
                }
            }
            travelGrid.setItems(travelRows);
            travelInfo.setText(travelRows.size() + " PLZ geladen. Leer/0 bedeutet beim Speichern Standard " + defaultTravelTimeMinutes + " Minuten.");
        } catch (Exception e) {
            showError("Anfahrtszeiten konnten nicht geladen werden: " + e.getMessage());
        }
    }

    private void saveTravelTimes() {
        if (sessionData.getHeadquartersId() == null || travelModeField.getValue() == null || travelRows.isEmpty()) {
            return;
        }
        try {
            Map<String, Object> body = new LinkedHashMap<>();
            body.put("hqId", sessionData.getHeadquartersId());
            body.put("mode", travelModeField.getValue().code());
            List<Map<String, Object>> rows = new ArrayList<>();
            for (MutableTravelTimeRow row : travelRows) {
                Map<String, Object> item = new LinkedHashMap<>();
                item.put("serviceZipId", row.serviceZipId());
                item.put("zipcode", row.zipcode());
                item.put("travelTime", row.travelTime());
                rows.add(item);
            }
            body.put("rows", rows);
            @SuppressWarnings("unchecked")
            Map<String, Object> result = (Map<String, Object>) restClient.post("/geo-operations/travel-times", body, LinkedHashMap.class);
            showInfo(toInt(result.get("updatedCount"), rows.size()) + " Anfahrtszeiten gespeichert.");
            loadTravelTimes();
        } catch (Exception e) {
            showError("Anfahrtszeiten konnten nicht gespeichert werden: " + e.getMessage());
        }
    }

    private MapHeadquarters parseHeadquarters(Object rawHeadquarters) {
        if (!(rawHeadquarters instanceof Map<?, ?> row)) {
            return null;
        }
        return new MapHeadquarters(
                toLong(row.get("id")),
                str(row.get("name")),
                str(row.get("mnemonic")),
                toDouble(row.get("latitude")),
                toDouble(row.get("longitude"))
        );
    }

    private String buildMapInfo(Map<String, Object> map) {
        String headquartersLabel = headquarters != null ? headquarters.label() : "Keine HQ-Koordinate";
        return headquartersLabel + " | " + toInt(map.get("mappedCourierCount"), mapRows.size()) + " ortbare Kuriere";
    }

    private Anchor mapLink(MapCourierRow row) {
        if (row.googleMapsUrl().isBlank()) {
            return new Anchor();
        }
        Anchor anchor = new Anchor(row.googleMapsUrl(), "Oeffnen");
        anchor.setTarget("_blank");
        return anchor;
    }

    private void updateMap() {
        if (!mapVisible) {
            return;
        }
        if ((headquarters == null || !headquarters.hasPoint()) && mapRows.stream().noneMatch(MapCourierRow::hasPoint)) {
            mapView.getElement().setProperty("innerHTML", buildEmptyMapSvg("Keine geocodierten Kartendaten"));
            return;
        }
        mapView.getElement().setProperty("innerHTML", buildMapSvg());
    }

    private String buildMapSvg() {
        List<PointSource> points = new ArrayList<>();
        if (headquarters != null && headquarters.hasPoint()) {
            points.add(new PointSource(headquarters.latitude(), headquarters.longitude()));
        }
        for (MapCourierRow row : mapRows) {
            if (row.hasPoint()) {
                points.add(new PointSource(row.latitude(), row.longitude()));
            }
        }
        if (points.isEmpty()) {
            return buildEmptyMapSvg("Keine geocodierten Kartendaten");
        }

        double minLongitude = points.stream().mapToDouble(PointSource::longitude).min().orElse(5.5d);
        double maxLongitude = points.stream().mapToDouble(PointSource::longitude).max().orElse(15.5d);
        double minLatitude = points.stream().mapToDouble(PointSource::latitude).min().orElse(47.0d);
        double maxLatitude = points.stream().mapToDouble(PointSource::latitude).max().orElse(55.0d);
        double longitudeSpan = Math.max(maxLongitude - minLongitude, 4.0d);
        double latitudeSpan = Math.max(maxLatitude - minLatitude, 3.0d);
        minLongitude -= longitudeSpan * 0.12d;
        maxLongitude += longitudeSpan * 0.12d;
        minLatitude -= latitudeSpan * 0.12d;
        maxLatitude += latitudeSpan * 0.12d;

        int width = 860;
        int height = 380;
        int padding = 26;
        StringBuilder svg = new StringBuilder();
        svg.append("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ").append(width).append(' ').append(height).append("'>");
        svg.append("<rect width='").append(width).append("' height='").append(height).append("' fill='#edf6ff'/>");
        svg.append("<rect x='").append(padding).append("' y='").append(padding).append("' width='")
                .append(width - (padding * 2)).append("' height='").append(height - (padding * 2))
                .append("' rx='18' fill='#ffffff' fill-opacity='0.55' stroke='#0f172a' stroke-opacity='0.10'/>");

        for (int step = 1; step < 5; step++) {
            double verticalX = padding + ((width - (padding * 2)) / 5.0d) * step;
            double horizontalY = padding + ((height - (padding * 2)) / 5.0d) * step;
            svg.append("<line x1='").append(roundSvg(verticalX)).append("' y1='").append(padding)
                    .append("' x2='").append(roundSvg(verticalX)).append("' y2='").append(height - padding)
                    .append("' stroke='#0f172a' stroke-opacity='0.08' stroke-dasharray='4 8'/>");
            svg.append("<line x1='").append(padding).append("' y1='").append(roundSvg(horizontalY))
                    .append("' x2='").append(width - padding).append("' y2='").append(roundSvg(horizontalY))
                    .append("' stroke='#0f172a' stroke-opacity='0.08' stroke-dasharray='4 8'/>");
        }

        if (headquarters != null && headquarters.hasPoint()) {
            double x = projectLongitude(headquarters.longitude(), minLongitude, maxLongitude, width, padding);
            double y = projectLatitude(headquarters.latitude(), minLatitude, maxLatitude, height, padding);
            svg.append("<circle cx='").append(roundSvg(x)).append("' cy='").append(roundSvg(y))
                    .append("' r='10' fill='#101828' stroke='#ffffff' stroke-width='2'>")
                    .append("<title>").append(escapeSvgText(headquarters.label())).append("</title></circle>");
            svg.append("<text x='").append(roundSvg(x + 14)).append("' y='").append(roundSvg(y + 4))
                    .append("' fill='#101828' font-size='12' font-family='Arial, sans-serif'>HQ</text>");
        }

        for (MapCourierRow row : mapRows) {
            appendMapCourier(svg, row, minLongitude, maxLongitude, minLatitude, maxLatitude, width, height, padding);
        }

        svg.append("</svg>");
        return svg.toString();
    }

    private void appendMapCourier(StringBuilder svg,
                                  MapCourierRow row,
                                  double minLongitude,
                                  double maxLongitude,
                                  double minLatitude,
                                  double maxLatitude,
                                  int width,
                                  int height,
                                  int padding) {
        if (!row.hasPoint()) {
            return;
        }
        double x = projectLongitude(row.longitude(), minLongitude, maxLongitude, width, padding);
        double y = projectLatitude(row.latitude(), minLatitude, maxLatitude, height, padding);
        boolean selected = selectedMapCourier != null && row.courierId().equals(selectedMapCourier.courierId());
        String fill = row.gpsBased()
                ? (row.available() ? "#087443" : "#b42318")
                : "#b54708";
        String tooltip = row.sid() + " | " + row.companyName() + " | " + row.gpsTypeLabel();

        if (selected) {
            svg.append("<circle cx='").append(roundSvg(x)).append("' cy='").append(roundSvg(y))
                    .append("' r='13' fill='#101828' fill-opacity='0.16'/>");
        }
        svg.append("<circle cx='").append(roundSvg(x)).append("' cy='").append(roundSvg(y))
                .append("' r='").append(selected ? "7" : "5.5").append("' fill='").append(fill)
                .append("' stroke='#ffffff' stroke-width='2'>")
                .append("<title>").append(escapeSvgText(tooltip)).append("</title></circle>");
    }

    private String buildEmptyMapSvg(String message) {
        return "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 860 380'>"
                + "<rect width='860' height='380' fill='#f7f9fc'/>"
                + "<rect x='26' y='26' width='808' height='328' rx='18' fill='#ffffff' stroke='#0f172a' stroke-opacity='0.10'/>"
                + "<text x='430' y='190' text-anchor='middle' fill='#475467' font-size='16' font-family='Arial, sans-serif'>"
                + escapeSvgText(message)
                + "</text></svg>";
    }

    private double projectLongitude(Double longitude, double minLongitude, double maxLongitude, int width, int padding) {
        double usableWidth = width - (padding * 2.0d);
        return padding + ((longitude - minLongitude) / (maxLongitude - minLongitude)) * usableWidth;
    }

    private double projectLatitude(Double latitude, double minLatitude, double maxLatitude, int height, int padding) {
        double usableHeight = height - (padding * 2.0d);
        return padding + (1.0d - ((latitude - minLatitude) / (maxLatitude - minLatitude))) * usableHeight;
    }

    private String roundSvg(double value) {
        return String.format(Locale.ROOT, "%.2f", value);
    }

    private String escapeSvgText(String value) {
        return value == null ? "" : value
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&apos;");
    }

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

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

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

    private Long toLong(Object value) {
        if (value instanceof Number number) {
            return number.longValue();
        }
        try {
            return value != null && !String.valueOf(value).isBlank() ? Long.parseLong(String.valueOf(value)) : null;
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private Integer toNullableInteger(Object value) {
        if (value instanceof Number number) {
            return number.intValue();
        }
        try {
            return value != null && !String.valueOf(value).isBlank() ? Integer.parseInt(String.valueOf(value)) : null;
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private int toInt(Object value, int fallback) {
        Integer parsed = toNullableInteger(value);
        return parsed != null ? parsed : fallback;
    }

    private Double toDouble(Object value) {
        if (value instanceof Number number) {
            return number.doubleValue();
        }
        try {
            return value != null && !String.valueOf(value).isBlank() ? Double.parseDouble(String.valueOf(value)) : null;
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private void showInfo(String message) {
        Notification.show(message, 3000, Notification.Position.BOTTOM_START)
                .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
    }

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

    private record TabBinding(Tab tab, Component content) {
    }

    private record SearchTypeOption(String code, String label) {
    }

    private record TravelModeOption(String code, String label) {
    }

    private record PointSource(Double latitude, Double longitude) {
    }

    private record MapHeadquarters(Long id, String name, String mnemonic, Double latitude, Double longitude) {
        public boolean hasPoint() {
            return latitude != null && longitude != null;
        }

        public String label() {
            if (!mnemonic.isBlank()) {
                return mnemonic + " - " + name;
            }
            return name;
        }
    }

    private record MapCourierRow(Long courierId,
                                 String courierCode,
                                 String sid,
                                 String companyName,
                                 String vehicleType,
                                 boolean available,
                                 String locationZipcode,
                                 String availableTime,
                                 String gpsTime,
                                 String gpsTypeLabel,
                                 String mobilePda,
                                 String phone,
                                 String phone2,
                                 Double latitude,
                                 Double longitude,
                                 boolean gpsBased,
                                 String googleMapsUrl) {
        public boolean hasPoint() {
            return latitude != null && longitude != null;
        }
    }

    private record AddressRow(Long id, String street, String zipcode, String city, String country) {
    }

    private record ValidatedStreetRow(Long id, String district, String street, String zipcode, String city, String country) {
    }

    private record SimilarityRow(String type,
                                 Long entityId,
                                 String entityCode,
                                 String companyName,
                                 String companyName2,
                                 String headquartersMnemonic,
                                 int score) {
    }

    private static final class MutableTravelTimeRow {
        private final Long serviceZipId;
        private final String zipcode;
        private Integer travelTime;

        private MutableTravelTimeRow(Long serviceZipId, String zipcode, Integer travelTime) {
            this.serviceZipId = serviceZipId;
            this.zipcode = zipcode;
            this.travelTime = travelTime;
        }

        public Long serviceZipId() {
            return serviceZipId;
        }

        public String zipcode() {
            return zipcode;
        }

        public Integer travelTime() {
            return travelTime;
        }

        public void setTravelTime(Integer travelTime) {
            this.travelTime = travelTime;
        }
    }
}
