package de.assecutor.votianlt.service;

import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
import de.assecutor.votianlt.model.invoices.InvoiceAuditAction;
import de.assecutor.votianlt.model.invoices.InvoiceAuditEntry;
import de.assecutor.votianlt.model.invoices.InvoiceStatus;
import de.assecutor.votianlt.model.invoices.InvoiceType;
import de.assecutor.votianlt.model.invoices.PaymentStatus;
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
import de.assecutor.votianlt.security.SecurityService;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

/**
 * Verwaltet den Lebenszyklus einer {@link CustomerInvoice} gemäß den Regeln aus
 * <code>invoices_rules.md</code>.
 *
 * Phase 1 stellt die Status- und Audit-Mechanik bereit:
 * <ul>
 *   <li>R-02/R-03: Entwürfe sind editier-/löschbar, finalisierte Belege nicht.</li>
 *   <li>R-07: Finalisierung markiert eine Rechnung als verbindlich.</li>
 *   <li>R-08/R-11: Verhindert Doppelvergabe der Rechnungsnummer und unsichtbare Änderungen.</li>
 *   <li>R-35: Direktes Löschen finalisierter Belege wird abgelehnt.</li>
 *   <li>R-36 bis R-39: Jede Statusänderung wird im Audit-Log protokolliert.</li>
 * </ul>
 *
 * Korrektur-/Storno-Workflows folgen in Phase 2; entsprechende Hooks werden hier vorbereitet.
 */
@Service
public class InvoiceLifecycleService {

    private static final Logger log = LoggerFactory.getLogger(InvoiceLifecycleService.class);

    private final CustomerInvoiceRepository invoiceRepository;
    private final SecurityService securityService;
    private final InvoiceComplianceValidator complianceValidator;
    private final InvoiceNumberAuditService numberAuditService;

    public InvoiceLifecycleService(CustomerInvoiceRepository invoiceRepository, SecurityService securityService,
            InvoiceComplianceValidator complianceValidator,
            InvoiceNumberAuditService numberAuditService) {
        this.invoiceRepository = invoiceRepository;
        this.securityService = securityService;
        this.complianceValidator = complianceValidator;
        this.numberAuditService = numberAuditService;
    }

    /**
     * Persistiert einen neu erzeugten Rechnungsentwurf (Status DRAFT).
     */
    public CustomerInvoice createDraft(CustomerInvoice draft, String reason) {
        if (draft == null) {
            throw new IllegalArgumentException("Rechnungsentwurf darf nicht null sein.");
        }
        draft.setStatus(InvoiceStatus.DRAFT);
        if (draft.getType() == null) {
            draft.setType(InvoiceType.INVOICE);
        }
        draft.addAuditEntry(audit(InvoiceAuditAction.CREATED_DRAFT, reason));
        return invoiceRepository.save(draft);
    }

    /**
     * Erzeugt eine Rechnung und finalisiert sie unmittelbar (Status ISSUED).
     * Wird vom bestehenden Erstell-Flow verwendet, der Vorschau und Speichern in
     * einem Schritt vereint (R-06/R-07).
     */
    public CustomerInvoice createAndIssue(CustomerInvoice invoice, String reason) {
        if (invoice == null) {
            throw new IllegalArgumentException("Rechnung darf nicht null sein.");
        }
        if (invoice.getType() == null) {
            invoice.setType(InvoiceType.INVOICE);
        }
        complianceValidator.validateForIssuance(invoice);
        ensureInvoiceNumberUnique(invoice);
        invoice.setStatus(InvoiceStatus.ISSUED);
        invoice.setIssuedAt(LocalDateTime.now());
        invoice.addAuditEntry(audit(InvoiceAuditAction.CREATED_DRAFT, reason));
        invoice.addAuditEntry(audit(InvoiceAuditAction.ISSUED, reason));
        CustomerInvoice saved = invoiceRepository.save(invoice);
        numberAuditService.markUsed(saved);
        return saved;
    }

    /**
     * Speichert Änderungen an einem bestehenden Entwurf (R-02/R-05).
     * Lehnt Änderungen an finalisierten Rechnungen ab (R-03/R-08).
     */
    public CustomerInvoice updateDraft(CustomerInvoice draft, String reason) {
        if (draft == null || draft.getId() == null) {
            throw new IllegalArgumentException("Bestehender Entwurf erwartet.");
        }
        CustomerInvoice persisted = invoiceRepository.findById(draft.getId())
                .orElseThrow(() -> new IllegalStateException("Rechnung nicht gefunden: " + draft.getId()));
        if (!persisted.getStatus().isMutable()) {
            throw new InvoiceLifecycleException(
                    "Diese Rechnung ist bereits ausgestellt und kann nicht mehr direkt bearbeitet werden. "
                            + "Bitte erstellen Sie eine Berichtigung oder ein Storno.");
        }
        draft.setStatus(InvoiceStatus.DRAFT);
        draft.setAuditLog(persisted.getAuditLog());
        draft.addAuditEntry(audit(InvoiceAuditAction.UPDATED_DRAFT, reason));
        return invoiceRepository.save(draft);
    }

    /**
     * Finalisiert einen Entwurf (Status ISSUED). Stellt sicher, dass die Rechnungsnummer
     * eindeutig ist (R-11) und protokolliert den Wechsel.
     */
    public CustomerInvoice issue(String invoiceId, String reason) {
        CustomerInvoice invoice = requireInvoice(invoiceId);
        if (invoice.getStatus() == InvoiceStatus.ISSUED || invoice.getStatus() == InvoiceStatus.SENT) {
            return invoice;
        }
        if (invoice.getStatus() != InvoiceStatus.DRAFT) {
            throw new InvoiceLifecycleException(
                    "Nur Entwürfe können ausgestellt werden. Aktueller Status: " + invoice.getStatus());
        }
        complianceValidator.validateForIssuance(invoice);
        ensureInvoiceNumberUnique(invoice);
        invoice.setStatus(InvoiceStatus.ISSUED);
        invoice.setIssuedAt(LocalDateTime.now());
        invoice.addAuditEntry(audit(InvoiceAuditAction.ISSUED, reason));
        CustomerInvoice saved = invoiceRepository.save(invoice);
        numberAuditService.markUsed(saved);
        return saved;
    }

    /**
     * Markiert eine ausgestellte Rechnung als versendet (R-08).
     */
    public CustomerInvoice markAsSent(String invoiceId, String reason) {
        CustomerInvoice invoice = requireInvoice(invoiceId);
        if (invoice.getStatus() == InvoiceStatus.DRAFT) {
            throw new InvoiceLifecycleException(
                    "Eine Rechnung muss vor dem Versand zunächst ausgestellt werden.");
        }
        if (invoice.getStatus() == InvoiceStatus.CANCELLED) {
            throw new InvoiceLifecycleException("Eine stornierte Rechnung kann nicht mehr versendet werden.");
        }
        invoice.setStatus(InvoiceStatus.SENT);
        invoice.setSentAt(LocalDateTime.now());
        invoice.addAuditEntry(audit(InvoiceAuditAction.SENT, reason));
        return invoiceRepository.save(invoice);
    }

    /**
     * Erzeugt einen Stornobeleg zu einer bereits ausgestellten Rechnung (R-17 bis R-22).
     *
     * Der Stornobeleg ist ein eigenständiger Beleg vom Typ {@link InvoiceType#CANCELLATION}
     * mit eigener (neuer) Rechnungsnummer. Die Originalrechnung wird auf
     * {@link InvoiceStatus#CANCELLED} gesetzt; Original und Storno sind über
     * {@code originalInvoiceId} bzw. {@code cancellationInvoiceId} verlinkt.
     *
     * @param originalId        ID der zu stornierenden Rechnung
     * @param cancellationNumber neue, fortlaufende Rechnungsnummer für den Stornobeleg
     * @param cancellationDate   Belegdatum des Stornos
     * @param pdfData            generiertes PDF des Stornobelegs
     * @param reason             nachvollziehbarer Grund (R-36)
     */
    public CustomerInvoice cancel(String originalId, String cancellationNumber, LocalDate cancellationDate,
            byte[] pdfData, String reason) {
        CustomerInvoice original = requireInvoice(originalId);
        if (original.getType() != InvoiceType.INVOICE) {
            throw new InvoiceLifecycleException(
                    "Nur reguläre Rechnungen können storniert werden. Belegtyp: " + original.getType());
        }
        if (original.getStatus() == InvoiceStatus.CANCELLED) {
            throw new InvoiceLifecycleException("Diese Rechnung ist bereits storniert.");
        }
        if (original.getStatus() == InvoiceStatus.DRAFT) {
            throw new InvoiceLifecycleException(
                    "Ein Entwurf wird nicht storniert, sondern gelöscht oder bearbeitet.");
        }
        if (cancellationNumber == null || cancellationNumber.isBlank()) {
            throw new InvoiceLifecycleException("Stornobeleg benötigt eine fortlaufende Belegnummer.");
        }

        CustomerInvoice cancellation = new CustomerInvoice();
        cancellation.setType(InvoiceType.CANCELLATION);
        cancellation.setStatus(InvoiceStatus.ISSUED);
        cancellation.setInvoiceNumber(cancellationNumber);
        cancellation.setInvoiceDate(cancellationDate != null ? cancellationDate : LocalDate.now());
        cancellation.setIssuedAt(LocalDateTime.now());
        cancellation.setUserId(original.getUserId());
        cancellation.setJobId(original.getJobId());
        cancellation.setOriginalInvoiceId(original.getId());
        cancellation.setOriginalInvoiceNumber(original.getInvoiceNumber());
        cancellation.setOriginalInvoiceDate(original.getInvoiceDate());

        // Empfänger-/Sender-Daten übernehmen für vollständige Pflichtangaben
        copyParties(original, cancellation);
        cancellation.setItems(original.getItems());

        // Beträge negieren – Storno bucht den Originalbetrag aus
        cancellation.setNetAmount(negate(original.getNetAmount()));
        cancellation.setVatRate(original.getVatRate());
        cancellation.setVatAmount(negate(original.getVatAmount()));
        cancellation.setTotalAmount(negate(original.getTotalAmount()));

        cancellation.setDescription("Stornorechnung zu Rechnung " + original.getInvoiceNumber());
        cancellation.setPdfData(pdfData);
        cancellation.addAuditEntry(audit(InvoiceAuditAction.CREATED_DRAFT, reason));
        InvoiceAuditEntry issuedEntry = audit(InvoiceAuditAction.ISSUED, reason);
        issuedEntry.setResultingInvoiceNumber(cancellationNumber);
        cancellation.addAuditEntry(issuedEntry);

        ensureInvoiceNumberUnique(cancellation);
        CustomerInvoice savedCancellation = invoiceRepository.save(cancellation);
        numberAuditService.markUsed(savedCancellation);

        // Original markieren und verlinken
        original.setStatus(InvoiceStatus.CANCELLED);
        original.setCancelledAt(LocalDateTime.now());
        original.setCancellationInvoiceId(savedCancellation.getId());
        // Wenn die Originalrechnung bereits (teil-)bezahlt war, entsteht ein Erstattungsanspruch (R-26)
        BigDecimal paid = original.getPaidAmount() != null ? original.getPaidAmount() : BigDecimal.ZERO;
        original.setPaymentStatus(computePaymentStatus(original, paid));
        InvoiceAuditEntry cancelEntry = audit(InvoiceAuditAction.CANCELLED, reason);
        cancelEntry.setResultingInvoiceId(savedCancellation.getId());
        cancelEntry.setResultingInvoiceNumber(savedCancellation.getInvoiceNumber());
        original.addAuditEntry(cancelEntry);
        invoiceRepository.save(original);

        log.info("Rechnung {} storniert durch Beleg {}.", original.getInvoiceNumber(),
                savedCancellation.getInvoiceNumber());
        return savedCancellation;
    }

    /**
     * Erzeugt einen Berichtigungsbeleg zu einer bereits ausgestellten Rechnung (R-12 bis R-16).
     *
     * Eine Berichtigung adressiert formale Fehler (Adresse, Leistungsdatum, Pflichtangabe).
     * Sie ersetzt die Originalrechnung nicht, sondern verweist auf sie. Originalrechnung
     * wechselt in den Status {@link InvoiceStatus#CORRECTED} und hält eine Referenz auf den
     * Berichtigungsbeleg.
     *
     * @param originalId         ID der zu berichtigenden Rechnung
     * @param correctionNumber   fortlaufende Belegnummer für den Berichtigungsbeleg
     * @param correctionDate     Belegdatum
     * @param pdfData            generiertes PDF des Berichtigungsbelegs
     * @param correctedFields    Beschreibung der ergänzten/korrigierten Angaben (R-14)
     * @param reason             Grund der Berichtigung (R-36)
     */
    public CustomerInvoice correct(String originalId, String correctionNumber, LocalDate correctionDate,
            byte[] pdfData, String correctedFields, String reason) {
        CustomerInvoice original = requireInvoice(originalId);
        if (original.getType() != InvoiceType.INVOICE) {
            throw new InvoiceLifecycleException(
                    "Nur reguläre Rechnungen können berichtigt werden. Belegtyp: " + original.getType());
        }
        if (original.getStatus() == InvoiceStatus.DRAFT) {
            throw new InvoiceLifecycleException(
                    "Ein Entwurf wird nicht berichtigt, sondern direkt bearbeitet.");
        }
        if (original.getStatus() == InvoiceStatus.CANCELLED) {
            throw new InvoiceLifecycleException(
                    "Eine bereits stornierte Rechnung kann nicht berichtigt werden. Erstellen Sie eine neue Rechnung.");
        }
        if (correctionNumber == null || correctionNumber.isBlank()) {
            throw new InvoiceLifecycleException("Berichtigungsbeleg benötigt eine fortlaufende Belegnummer.");
        }

        CustomerInvoice correction = new CustomerInvoice();
        correction.setType(InvoiceType.CORRECTION);
        correction.setStatus(InvoiceStatus.ISSUED);
        correction.setInvoiceNumber(correctionNumber);
        correction.setInvoiceDate(correctionDate != null ? correctionDate : LocalDate.now());
        correction.setIssuedAt(LocalDateTime.now());
        correction.setUserId(original.getUserId());
        correction.setJobId(original.getJobId());
        correction.setOriginalInvoiceId(original.getId());
        correction.setOriginalInvoiceNumber(original.getInvoiceNumber());
        correction.setOriginalInvoiceDate(original.getInvoiceDate());

        copyParties(original, correction);
        correction.setItems(original.getItems());
        correction.setNetAmount(original.getNetAmount());
        correction.setVatRate(original.getVatRate());
        correction.setVatAmount(original.getVatAmount());
        correction.setTotalAmount(original.getTotalAmount());

        String descriptionPrefix = "Berichtigung zu Rechnung " + original.getInvoiceNumber();
        correction.setDescription(
                correctedFields == null || correctedFields.isBlank() ? descriptionPrefix
                        : descriptionPrefix + " — " + correctedFields);
        correction.setPdfData(pdfData);
        correction.addAuditEntry(audit(InvoiceAuditAction.CREATED_DRAFT, reason));
        InvoiceAuditEntry issuedEntry = audit(InvoiceAuditAction.ISSUED, reason);
        issuedEntry.setResultingInvoiceNumber(correctionNumber);
        correction.addAuditEntry(issuedEntry);

        ensureInvoiceNumberUnique(correction);
        CustomerInvoice savedCorrection = invoiceRepository.save(correction);
        numberAuditService.markUsed(savedCorrection);

        original.setStatus(InvoiceStatus.CORRECTED);
        original.setCorrectionInvoiceId(savedCorrection.getId());
        InvoiceAuditEntry correctEntry = audit(InvoiceAuditAction.CORRECTED, reason);
        correctEntry.setResultingInvoiceId(savedCorrection.getId());
        correctEntry.setResultingInvoiceNumber(savedCorrection.getInvoiceNumber());
        original.addAuditEntry(correctEntry);
        invoiceRepository.save(original);

        log.info("Rechnung {} berichtigt durch Beleg {}.", original.getInvoiceNumber(),
                savedCorrection.getInvoiceNumber());
        return savedCorrection;
    }

    /**
     * Speichert eine vollständig neue Ersatzrechnung nach einem Storno (R-20 bis R-22).
     * Die neue Rechnung erhält eine eigene Rechnungsnummer und referenziert die stornierte
     * Originalrechnung, sodass die Verkettung Original → Storno → Ersatzrechnung erhalten bleibt.
     */
    public CustomerInvoice createReplacementInvoice(String cancelledOriginalId, CustomerInvoice replacement,
            String reason) {
        CustomerInvoice cancelledOriginal = requireInvoice(cancelledOriginalId);
        if (cancelledOriginal.getStatus() != InvoiceStatus.CANCELLED) {
            throw new InvoiceLifecycleException(
                    "Eine Ersatzrechnung kann nur zu einer stornierten Rechnung erstellt werden.");
        }
        if (replacement == null) {
            throw new IllegalArgumentException("Ersatzrechnung darf nicht null sein.");
        }
        replacement.setType(InvoiceType.INVOICE);
        replacement.setOriginalInvoiceId(cancelledOriginal.getId());
        replacement.setOriginalInvoiceNumber(cancelledOriginal.getInvoiceNumber());
        replacement.setOriginalInvoiceDate(cancelledOriginal.getInvoiceDate());

        CustomerInvoice savedReplacement = createAndIssue(replacement,
                reason != null ? reason : "Ersatzrechnung nach Storno");

        cancelledOriginal.setReplacementInvoiceId(savedReplacement.getId());
        InvoiceAuditEntry replaceEntry = audit(InvoiceAuditAction.REPLACED, reason);
        replaceEntry.setResultingInvoiceId(savedReplacement.getId());
        replaceEntry.setResultingInvoiceNumber(savedReplacement.getInvoiceNumber());
        cancelledOriginal.addAuditEntry(replaceEntry);
        invoiceRepository.save(cancelledOriginal);

        return savedReplacement;
    }

    /**
     * Liefert die Folgebelege (Storno, Berichtigung, Ersatzrechnung) zu einer Rechnung.
     */
    public List<CustomerInvoice> findRelatedDocuments(String originalInvoiceId) {
        if (originalInvoiceId == null) {
            return List.of();
        }
        return invoiceRepository.findByOriginalInvoiceId(originalInvoiceId);
    }

    /**
     * Erfasst eine Zahlung zur Rechnung (R-23 bis R-26).
     *
     * Eine Zahlung wird ausschließlich erfasst – die Rechnungsdaten selbst werden nicht
     * verändert (R-25). Der Zahlungsstatus ergibt sich aus dem Verhältnis Zahlung zu
     * Bruttobetrag der Rechnung.
     *
     * @param invoiceId       Rechnung, auf die gebucht wird
     * @param amount          erfasster Zahlbetrag (kann negativ sein für Korrekturen)
     * @param paymentReference frei wählbarer Referenztext (z.B. Kontoauszug, Beleg)
     * @param reason           erläuternder Grund für das Audit-Log
     */
    public CustomerInvoice registerPayment(String invoiceId, BigDecimal amount, String paymentReference,
            String reason) {
        if (amount == null) {
            throw new IllegalArgumentException("Zahlbetrag erforderlich.");
        }
        CustomerInvoice invoice = requireInvoice(invoiceId);
        if (invoice.getStatus() == InvoiceStatus.DRAFT) {
            throw new InvoiceLifecycleException(
                    "Auf Entwürfen können keine Zahlungen erfasst werden. Bitte zuerst ausstellen.");
        }
        if (invoice.getType() == InvoiceType.CORRECTION) {
            throw new InvoiceLifecycleException(
                    "Auf Berichtigungsbelegen werden keine Zahlungen erfasst – buchen Sie auf der Originalrechnung.");
        }

        BigDecimal previous = invoice.getPaidAmount() != null ? invoice.getPaidAmount() : BigDecimal.ZERO;
        BigDecimal newPaid = previous.add(amount);
        invoice.setPaidAmount(newPaid);
        invoice.setLastPaymentAt(LocalDateTime.now());
        invoice.setPaymentStatus(computePaymentStatus(invoice, newPaid));

        StringBuilder logReason = new StringBuilder();
        logReason.append("Zahlung erfasst: ").append(amount.toPlainString());
        if (paymentReference != null && !paymentReference.isBlank()) {
            logReason.append(" (Referenz: ").append(paymentReference).append(")");
        }
        if (reason != null && !reason.isBlank()) {
            logReason.append(" – ").append(reason);
        }
        invoice.addAuditEntry(audit(InvoiceAuditAction.PAYMENT_RECORDED, logReason.toString()));

        return invoiceRepository.save(invoice);
    }

    /**
     * Liefert den noch offenen Betrag einer Rechnung. Negative Werte zeigen eine
     * Überzahlung bzw. einen Erstattungsanspruch (R-26).
     */
    public BigDecimal computeOutstandingAmount(CustomerInvoice invoice) {
        BigDecimal total = invoice.getTotalAmount() != null ? invoice.getTotalAmount() : BigDecimal.ZERO;
        BigDecimal paid = invoice.getPaidAmount() != null ? invoice.getPaidAmount() : BigDecimal.ZERO;
        return total.subtract(paid);
    }

    private PaymentStatus computePaymentStatus(CustomerInvoice invoice, BigDecimal paid) {
        BigDecimal total = invoice.getTotalAmount() != null ? invoice.getTotalAmount() : BigDecimal.ZERO;
        if (invoice.getStatus() == InvoiceStatus.CANCELLED) {
            return paid.signum() == 0 ? PaymentStatus.UNPAID : PaymentStatus.REFUND_DUE;
        }
        int cmp = paid.compareTo(total);
        if (paid.signum() == 0) {
            return PaymentStatus.UNPAID;
        }
        if (cmp == 0) {
            return PaymentStatus.PAID;
        }
        if (cmp < 0) {
            return PaymentStatus.PARTIALLY_PAID;
        }
        return PaymentStatus.OVERPAID;
    }

    /**
     * Löscht einen Entwurf. Finalisierte Rechnungen dürfen nicht gelöscht werden (R-35).
     */
    public void deleteDraft(String invoiceId, String reason) {
        CustomerInvoice invoice = requireInvoice(invoiceId);
        if (!invoice.getStatus().isMutable()) {
            throw new InvoiceLifecycleException(
                    "Finalisierte Rechnungen können nicht gelöscht werden. "
                            + "Bitte führen Sie stattdessen ein Storno oder eine Berichtigung durch.");
        }
        invoice.addAuditEntry(audit(InvoiceAuditAction.DELETED_DRAFT, reason));
        log.info("Rechnungsentwurf {} wird gelöscht: {}", invoiceId, reason);
        invoiceRepository.delete(invoice);
        // Wenn dem Entwurf bereits eine Nummer reserviert wurde, dokumentiert das
        // Verwerfen jetzt die Lücke im Nummernkreis — sonst bliebe die Reservierung
        // als unerklärter „RESERVED"-Eintrag im Audit hängen.
        if (invoice.getInvoiceNumber() != null && !invoice.getInvoiceNumber().isBlank()
                && invoice.getUserId() != null) {
            try {
                ObjectId userId = new ObjectId(invoice.getUserId());
                String voidReason = (reason != null && !reason.isBlank())
                        ? reason
                        : "Rechnungsentwurf gelöscht";
                numberAuditService.markVoided(userId, invoice.getInvoiceNumber(), voidReason);
            } catch (IllegalArgumentException | InvoiceLifecycleException ex) {
                // Keine Reservierung vorhanden oder UserId nicht parsebar — Lücken-Detektor
                // erfasst das später; das Löschen selbst soll nicht blockiert werden.
                log.debug("VOIDED-Markierung beim Löschen des Entwurfs übersprungen: {}", ex.getMessage());
            }
        }
    }

    public Optional<CustomerInvoice> findById(String invoiceId) {
        return invoiceRepository.findById(invoiceId);
    }

    private CustomerInvoice requireInvoice(String invoiceId) {
        if (invoiceId == null || invoiceId.isBlank()) {
            throw new IllegalArgumentException("Rechnungs-ID erforderlich.");
        }
        return invoiceRepository.findById(invoiceId)
                .orElseThrow(() -> new IllegalStateException("Rechnung nicht gefunden: " + invoiceId));
    }

    private void ensureInvoiceNumberUnique(CustomerInvoice invoice) {
        String number = invoice.getInvoiceNumber();
        if (number == null || number.isBlank()) {
            throw new InvoiceLifecycleException("Eine ausgestellte Rechnung benötigt eine Rechnungsnummer.");
        }
        invoiceRepository.findByInvoiceNumberAndStatusNot(number, InvoiceStatus.CANCELLED).ifPresent(existing -> {
            if (!existing.getId().equals(invoice.getId())) {
                throw new InvoiceLifecycleException(
                        "Rechnungsnummer " + number + " wird bereits von einer aktiven Rechnung verwendet.");
            }
        });
    }

    private InvoiceAuditEntry audit(InvoiceAuditAction action, String reason) {
        String userId = null;
        String displayName = "system";
        try {
            User user = securityService.getCurrentDatabaseUser();
            userId = user.getId() != null ? user.getId().toHexString() : null;
            String composed = (safe(user.getFirstname()) + " " + safe(user.getName())).trim();
            displayName = composed.isBlank() ? safe(user.getEmail()) : composed;
        } catch (Exception ignored) {
            // Audit funktioniert auch außerhalb einer Vaadin-Session (z.B. Migration).
        }
        return new InvoiceAuditEntry(action, userId, displayName, reason);
    }

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

    private void copyParties(CustomerInvoice source, CustomerInvoice target) {
        target.setSenderName(source.getSenderName());
        target.setSenderAddress(source.getSenderAddress());
        target.setSenderPostcode(source.getSenderPostcode());
        target.setSenderCity(source.getSenderCity());
        target.setSenderCountry(source.getSenderCountry());
        target.setSenderTaxNumber(source.getSenderTaxNumber());
        target.setSenderVatId(source.getSenderVatId());
        target.setSenderPhone(source.getSenderPhone());
        target.setSenderEmail(source.getSenderEmail());
        target.setSenderWebsite(source.getSenderWebsite());

        target.setRecipientName(source.getRecipientName());
        target.setRecipientCompany(source.getRecipientCompany());
        target.setRecipientAddress(source.getRecipientAddress());
        target.setRecipientPostcode(source.getRecipientPostcode());
        target.setRecipientCity(source.getRecipientCity());
        target.setRecipientCountry(source.getRecipientCountry());
        target.setRecipientVatId(source.getRecipientVatId());
    }

    private BigDecimal negate(BigDecimal value) {
        return value != null ? value.negate() : null;
    }
}
