package de.assecutor.votianlt.pages.service;

import de.assecutor.votianlt.model.UserInvoiceData;
import de.assecutor.votianlt.model.invoices.InvoiceNumberReservation;
import de.assecutor.votianlt.model.invoices.InvoiceNumberReservationStatus;
import de.assecutor.votianlt.repository.InvoiceNumberReservationRepository;
import de.assecutor.votianlt.repository.UserInvoiceDataRepository;
import de.assecutor.votianlt.security.SecurityService;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.Optional;

@Service
public class UserInvoiceDataService {

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

    private final UserInvoiceDataRepository userInvoiceDataRepository;
    private final MongoTemplate mongoTemplate;
    private final InvoiceNumberReservationRepository reservationRepository;
    private final SecurityService securityService;

    public UserInvoiceDataService(UserInvoiceDataRepository userInvoiceDataRepository, MongoTemplate mongoTemplate,
            InvoiceNumberReservationRepository reservationRepository, SecurityService securityService) {
        this.userInvoiceDataRepository = userInvoiceDataRepository;
        this.mongoTemplate = mongoTemplate;
        this.reservationRepository = reservationRepository;
        this.securityService = securityService;
    }

    public Optional<UserInvoiceData> findByUserId(ObjectId userId) {
        return userInvoiceDataRepository.findByUserId(userId);
    }

    public UserInvoiceData save(UserInvoiceData userInvoiceData) {
        userInvoiceData.updateTimestamp();
        return userInvoiceDataRepository.save(userInvoiceData);
    }

    public UserInvoiceData createOrUpdate(ObjectId userId, boolean billingEnabled, String prefix, String ustId,
            String taxNumber, String bankName, String iban, String taxRate, String introText, String paymentTerms) {
        // If billing is disabled, delete any existing record and return null
        if (!billingEnabled) {
            deleteByUserId(userId);
            return null;
        }

        // Otherwise, create or update the record
        UserInvoiceData invoiceData = findByUserId(userId).orElse(new UserInvoiceData());

        invoiceData.setUserId(userId);
        invoiceData.setBillingEnabled(billingEnabled);
        invoiceData.setPrefix(prefix);
        invoiceData.setUstId(ustId);
        invoiceData.setTaxNumber(taxNumber);
        invoiceData.setBankName(bankName);
        invoiceData.setIban(iban);
        invoiceData.setTaxRate(taxRate);
        invoiceData.setIntroText(introText);
        invoiceData.setPaymentTerms(paymentTerms);

        return save(invoiceData);
    }

    public void deleteByUserId(ObjectId userId) {
        userInvoiceDataRepository.deleteByUserId(userId);
    }

    /**
     * Generiert atomar die nächste Rechnungsnummer für den Benutzer und erhöht den
     * Zähler um 1. Gibt die vollständige Rechnungsnummer zurück (Präfix + Nummer).
     *
     * Jede Vergabe wird als {@link InvoiceNumberReservation} mit Status RESERVED
     * persistiert. Damit ist auch nachvollziehbar, wenn eine Nummer aus dem
     * Counter gezogen, aber nie zu einer ausgestellten Rechnung wird (abgebrochener
     * Erstell-Prozess, fehlgeschlagene Validierung). Die Reservierung wird später
     * vom Lifecycle-Service auf USED bzw. VOIDED gesetzt.
     */
    public String generateNextInvoiceNumber(ObjectId userId) {
        Query query = Query.query(Criteria.where("userId").is(userId));
        Update update = new Update().inc("nextInvoiceNumber", 1);
        FindAndModifyOptions options = FindAndModifyOptions.options().returnNew(false).upsert(false);

        UserInvoiceData before = mongoTemplate.findAndModify(query, update, options, UserInvoiceData.class);
        if (before == null) {
            // Kein Eintrag vorhanden - Fallback auf aktuelle Daten
            return findByUserId(userId).map(d -> {
                String prefix = d.getPrefix() != null ? d.getPrefix() : "";
                long sequence = d.getNextInvoiceNumber();
                String number = prefix + String.format("%06d", sequence);
                recordReservation(userId, number, sequence, prefix);
                return number;
            }).orElse("000000");
        }

        String prefix = before.getPrefix() != null ? before.getPrefix() : "";
        long sequence = before.getNextInvoiceNumber();
        String number = prefix + String.format("%06d", sequence);
        recordReservation(userId, number, sequence, prefix);
        return number;
    }

    /**
     * Persistiert die Reservierung einer Nummer. Das Schreiben des Audit-Eintrags
     * ist von der Counter-Vergabe entkoppelt: Sollte das Audit-Repository
     * vorübergehend ausfallen, geht die Nummer-Vergabe nicht verloren — wir
     * loggen den Fehler und vertrauen darauf, dass die anschließende Lücken-
     * Analyse auf Basis der ausgestellten Rechnungen die fehlende Reservierung
     * sichtbar macht.
     */
    private void recordReservation(ObjectId userId, String number, long sequence, String prefix) {
        try {
            InvoiceNumberReservation reservation = new InvoiceNumberReservation();
            reservation.setUserId(userId);
            reservation.setNumber(number);
            reservation.setSequence(sequence);
            reservation.setPrefix(prefix);
            reservation.setReservedAt(Instant.now());
            reservation.setReservedBy(currentUserDisplayName());
            reservation.setStatus(InvoiceNumberReservationStatus.RESERVED);
            reservationRepository.save(reservation);
        } catch (Exception ex) {
            log.warn("Reservierung der Rechnungsnummer {} (User {}) konnte nicht persistiert werden: {}",
                    number, userId, ex.getMessage(), ex);
        }
    }

    private String currentUserDisplayName() {
        try {
            var user = securityService.getCurrentDatabaseUser();
            String composed = (safe(user.getFirstname()) + " " + safe(user.getName())).trim();
            return composed.isBlank() ? safe(user.getEmail()) : composed;
        } catch (Exception ignored) {
            return "system";
        }
    }

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