package de.assecutor.pdftool.ui;

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.confirmdialog.ConfirmDialog;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.ColumnTextAlign;
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.Paragraph;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
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.textfield.BigDecimalField;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import de.assecutor.pdftool.address.Address;
import de.assecutor.pdftool.address.AddressRepository;
import de.assecutor.pdftool.address.AddressType;
import de.assecutor.pdftool.invoice.CapturedInvoiceData;
import de.assecutor.pdftool.invoice.InvoiceDraft;
import de.assecutor.pdftool.invoice.InvoiceDraftService;
import de.assecutor.pdftool.invoice.InvoiceMetadata;
import de.assecutor.pdftool.invoice.VatIdValidator;
import de.assecutor.pdftool.template.InvoiceTemplateService;
import de.assecutor.pdftool.template.TemplatePdfService;
import de.assecutor.pdftool.template.TemplateRenderException;
import de.assecutor.pdftool.template.TemplateVariables;
import de.assecutor.pdftool.zugferd.ZugferdConversionException;
import de.assecutor.pdftool.zugferd.ZugferdResult;
import de.assecutor.pdftool.zugferd.ZugferdService;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

@Route(value = "", layout = MainLayout.class)
@PageTitle("E-Rechnung erzeugen")
public class MainView extends VerticalLayout {

    private static final BigDecimal MAX_VAT_PERCENT = new BigDecimal("100");

    private final ZugferdService zugferdService;
    private final CapturedInvoiceData capturedInvoiceData;
    private final InvoiceTemplateService invoiceTemplateService;
    private final TemplatePdfService templatePdfService;

    private final MemoryBuffer uploadBuffer = new MemoryBuffer();
    private byte[] uploadedPdf;

    /** Alternative zum PDF-Upload: ein gespeichertes Rechnungstemplate. */
    private final ComboBox<String> templateSelect = new ComboBox<>("Rechnungstemplate");

    /** Vorbelegung der Rechnungssteller-Felder aus dem Adressbuch. */
    private final ComboBox<Address> senderAddressSelect = new ComboBox<>("Aus dem Adressbuch übernehmen");

    /** Vorbelegung der Rechnungsempfänger-Felder aus dem Adressbuch. */
    private final ComboBox<Address> recipientAddressSelect = new ComboBox<>("Aus dem Adressbuch übernehmen");

    private final AddressRepository addressRepository;
    private final InvoiceDraftService invoiceDraftService;

    /** Laden zuvor gespeicherter Eingaben dieser Maske. */
    private final ComboBox<String> draftSelect = new ComboBox<>("Vorbelegung laden");

    private final Button saveDraftButton = new Button("Eingaben speichern", event -> openSaveDraftDialog());

    /**
     * Formularstand zum Zeitpunkt des letzten Ladens bzw. Speicherns einer
     * Vorbelegung; {@code null}, solange keine geladen wurde. Der
     * Speichern-Button ist nur aktiv, wenn keine Vorbelegung geladen ist oder
     * sich die Eingaben seither geändert haben.
     */
    private InvoiceDraft loadedDraftState;

    private final Button generate = new Button("E-Rechnung erzeugen", event -> generate());

    private final TextField invoiceNumber = new TextField("Rechnungsnummer");
    private final DatePicker issueDate = new DatePicker("Rechnungsdatum");
    private final DatePicker deliveryDate = new DatePicker("Lieferdatum");
    private final DatePicker dueDate = new DatePicker("Fälligkeitsdatum");
    private final TextField currency = new TextField("Währung");
    private final TextField paymentTerms = new TextField("Zahlungsbedingungen");
    private final TextField buyerReference = new TextField("Käuferreferenz / Leitweg-ID");
    private final TextField iban = new TextField("IBAN (Rechnungssteller)");
    private final TextField bic = new TextField("BIC");

    private final PartyForm sender = new PartyForm(true);
    private final PartyForm recipient = new PartyForm(false);

    /** Die erfassten Rechnungspositionen, dargestellt in einer Tabelle. */
    private final List<InvoiceMetadata.LineItem> lineItems = new ArrayList<>();
    private final Grid<InvoiceMetadata.LineItem> itemsGrid = new Grid<>();

    private final Anchor downloadLink = new Anchor();

    /**
     * Alle Controls unterhalb der Quellenwahl; sie sind erst aktiv, wenn ein PDF
     * hochgeladen oder ein Template ausgewählt wurde.
     */
    private final List<com.vaadin.flow.component.HasEnabled> sourceDependentControls = new ArrayList<>();

    public MainView(ZugferdService zugferdService, CapturedInvoiceData capturedInvoiceData,
            InvoiceTemplateService invoiceTemplateService, TemplatePdfService templatePdfService,
            AddressRepository addressRepository, InvoiceDraftService invoiceDraftService,
            Environment environment) {
        this.zugferdService = zugferdService;
        this.capturedInvoiceData = capturedInvoiceData;
        this.invoiceTemplateService = invoiceTemplateService;
        this.templatePdfService = templatePdfService;
        this.addressRepository = addressRepository;
        this.invoiceDraftService = invoiceDraftService;

        setMaxWidth("960px");

        add(new Paragraph("Laden Sie ein Rechnungs-PDF hoch oder wählen Sie ein gespeichertes "
                + "Rechnungstemplate und ergänzen Sie die Rechnungsdaten. Die Rechnung wird als "
                + "rechtskonforme ZUGFeRD-Rechnung (PDF/A-3 mit eingebettetem EN16931-XML) erzeugt "
                + "und als ZIP-Datei bereitgestellt."));

        // Eingaben unter einem Namen speichern bzw. gespeicherte Eingaben laden.
        draftSelect.setPlaceholder("Vorbelegung laden");
        draftSelect.setClearButtonVisible(true);
        draftSelect.setWidth("300px");
        draftSelect.addValueChangeListener(event -> {
            if (event.isFromClient() && event.getValue() != null) {
                loadDraft(event.getValue());
            } else if (event.isFromClient()) {
                loadedDraftState = null;
                updateSaveDraftEnabled();
            }
        });
        add(draftSelect);

        Upload upload = new Upload(uploadBuffer);
        upload.setAcceptedFileTypes("application/pdf", ".pdf");
        upload.setMaxFiles(1);
        upload.setMaxFileSize(25 * 1024 * 1024);
        upload.addSucceededListener(event -> {
            try {
                uploadedPdf = uploadBuffer.getInputStream().readAllBytes();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
            // Entweder PDF oder Template: hochgeladenes PDF hebt die Template-Auswahl auf.
            templateSelect.clear();
            updateControlsEnabled();
        });
        upload.addFileRemovedListener(event -> {
            uploadedPdf = null;
            updateControlsEnabled();
        });

        templateSelect.setItems(invoiceTemplateService.templateNames());
        templateSelect.setPlaceholder("Template auswählen");
        templateSelect.setClearButtonVisible(true);
        templateSelect.setWidth("300px");
        templateSelect.addValueChangeListener(event -> {
            if (event.isFromClient() && event.getValue() != null) {
                // Entweder PDF oder Template: Auswahl entfernt ein hochgeladenes PDF.
                upload.clearFileList();
                uploadedPdf = null;
                // Rechnungssteller-Felder leer beginnen lassen.
                senderAddressSelect.clear();
                sender.clearFields();
            }
            updateControlsEnabled();
        });
        HorizontalLayout sourceLayout = new HorizontalLayout(upload, templateSelect);
        sourceLayout.setAlignItems(Alignment.CENTER);
        // Das Label der ComboBox ausgleichen, damit das Eingabefeld selbst
        // mittig zur Upload-Fläche steht.
        templateSelect.getStyle().set("margin-top", "-1.25em");
        add(new H3("1. Rechnungs-PDF hochladen oder Template auswählen"), sourceLayout);

        invoiceNumber.setRequired(true);
        invoiceNumber.setErrorMessage("Rechnungsnummer ist ein Pflichtfeld.");
        issueDate.setRequired(true);
        issueDate.setErrorMessage("Rechnungsdatum ist ein Pflichtfeld.");
        issueDate.setValue(LocalDate.now());
        currency.setRequired(true);
        currency.setMaxLength(3);
        currency.setPattern("[A-Za-z]{3}");
        currency.setErrorMessage("Währung als dreistelliger ISO-Code, z. B. EUR.");
        currency.setValue("EUR");
        buyerReference.setHelperText("BT-10 — für Rechnungen an Behörden erforderlich");
        iban.setHelperText("Für die Zahlungsangaben (BG-16) empfohlen");
        FormLayout invoiceForm = new FormLayout(invoiceNumber, issueDate, deliveryDate, dueDate, currency,
                paymentTerms, buyerReference, iban, bic);
        invoiceForm.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("600px", 3));
        add(new H3("2. Rechnungsdaten"), invoiceForm);

        configureAddressSelect(senderAddressSelect, sender);
        configureAddressSelect(recipientAddressSelect, recipient);

        add(new H3("3. Rechnungssteller"), senderAddressSelect, sender);
        add(new H3("4. Rechnungsempfänger"), recipientAddressSelect, recipient);

        configureItemsGrid();
        Button addItem = new Button("Position hinzufügen", new Icon(VaadinIcon.PLUS),
                event -> openItemDialog(null));
        add(new H3("5. Rechnungspositionen (netto)"), itemsGrid, addItem);

        generate.addThemeVariants(ButtonVariant.LUMO_PRIMARY);

        sourceDependentControls.addAll(List.of(invoiceForm, senderAddressSelect, sender,
                recipientAddressSelect, recipient, itemsGrid, addItem, generate));
        updateControlsEnabled();

        downloadLink.getElement().setAttribute("download", true);
        downloadLink.setVisible(false);

        add(new HorizontalLayout(generate, saveDraftButton, downloadLink));

        registerDraftDirtyTracking();
        updateSaveDraftEnabled();

        if (environment.acceptsProfiles(Profiles.of("dev"))) {
            prefillDevData();
        }
    }

    @Override
    protected void onAttach(com.vaadin.flow.component.AttachEvent attachEvent) {
        super.onAttach(attachEvent);
        // Adressen bei jedem Anzeigen der Seite frisch laden, damit neu angelegte
        // Adressbuch-Einträge ohne Neustart sichtbar sind.
        senderAddressSelect.setItems(addressRepository.findByType(AddressType.RECHNUNGSSTELLER));
        recipientAddressSelect.setItems(addressRepository.findByType(AddressType.RECHNUNGSEMPFAENGER));
        draftSelect.setItems(invoiceDraftService.names());
    }

    /** Fragt den Namen ab, unter dem die Eingaben gespeichert werden sollen. */
    private void openSaveDraftDialog() {
        Dialog dialog = new Dialog();
        dialog.setHeaderTitle("Eingaben speichern");

        TextField nameField = new TextField("Name");
        nameField.setWidth("300px");
        dialog.add(nameField);

        Button cancel = new Button("Abbrechen", event -> dialog.close());
        Button save = new Button("Speichern", event -> {
            String name = nameField.getValue() == null ? "" : nameField.getValue().trim();
            if (name.isEmpty()) {
                nameField.setErrorMessage("Bitte einen Namen angeben.");
                nameField.setInvalid(true);
                return;
            }
            dialog.close();
            if (invoiceDraftService.exists(name)) {
                confirmOverwriteDraft(name);
            } else {
                saveDraft(name);
            }
        });
        save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        dialog.getFooter().add(cancel, save);

        dialog.open();
        nameField.focus();
    }

    /** Rückfrage, ob die bereits vorhandenen Eingaben gleichen Namens überschrieben werden sollen. */
    private void confirmOverwriteDraft(String name) {
        ConfirmDialog confirm = new ConfirmDialog();
        confirm.setHeader("Eingaben überschreiben?");
        confirm.setText("Es sind bereits Eingaben mit dem Namen \"" + name
                + "\" gespeichert. Sollen sie überschrieben werden?");
        confirm.setConfirmText("Überschreiben");
        confirm.setCancelable(true);
        confirm.setCancelText("Abbrechen");
        confirm.addConfirmListener(event -> saveDraft(name));
        confirm.addCancelListener(event -> openSaveDraftDialog());
        confirm.open();
    }

    private void saveDraft(String name) {
        try {
            invoiceDraftService.save(buildDraft(name));
        } catch (Exception ex) {
            showErrors(List.of("Die Eingaben konnten nicht unter \"" + name
                    + "\" gespeichert werden: " + ex.getMessage()));
            return;
        }
        draftSelect.setItems(invoiceDraftService.names());
        draftSelect.setValue(name);
        loadedDraftState = comparableState();
        updateSaveDraftEnabled();
        Notification.show("Eingaben \"" + name + "\" gespeichert.", 3000, Notification.Position.BOTTOM_START)
                .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
    }

    /** Sammelt den aktuellen Formularstand; ein hochgeladenes PDF wird nicht mitgespeichert. */
    private InvoiceDraft buildDraft(String name) {
        return new InvoiceDraft(null, name,
                templateSelect.getValue(),
                invoiceNumber.getValue().trim(),
                issueDate.getValue(), deliveryDate.getValue(), dueDate.getValue(),
                currency.getValue().trim(),
                paymentTerms.getValue(),
                buyerReference.getValue().trim(),
                iban.getValue().trim(),
                bic.getValue().trim(),
                sender.toParty(), recipient.toParty(),
                List.copyOf(lineItems),
                LocalDateTime.now());
    }

    private void loadDraft(String name) {
        Optional<InvoiceDraft> draft = invoiceDraftService.load(name);
        if (draft.isEmpty()) {
            showErrors(List.of("Die gespeicherten Eingaben \"" + name + "\" konnten nicht geladen werden."));
            return;
        }
        applyDraft(draft.get());
        Notification.show("Eingaben \"" + name + "\" geladen.", 3000, Notification.Position.BOTTOM_START)
                .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
    }

    /** Überträgt gespeicherte Eingaben in das Formular. */
    private void applyDraft(InvoiceDraft draft) {
        // Die Template-Auswahl wiederherstellen, sofern das Template noch existiert;
        // ein hochgeladenes PDF ist nicht Teil der gespeicherten Eingaben.
        if (draft.templateName() != null && !draft.templateName().isBlank()
                && invoiceTemplateService.templateExists(draft.templateName())) {
            templateSelect.setValue(draft.templateName());
        }
        invoiceNumber.setValue(nullToEmpty(draft.invoiceNumber()));
        issueDate.setValue(draft.issueDate());
        deliveryDate.setValue(draft.deliveryDate());
        dueDate.setValue(draft.dueDate());
        currency.setValue(nullToEmpty(draft.currency()));
        paymentTerms.setValue(nullToEmpty(draft.paymentTerms()));
        buyerReference.setValue(nullToEmpty(draft.buyerReference()));
        iban.setValue(nullToEmpty(draft.iban()));
        bic.setValue(nullToEmpty(draft.bic()));
        applyParty(sender, draft.sender());
        applyParty(recipient, draft.recipient());
        lineItems.clear();
        if (draft.items() != null) {
            lineItems.addAll(draft.items());
        }
        refreshItemsGrid();
        updateControlsEnabled();
        // Frisch geladen: Speichern erst wieder anbieten, wenn sich etwas ändert.
        loadedDraftState = comparableState();
        updateSaveDraftEnabled();
    }

    private void applyParty(PartyForm form, InvoiceMetadata.Party party) {
        if (party == null) {
            form.clearFields();
            return;
        }
        form.prefill(nullToEmpty(party.name()), nullToEmpty(party.street()),
                nullToEmpty(party.zip()), nullToEmpty(party.city()),
                nullToEmpty(party.countryCode()), nullToEmpty(party.vatId()),
                nullToEmpty(party.email()), nullToEmpty(party.phone()));
    }

    /** Konfiguriert eine Adressbuch-Auswahl, die bei Auswahl das zugehörige Adressformular vorbelegt. */
    private void configureAddressSelect(ComboBox<Address> select, PartyForm form) {
        select.setItemLabelGenerator(address -> address.getName());
        select.setPlaceholder("Adresse auswählen");
        select.setClearButtonVisible(true);
        select.setWidth("300px");
        select.addValueChangeListener(event -> {
            Address address = event.getValue();
            if (address != null) {
                form.prefill(nullToEmpty(address.getName()), nullToEmpty(address.getStreet()),
                        nullToEmpty(address.getZip()), nullToEmpty(address.getCity()),
                        nullToEmpty(address.getCountryCode()), nullToEmpty(address.getVatId()),
                        nullToEmpty(address.getEmail()), nullToEmpty(address.getPhone()));
            }
        });
    }

    @Override
    protected void onDetach(com.vaadin.flow.component.DetachEvent detachEvent) {
        // Erfasste Positionen für den Rechnungsgenerator bereitstellen,
        // z. B. beim Wechsel über die Sidebar.
        capturedInvoiceData.setItems(snapshotItems());
        super.onDetach(detachEvent);
    }

    /** Übernimmt alle erfassten Positionen. */
    private List<InvoiceMetadata.LineItem> snapshotItems() {
        return List.copyOf(lineItems);
    }

    /** Belegt das Formular im dev-Profil mit Testdaten vor, um manuelles Tippen zu sparen. */
    private void prefillDevData() {
        invoiceNumber.setValue("11111");
        deliveryDate.setValue(LocalDate.now());
        dueDate.setValue(LocalDate.now().plusDays(14));
        paymentTerms.setValue("14 Tage");
        buyerReference.setValue("11111");
        iban.setValue("DE64 6001 0070 0379 0907 00");
        bic.setValue("PBNKDEFFXXX");
        // Rechnungssteller und -empfänger bleiben leer und werden über die
        // Adressbuch-Auswahl befüllt.
        lineItems.add(new InvoiceMetadata.LineItem("sdf", BigDecimal.ONE, new BigDecimal("100"),
                new BigDecimal("19")));
        refreshItemsGrid();
    }

    /** Tabelle der Rechnungspositionen mit Bearbeiten-/Löschen-Icons pro Zeile. */
    private void configureItemsGrid() {
        itemsGrid.addColumn(item -> item.description()).setHeader("Beschreibung").setFlexGrow(1);
        itemsGrid.addColumn(item -> formatDecimal(item.quantity())).setHeader("Menge")
                .setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0);
        itemsGrid.addColumn(item -> formatDecimal(item.unitPriceNet())).setHeader("Einzelpreis netto")
                .setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0);
        itemsGrid.addColumn(item -> formatDecimal(item.vatPercent())).setHeader("USt %")
                .setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0);
        itemsGrid.addComponentColumn(item -> {
            Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openItemDialog(item));
            edit.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
            edit.setTooltipText("Bearbeiten");
            Button delete = new Button(new Icon(VaadinIcon.TRASH), event -> {
                lineItems.remove(item);
                refreshItemsGrid();
            });
            delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR,
                    ButtonVariant.LUMO_SMALL);
            delete.setTooltipText("Löschen");
            return new HorizontalLayout(edit, delete);
        }).setHeader("").setAutoWidth(true).setFlexGrow(0);
        itemsGrid.addItemDoubleClickListener(event -> openItemDialog(event.getItem()));
        itemsGrid.setAllRowsVisible(true);
        itemsGrid.setWidthFull();
    }

    private void refreshItemsGrid() {
        itemsGrid.setItems(List.copyOf(lineItems));
        updateSaveDraftEnabled();
    }

    /** Aktualisiert den Speichern-Button bei jeder Änderung an den Eingabefeldern. */
    private void registerDraftDirtyTracking() {
        invoiceNumber.addValueChangeListener(event -> updateSaveDraftEnabled());
        issueDate.addValueChangeListener(event -> updateSaveDraftEnabled());
        deliveryDate.addValueChangeListener(event -> updateSaveDraftEnabled());
        dueDate.addValueChangeListener(event -> updateSaveDraftEnabled());
        currency.addValueChangeListener(event -> updateSaveDraftEnabled());
        paymentTerms.addValueChangeListener(event -> updateSaveDraftEnabled());
        buyerReference.addValueChangeListener(event -> updateSaveDraftEnabled());
        iban.addValueChangeListener(event -> updateSaveDraftEnabled());
        bic.addValueChangeListener(event -> updateSaveDraftEnabled());
        templateSelect.addValueChangeListener(event -> updateSaveDraftEnabled());
        sender.onAnyChange(this::updateSaveDraftEnabled);
        recipient.onAnyChange(this::updateSaveDraftEnabled);
    }

    /**
     * Der Speichern-Button ist nur aktiv, wenn keine Vorbelegung geladen wurde
     * oder sich die Eingaben seit dem Laden bzw. Speichern geändert haben.
     */
    private void updateSaveDraftEnabled() {
        saveDraftButton.setEnabled(loadedDraftState == null || !comparableState().equals(loadedDraftState));
    }

    /** Der aktuelle Formularstand ohne Name und Zeitstempel, zum Vergleich mit der geladenen Vorbelegung. */
    private InvoiceDraft comparableState() {
        return new InvoiceDraft(null, "",
                templateSelect.getValue(),
                invoiceNumber.getValue().trim(),
                issueDate.getValue(), deliveryDate.getValue(), dueDate.getValue(),
                currency.getValue().trim(),
                paymentTerms.getValue(),
                buyerReference.getValue().trim(),
                iban.getValue().trim(),
                bic.getValue().trim(),
                sender.toParty(), recipient.toParty(),
                List.copyOf(lineItems),
                null);
    }

    /** Dialog zum Anlegen ({@code existing == null}) bzw. Bearbeiten einer Rechnungsposition. */
    private void openItemDialog(InvoiceMetadata.LineItem existing) {
        Dialog dialog = new Dialog();
        dialog.setHeaderTitle(existing == null ? "Position hinzufügen" : "Position bearbeiten");

        TextField description = new TextField("Beschreibung");
        description.setRequired(true);
        description.setValue(existing != null ? existing.description() : "");
        description.setWidthFull();
        BigDecimalField quantity = new BigDecimalField("Menge");
        quantity.setRequiredIndicatorVisible(true);
        quantity.setValue(existing != null ? existing.quantity() : BigDecimal.ONE);
        BigDecimalField unitPrice = new BigDecimalField("Einzelpreis netto");
        unitPrice.setRequiredIndicatorVisible(true);
        unitPrice.setValue(existing != null ? existing.unitPriceNet() : null);
        BigDecimalField vatPercent = new BigDecimalField("USt %");
        vatPercent.setRequiredIndicatorVisible(true);
        vatPercent.setValue(existing != null ? existing.vatPercent() : new BigDecimal("19"));

        FormLayout form = new FormLayout(description, quantity, unitPrice, vatPercent);
        form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("500px", 3));
        form.setColspan(description, 3);
        form.setWidth("560px");
        dialog.add(form);

        Button cancel = new Button("Abbrechen", event -> dialog.close());
        Button save = new Button("Speichern", event -> {
            List<String> errors = new ArrayList<>();
            requireFilled(description, "Beschreibung ist ein Pflichtfeld.", errors);
            if (quantity.getValue() == null || quantity.getValue().signum() <= 0) {
                markInvalid(quantity, "Menge muss größer 0 sein.", errors);
            } else {
                quantity.setInvalid(false);
            }
            if (unitPrice.getValue() == null) {
                markInvalid(unitPrice, "Einzelpreis ist ein Pflichtfeld.", errors);
            } else {
                unitPrice.setInvalid(false);
            }
            if (vatPercent.getValue() == null || vatPercent.getValue().signum() < 0
                    || vatPercent.getValue().compareTo(MAX_VAT_PERCENT) > 0) {
                markInvalid(vatPercent, "USt-Satz muss zwischen 0 und 100 liegen.", errors);
            } else {
                vatPercent.setInvalid(false);
            }
            if (!errors.isEmpty()) {
                showErrors(errors);
                return;
            }

            InvoiceMetadata.LineItem item = new InvoiceMetadata.LineItem(description.getValue().trim(),
                    quantity.getValue(), unitPrice.getValue(), vatPercent.getValue());
            int index = existing != null ? lineItems.indexOf(existing) : -1;
            if (index >= 0) {
                lineItems.set(index, item);
            } else {
                lineItems.add(item);
            }
            refreshItemsGrid();
            dialog.close();
        });
        save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        dialog.getFooter().add(cancel, save);

        dialog.open();
        description.focus();
    }

    /**
     * Das Rechnungsformular und der Erzeugen-Button sind nur aktiv, wenn ein
     * PDF hochgeladen oder ein Template ausgewählt wurde.
     */
    private void updateControlsEnabled() {
        boolean sourceSelected = uploadedPdf != null || templateSelect.getValue() != null;
        sourceDependentControls.forEach(control -> control.setEnabled(sourceSelected));
    }

    private void generate() {
        downloadLink.setVisible(false);
        List<String> errors = validateForm();
        if (!errors.isEmpty()) {
            showErrors(errors);
            return;
        }
        if (uploadedPdf != null) {
            convertAndOfferDownload(uploadedPdf);
            return;
        }
        String templateName = templateSelect.getValue();
        Optional<String> templateData = invoiceTemplateService.loadTemplate(templateName);
        if (templateData.isEmpty()) {
            showErrors(List.of("Das Template \"" + templateName + "\" konnte nicht geladen werden."));
            return;
        }
        generateFromTemplate(templateData.get());
    }

    /** Rendert das Rechnungstemplate mit den Formulardaten und wandelt das Ergebnis in eine E-Rechnung um. */
    private void generateFromTemplate(String templateData) {
        try {
            InvoiceMetadata metadata = collectMetadata();
            byte[] templatePdf = templatePdfService.generatePdf(templateData, buildTemplateVariables(metadata), null);
            convertAndOfferDownload(templatePdf);
        } catch (TemplateRenderException e) {
            showErrors(List.of(e.getMessage()));
        }
    }

    /** Belegt die Template-Variablen (masterdata.*, customer.*, Positionen) mit den Formulardaten. */
    private Map<String, String> buildTemplateVariables(InvoiceMetadata metadata) {
        Map<String, String> variables = new LinkedHashMap<>();
        InvoiceMetadata.Party senderParty = metadata.sender();
        variables.put("masterdata.company_name", senderParty.name());
        variables.put("masterdata.street", senderParty.street());
        variables.put("masterdata.city", senderParty.zip() + " " + senderParty.city());
        variables.put("masterdata.phone", senderParty.phone());
        variables.put("masterdata.email", senderParty.email());
        variables.put("masterdata.website", "");
        variables.put("masterdata.sender_line",
                senderParty.name() + " · " + senderParty.street() + " · " + senderParty.zip() + " " + senderParty.city());
        variables.put("masterdata.payment_terms", metadata.paymentTerms());
        StringBuilder footer = new StringBuilder(senderParty.name());
        if (!senderParty.vatId().isEmpty()) {
            footer.append(" · USt-IdNr. ").append(senderParty.vatId());
        }
        if (!metadata.iban().isEmpty()) {
            footer.append("\nIBAN ").append(metadata.iban());
            if (!metadata.bic().isEmpty()) {
                footer.append(" · BIC ").append(metadata.bic());
            }
        }
        variables.put("masterdata.footer", footer.toString());
        variables.put("masterdata.invoice_number", metadata.invoiceNumber());
        variables.put("masterdata.invoice_date",
                metadata.issueDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));

        InvoiceMetadata.Party recipientParty = metadata.recipient();
        variables.put("customer.company_name", recipientParty.name());
        variables.put("customer.contact_name", "");
        variables.put("customer.street", recipientParty.street());
        variables.put("customer.city", recipientParty.zip() + " " + recipientParty.city());
        variables.put("customer.email", recipientParty.email());
        variables.put("customer.phone", recipientParty.phone());

        TemplateVariables.addPositions(variables, metadata.items());
        return variables;
    }

    /** Wandelt das Quell-PDF in die ZUGFeRD-E-Rechnung um und bietet das ZIP zum Download an. */
    private void convertAndOfferDownload(byte[] sourcePdf) {
        try {
            InvoiceMetadata metadata = collectMetadata();
            capturedInvoiceData.setItems(metadata.items());
            ZugferdResult result = zugferdService.createZugferdZip(sourcePdf, metadata);

            StreamResource resource = new StreamResource(result.zipFileName(),
                    () -> new ByteArrayInputStream(result.zip()));
            resource.setContentType("application/zip");
            downloadLink.setHref(resource);
            downloadLink.setText("ZIP herunterladen (" + result.zipFileName() + ")");
            downloadLink.setVisible(true);

            if (result.valid()) {
                Notification.show("E-Rechnung erzeugt und erfolgreich validiert.", 4000,
                                Notification.Position.BOTTOM_START)
                        .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
            } else {
                Notification.show("E-Rechnung erzeugt, aber die Validierung meldet Probleme. "
                                        + "Details stehen im Prüfbericht (validation-report.xml) im ZIP.", 8000,
                                Notification.Position.MIDDLE)
                        .addThemeVariants(NotificationVariant.LUMO_WARNING);
            }
        } catch (ZugferdConversionException e) {
            showErrors(List.of(e.getMessage()));
        }
    }

    /** Prüft alle Eingaben, markiert ungültige Felder und liefert die Fehlermeldungen. */
    private List<String> validateForm() {
        List<String> errors = new ArrayList<>();
        requireFilled(invoiceNumber, "Rechnungsnummer ist ein Pflichtfeld.", errors);
        if (issueDate.isEmpty()) {
            issueDate.setInvalid(true);
            errors.add("Rechnungsdatum ist ein Pflichtfeld.");
        } else {
            issueDate.setInvalid(false);
        }
        if (requireFilled(currency, "Währung ist ein Pflichtfeld.", errors)
                && !currency.getValue().trim().matches("[A-Za-z]{3}")) {
            markInvalid(currency, "Währung als dreistelliger ISO-Code, z. B. EUR.", errors);
        }
        if (!iban.isEmpty() && !isValidIban(iban.getValue())) {
            markInvalid(iban, "IBAN ist ungültig (Prüfsumme).", errors);
        } else {
            iban.setInvalid(false);
        }
        if (!bic.isEmpty() && !bic.getValue().trim().matches("[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?")) {
            markInvalid(bic, "BIC muss 8 oder 11 Stellen haben, z. B. NOLADE21RZB.", errors);
        } else {
            bic.setInvalid(false);
        }
        errors.addAll(sender.validateFields("Rechnungssteller"));
        errors.addAll(recipient.validateFields("Rechnungsempfänger"));
        if (lineItems.isEmpty()) {
            errors.add("Mindestens eine Rechnungsposition erfassen.");
        }
        return errors;
    }

    private InvoiceMetadata collectMetadata() {
        return new InvoiceMetadata(
                invoiceNumber.getValue().trim(),
                issueDate.getValue(),
                deliveryDate.getValue(),
                dueDate.getValue(),
                currency.getValue().trim().toUpperCase(),
                paymentTerms.getValue(),
                buyerReference.getValue().trim(),
                iban.getValue().replaceAll("\\s", "").toUpperCase(),
                bic.getValue().trim().toUpperCase(),
                sender.toParty(),
                recipient.toParty(),
                lineItems
        );
    }

    private static String nullToEmpty(String value) {
        return value != null ? value : "";
    }

    /** Formatiert einen Betrag bzw. eine Zahl für die Positionstabelle (deutsches Format). */
    private static String formatDecimal(BigDecimal value) {
        return value != null ? NumberFormat.getNumberInstance(Locale.GERMANY).format(value) : "";
    }

    /** Markiert das Feld als ungültig, wenn es leer ist, und ergänzt die Fehlermeldung. */
    private static boolean requireFilled(TextField field, String message, List<String> errors) {
        if (field.getValue() == null || field.getValue().isBlank()) {
            markInvalid(field, message, errors);
            return false;
        }
        field.setInvalid(false);
        return true;
    }

    private static void markInvalid(TextField field, String message, List<String> errors) {
        field.setErrorMessage(message);
        field.setInvalid(true);
        errors.add(message);
    }

    private static void markInvalid(BigDecimalField field, String message, List<String> errors) {
        field.setErrorMessage(message);
        field.setInvalid(true);
        errors.add(message);
    }

    /** IBAN-Prüfung nach ISO 13616 (MOD 97-10). */
    private static boolean isValidIban(String value) {
        String normalized = value.replaceAll("\\s", "").toUpperCase();
        if (!normalized.matches("[A-Z]{2}\\d{2}[A-Z0-9]{11,30}")) {
            return false;
        }
        String rearranged = normalized.substring(4) + normalized.substring(0, 4);
        int mod = 0;
        for (char c : rearranged.toCharArray()) {
            int digit = Character.isDigit(c) ? c - '0' : c - 'A' + 10;
            mod = digit < 10 ? (mod * 10 + digit) % 97 : (mod * 100 + digit) % 97;
        }
        return mod == 1;
    }

    private void showErrors(List<String> messages) {
        Notification notification = new Notification();
        notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
        notification.setPosition(Notification.Position.MIDDLE);
        notification.setDuration(8000);
        Div text = new Div();
        text.getStyle().set("white-space", "pre-line");
        text.setText(String.join("\n", messages));
        notification.add(text);
        notification.open();
    }

    /**
     * Adressformular für Rechnungssteller bzw. -empfänger.
     */
    private static class PartyForm extends FormLayout {

        static final String EMAIL_PATTERN = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$";

        private final boolean vatRequired;
        private final TextField name = new TextField("Firma / Name");
        private final TextField street = new TextField("Straße und Hausnummer");
        private final TextField zip = new TextField("PLZ");
        private final TextField city = new TextField("Ort");
        private final TextField countryCode = new TextField("Land (ISO-Code, z.B. DE)");
        private final TextField vatId = new TextField("USt-IdNr.");
        private final EmailField email = new EmailField("E-Mail");
        private final TextField phone = new TextField("Telefon");

        PartyForm(boolean vatRequired) {
            this.vatRequired = vatRequired;
            name.setRequired(true);
            street.setRequired(true);
            zip.setRequired(true);
            city.setRequired(true);
            countryCode.setRequired(true);
            countryCode.setValue("DE");
            countryCode.setMaxLength(2);
            countryCode.setPattern("[A-Za-z]{2}");
            countryCode.setErrorMessage("Zweistelliger ISO-Code, z. B. DE.");
            vatId.setRequired(vatRequired);
            vatId.setHelperText("z. B. DE261094748");
            // USt-IdNr. direkt beim Verlassen des Feldes prüfen
            vatId.addValueChangeListener(event -> checkVatId());
            // Elektronische Adresse (BT-34/BT-49) ist nach PEPPOL-EN16931 Pflicht
            email.setRequiredIndicatorVisible(true);
            email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
            // E-Mail direkt beim Verlassen des Feldes prüfen
            email.addValueChangeListener(event -> checkEmail());
            if (vatRequired) {
                phone.setHelperText("Für den Verkäufer-Kontakt (BR-DE-6) empfohlen");
            }
            add(name, street, zip, city, countryCode, vatId, email, phone);
            setResponsiveSteps(new ResponsiveStep("0", 1), new ResponsiveStep("600px", 3));
        }

        void prefill(String nameValue, String streetValue, String zipValue, String cityValue,
                     String countryValue, String vatIdValue, String emailValue, String phoneValue) {
            name.setValue(nameValue);
            street.setValue(streetValue);
            zip.setValue(zipValue);
            city.setValue(cityValue);
            countryCode.setValue(countryValue);
            vatId.setValue(vatIdValue);
            email.setValue(emailValue);
            phone.setValue(phoneValue);
        }

        /** Meldet jede Wertänderung eines Feldes, z. B. für die Dirty-Erkennung. */
        void onAnyChange(Runnable listener) {
            name.addValueChangeListener(event -> listener.run());
            street.addValueChangeListener(event -> listener.run());
            zip.addValueChangeListener(event -> listener.run());
            city.addValueChangeListener(event -> listener.run());
            countryCode.addValueChangeListener(event -> listener.run());
            vatId.addValueChangeListener(event -> listener.run());
            email.addValueChangeListener(event -> listener.run());
            phone.addValueChangeListener(event -> listener.run());
        }

        /** Leert alle Felder; das Land behält den Standardwert "DE". */
        void clearFields() {
            prefill("", "", "", "", "DE", "", "", "");
            name.setInvalid(false);
            street.setInvalid(false);
            zip.setInvalid(false);
            city.setInvalid(false);
            countryCode.setInvalid(false);
            vatId.setInvalid(false);
            email.setInvalid(false);
        }

        /** @return true, wenn die E-Mail-Adresse gültig (oder leer) ist. */
        private boolean checkEmail() {
            String value = email.getValue();
            if (value == null || value.isBlank()) {
                email.setInvalid(false);
                return true;
            }
            boolean valid = value.matches(EMAIL_PATTERN);
            email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
            email.setInvalid(!valid);
            return valid;
        }

        /** @return true, wenn die USt-IdNr. gültig (oder leer) ist. */
        private boolean checkVatId() {
            String value = vatId.getValue();
            if (value == null || value.isBlank()) {
                vatId.setInvalid(false);
                return true;
            }
            Optional<String> error = VatIdValidator.validate(value);
            vatId.setErrorMessage(error.orElse(null));
            vatId.setInvalid(error.isPresent());
            return error.isEmpty();
        }

        List<String> validateFields(String partyLabel) {
            List<String> errors = new ArrayList<>();
            requireFilled(name, partyLabel + ": Firma / Name ist ein Pflichtfeld.", errors);
            requireFilled(street, partyLabel + ": Straße und Hausnummer ist ein Pflichtfeld.", errors);
            requireFilled(zip, partyLabel + ": PLZ ist ein Pflichtfeld.", errors);
            requireFilled(city, partyLabel + ": Ort ist ein Pflichtfeld.", errors);
            if (requireFilled(countryCode, partyLabel + ": Land ist ein Pflichtfeld.", errors)
                    && !countryCode.getValue().trim().matches("[A-Za-z]{2}")) {
                markInvalid(countryCode, partyLabel + ": Land muss ein zweistelliger ISO-Code sein.", errors);
            }
            if (vatRequired && vatId.isEmpty()) {
                markInvalid(vatId, partyLabel + ": USt-IdNr. ist ein Pflichtfeld.", errors);
            } else if (!checkVatId()) {
                errors.add(partyLabel + ": " + vatId.getErrorMessage());
            }
            if (email.isEmpty()) {
                email.setErrorMessage(partyLabel + ": E-Mail ist ein Pflichtfeld.");
                email.setInvalid(true);
                errors.add(partyLabel + ": E-Mail ist ein Pflichtfeld (elektronische Adresse der E-Rechnung).");
            } else if (!checkEmail()) {
                errors.add(partyLabel + ": E-Mail-Adresse ist ungültig.");
            }
            return errors;
        }

        InvoiceMetadata.Party toParty() {
            return new InvoiceMetadata.Party(
                    name.getValue().trim(),
                    street.getValue().trim(),
                    zip.getValue().trim(),
                    city.getValue().trim(),
                    countryCode.getValue().trim().toUpperCase(),
                    vatId.isEmpty() ? "" : VatIdValidator.normalize(vatId.getValue()),
                    email.getValue().trim(),
                    phone.getValue().trim()
            );
        }
    }

}
