package com.pressing.modules.operations;

import com.pressing.common.dto.PageResponse;
import com.pressing.common.exception.ApiException;
import com.pressing.domain.catalog.Article;
import com.pressing.domain.catalog.ArticleRepository;
import com.pressing.domain.catalog.ArticleType;
import com.pressing.domain.catalog.ArticleTypeRepository;
import com.pressing.domain.catalog.PricingRepository;
import com.pressing.domain.catalog.StorageLocation;
import com.pressing.domain.catalog.StorageLocationRepository;
import com.pressing.domain.customer.Customer;
import com.pressing.domain.invoice.Invoice;
import com.pressing.domain.invoice.InvoiceLine;
import com.pressing.domain.invoice.InvoiceStatus;
import com.pressing.domain.invoice.PaymentStatus;
import com.pressing.domain.ironing.IroningInvoice;
import com.pressing.domain.ironing.IroningInvoiceLine;
import com.pressing.domain.ironing.IroningInvoiceRepository;
import com.pressing.domain.payment.Payment;
import com.pressing.domain.payment.PaymentRepository;
import com.pressing.domain.pickup.Pickup;
import com.pressing.domain.pickup.PickupLine;
import com.pressing.domain.pickup.PickupRepository;
import com.pressing.domain.storage.StorageAssignment;
import com.pressing.domain.storage.StorageAssignmentRepository;
import com.pressing.domain.wallet.WalletEntry;
import com.pressing.domain.wallet.WalletEntryRepository;
import com.pressing.modules.audit.AuditService;

import com.pressing.modules.catalog.dto.CatalogDtos.LocationResponse;

import com.pressing.modules.common.DomainSupport;
import com.pressing.modules.customer.CustomerService;
import com.pressing.modules.customer.dto.CustomerDtos.CustomerRequest;
import com.pressing.modules.invoice.InvoiceService;
import com.pressing.modules.invoice.dto.InvoiceDtos.InvoicePickupDetailsDto;
import com.pressing.modules.invoice.dto.InvoiceDtos.InvoicePickupLineDto;

import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Predicate;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OperationsService {

    private final PickupRepository pickupRepository;
    private final IroningInvoiceRepository ironingRepository;
    private final PaymentRepository paymentRepository;
    private final WalletEntryRepository walletRepository;
    private final StorageAssignmentRepository assignmentRepository;
    private final StorageLocationRepository locationRepository;
    private final ArticleTypeRepository articleTypeRepository;
    private final ArticleRepository articleRepository;
    private final PricingRepository pricingRepository;
    private final InvoiceService invoiceService;
    private final CustomerService customerService;
    private final DomainSupport domainSupport;
    private final AuditService auditService;

    public OperationsService(
            PickupRepository pickupRepository,
            IroningInvoiceRepository ironingRepository,
            PaymentRepository paymentRepository,
            WalletEntryRepository walletRepository,
            StorageAssignmentRepository assignmentRepository,
            StorageLocationRepository locationRepository,
            ArticleTypeRepository articleTypeRepository,
            ArticleRepository articleRepository,
            PricingRepository pricingRepository,
            InvoiceService invoiceService,
            CustomerService customerService,
            DomainSupport domainSupport,
            AuditService auditService
    ) {
        this.pickupRepository = pickupRepository;
        this.ironingRepository = ironingRepository;
        this.paymentRepository = paymentRepository;
        this.walletRepository = walletRepository;
        this.assignmentRepository = assignmentRepository;
        this.locationRepository = locationRepository;
        this.articleTypeRepository = articleTypeRepository;
        this.articleRepository = articleRepository;
        this.pricingRepository = pricingRepository;
        this.invoiceService = invoiceService;
        this.customerService = customerService;
        this.domainSupport = domainSupport;
        this.auditService = auditService;
    }

    public record PickupLineRequest(@NotNull Long invoiceLineId, @NotNull Integer quantity) {
    }

    public record PickupRequest(@NotNull Long invoiceId, String notes, List<PickupLineRequest> lines) {
    }

    public record PickupHistoryRow(
            Long id,
            LocalDateTime pickupDateTime,
            String invoiceNumber,
            String designation,
            int quantity,
            String customerName,
            String operatorName
    ) {
    }


    @Transactional
public List<PickupHistoryRow> createPickup(PickupRequest req) {
    Invoice invoice = invoiceService.find(req.invoiceId());

    // 0. Ratio remise+taxe de la facture : les lignes stockent le prix UNITAIRE INITIAL
    // (avant remise), qui ne bouge jamais. Sans ce ratio, la valeur des articles retirés
    // est calculée au prix plein alors que "amountPaid" reflète le montant APRÈS remise
    // — dès qu'une remise est appliquée, le retrait se bloquait à tort en réclamant un
    // "solde insuffisant" même facture intégralement soldée (remise non prise en compte).
    BigDecimal invoiceHt = invoice.getLines().stream()
            .map(l -> l.getUnitPrice().multiply(BigDecimal.valueOf(l.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    BigDecimal discountRatio = invoiceHt.compareTo(BigDecimal.ZERO) > 0
            ? invoice.getAmountTtc().divide(invoiceHt, 6, RoundingMode.HALF_UP)
            : BigDecimal.ONE;

    // 1. Calcul de la valeur totale (remisée) des articles demandés dans CE retrait
    BigDecimal requestedPickupAmount = BigDecimal.ZERO;

    for (PickupLineRequest lineReq : req.lines() == null ? List.<PickupLineRequest>of() : req.lines()) {
        InvoiceLine invoiceLine = invoice.getLines().stream()
                .filter(l -> l.getId().equals(lineReq.invoiceLineId()))
                .findFirst()
                .orElseThrow(() -> ApiException.notFound("Ligne de facture introuvable"));

        int remaining = invoiceLine.getQuantity() - invoiceLine.getQuantityPickedUp();
        if (lineReq.quantity() > remaining) {
            throw ApiException.badRequest("Quantité retirée supérieure au reste pour " + invoiceLine.getDesignation());
        }

        // Prix total (remisé) de la quantité demandée pour cet article
        BigDecimal lineAmount = invoiceLine.getUnitPrice()
                .multiply(BigDecimal.valueOf(lineReq.quantity()))
                .multiply(discountRatio);
        requestedPickupAmount = requestedPickupAmount.add(lineAmount);
    }

    // 2. Calcul de la valeur (remisée) déjà retirée auparavant
    BigDecimal alreadyPickedUpAmount = invoice.getLines().stream()
            .map(l -> l.getUnitPrice().multiply(BigDecimal.valueOf(l.getQuantityPickedUp())).multiply(discountRatio))
            .reduce(BigDecimal.ZERO, BigDecimal::add);

    // 3. Vérification : La valeur cumulée (remisée) des retraits ne doit pas dépasser le montant payé
    BigDecimal totalPickupValue = alreadyPickedUpAmount.add(requestedPickupAmount);
    if (totalPickupValue.compareTo(invoice.getAmountPaid()) > 0) {
        BigDecimal allowedValue = invoice.getAmountPaid().subtract(alreadyPickedUpAmount).max(BigDecimal.ZERO);
        throw ApiException.badRequest(
            "Montant payé insuffisant. Le solde actuel vous permet de retirer pour encore " + allowedValue + " FCFA d'articles."
        );
    }

    // 4. Création du retrait et mise à jour des lignes
    Pickup pickup = new Pickup();
    pickup.setSiteId(domainSupport.siteId());
    pickup.setInvoice(invoice);
    pickup.setPickupDateTime(LocalDateTime.now());
    pickup.setOperatorName(domainSupport.operatorName());
    pickup.setNotes(req.notes());

    for (PickupLineRequest lineReq : req.lines() == null ? List.<PickupLineRequest>of() : req.lines()) {
        InvoiceLine invoiceLine = invoice.getLines().stream()
                .filter(l -> l.getId().equals(lineReq.invoiceLineId()))
                .findFirst()
                .get();

        invoiceLine.setQuantityPickedUp(invoiceLine.getQuantityPickedUp() + lineReq.quantity());

        PickupLine pickupLine = new PickupLine();
        pickupLine.setPickup(pickup);
        pickupLine.setInvoiceLine(invoiceLine);
        pickupLine.setQuantity(lineReq.quantity());
        pickupLine.setDesignation(invoiceLine.getDesignation());
        pickup.getLines().add(pickupLine);
    }

    // 5. Mise à jour des statuts
    boolean allPicked = invoice.getLines().stream().allMatch(l -> l.getQuantityPickedUp() >= l.getQuantity());
    boolean anyPicked = invoice.getLines().stream().anyMatch(l -> l.getQuantityPickedUp() > 0);

    if (allPicked) {
        invoice.setStatus(InvoiceStatus.PICKED_UP);
    } else if (anyPicked) {
        invoice.setStatus(InvoiceStatus.PARTIALLY_PICKED_UP);
    }

    pickupRepository.save(pickup);
    auditService.log("PICKUP", "Retrait facture " + invoice.getInvoiceNumber(), "Pickup", pickup.getId());
    
    return toHistory(pickup);
}

/* 
    @Transactional
    public List<PickupHistoryRow> createPickup(PickupRequest req) {
        Invoice invoice = invoiceService.find(req.invoiceId());

        // Vérification du paiement complet
    if (invoice.getAmountDue() != null && invoice.getAmountDue().compareTo(BigDecimal.ZERO) > 0) {
        throw ApiException.badRequest(
            "Impossible d'effectuer le retrait : il reste " + invoice.getAmountDue() + " FCFA à payer."
        );
    }

        Pickup pickup = new Pickup();
        pickup.setSiteId(domainSupport.siteId());
        pickup.setInvoice(invoice);
        pickup.setPickupDateTime(LocalDateTime.now());
        pickup.setOperatorName(domainSupport.operatorName());
        pickup.setNotes(req.notes());
        for (PickupLineRequest lineReq : req.lines() == null ? List.<PickupLineRequest>of() : req.lines()) {
            InvoiceLine invoiceLine = invoice.getLines().stream()
                    .filter(l -> l.getId().equals(lineReq.invoiceLineId()))
                    .findFirst()
                    .orElseThrow(() -> ApiException.notFound("Ligne de facture introuvable"));
            int remaining = invoiceLine.getQuantity() - invoiceLine.getQuantityPickedUp();
            if (lineReq.quantity() > remaining) {
                throw ApiException.badRequest("Quantité retirée supérieure au reste pour " + invoiceLine.getDesignation());
            }
            invoiceLine.setQuantityPickedUp(invoiceLine.getQuantityPickedUp() + lineReq.quantity());
            PickupLine pickupLine = new PickupLine();
            pickupLine.setPickup(pickup);
            pickupLine.setInvoiceLine(invoiceLine);
            pickupLine.setQuantity(lineReq.quantity());
            pickupLine.setDesignation(invoiceLine.getDesignation());
            pickup.getLines().add(pickupLine);
        }
        boolean allPicked = invoice.getLines().stream().allMatch(l -> l.getQuantityPickedUp() >= l.getQuantity());
        boolean anyPicked = invoice.getLines().stream().anyMatch(l -> l.getQuantityPickedUp() > 0);
        if (allPicked) {
            invoice.setStatus(InvoiceStatus.PICKED_UP);
        } else if (anyPicked) {
            invoice.setStatus(InvoiceStatus.PARTIALLY_PICKED_UP);
        }
        pickupRepository.save(pickup);
        auditService.log("PICKUP", "Retrait facture " + invoice.getInvoiceNumber(), "Pickup", pickup.getId());
        return toHistory(pickup);
    }*/

    @Transactional(readOnly = true)
    public PageResponse<PickupHistoryRow> pickupHistory(String q, LocalDate from, LocalDate to, int page, int size) {
        Long siteId = domainSupport.siteId();
        Specification<Pickup> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            predicates.add(cb.equal(root.get("siteId"), siteId));
            if (from != null) {
                predicates.add(cb.greaterThanOrEqualTo(root.get("pickupDateTime"), from.atStartOfDay()));
            }
            if (to != null) {
                predicates.add(cb.lessThan(root.get("pickupDateTime"), to.plusDays(1).atStartOfDay()));
            }
            if (q != null && !q.isBlank()) {
                String like = "%" + q.toLowerCase() + "%";
                Join<Object, Object> invoice = root.join("invoice", JoinType.LEFT);
                Join<Object, Object> customer = invoice.join("customer", JoinType.LEFT);
                predicates.add(cb.or(
                        cb.like(cb.lower(invoice.get("invoiceNumber")), like),
                        cb.like(cb.lower(customer.get("lastName")), like),
                        cb.like(cb.lower(root.get("operatorName")), like)
                ));
            }
            query.distinct(true);
            return cb.and(predicates.toArray(Predicate[]::new));
        };
        return PageResponse.from(pickupRepository.findAll(spec, PageRequest.of(page, size, Sort.by("pickupDateTime").descending()))
                .map(p -> toHistory(p).isEmpty() ? new PickupHistoryRow(p.getId(), p.getPickupDateTime(),
                        p.getInvoice().getInvoiceNumber(), "", 0,
                        p.getInvoice().getCustomer().getFirstName() + " " + p.getInvoice().getCustomer().getLastName(),
                        p.getOperatorName()) : toHistory(p).get(0)));
    }



  @Transactional(readOnly = true)
  public InvoicePickupDetailsDto getInvoiceForPickup(String invoiceNumber) {
    // Appel propre via InvoiceService
    Invoice invoice = invoiceService.findByNumber(invoiceNumber);

    List<InvoicePickupLineDto> lines = invoice.getLines().stream().map(line -> new InvoicePickupLineDto(
            line.getId(),
            line.getDesignation(),
            line.getBrand(),
            line.getObservation(),
            line.getQuantity(),
            line.getQuantityPickedUp(),
            line.getQuantity() - line.getQuantityPickedUp(),
            line.getUnitPrice(),
            line.getLineTotal(),
            line.getStorageLocation() != null ? line.getStorageLocation().getId() : null,
            line.getStorageLocation() != null ? line.getStorageLocation().getLabel() : "Non assigné"
    )).toList();

    return new InvoicePickupDetailsDto(
            invoice.getId(),
            invoice.getInvoiceNumber(),
            invoice.getCustomer().getFirstName() + " " + invoice.getCustomer().getLastName(),
            invoice.getCustomer().getPhone(),
            invoice.getStatus().name(),
            invoice.getPaymentStatus().name(),
            invoice.getAmountTtc(),
            invoice.getAmountPaid(),
            invoice.getAmountDue(),
            lines
    );
}

    // Repassage : IroningInvoice = en-tête/reçu, IroningInvoiceLine = un article repassé.
    // Plusieurs articles peuvent être regroupés sous un même numéro de reçu.
    public record IroningLineRequest(
            @NotNull Long articleTypeId,
            @NotNull Long articleId,
            @NotNull Integer quantity,
            BigDecimal unitPrice,
            String observation
    ) {
    }

    public record IroningRequest(
            Long customerId,
            String firstName,
            String lastName,
            String phone,
            String notes,
            @NotNull List<IroningLineRequest> lines
    ) {
    }

    public record IroningLineResponse(
            Long id,
            Long articleTypeId,
            String articleTypeLabel,
            Long articleId,
            String articleLabel,
            int quantity,
            BigDecimal unitPrice,
            BigDecimal lineTotal,
            String observation
    ) {
    }

    public record IroningResponse(
        Long id,
        String invoiceNumber,
        LocalDateTime invoiceDateTime,
        Long customerId,
        String customerName,
        String customerPhone,
        int articleCount,
        BigDecimal amountTtc,
        BigDecimal amountPaid,
        BigDecimal amountDue,
        String paymentStatus,
        String status,
        String operatorName,
        String notes,
        List<IroningLineResponse> lines
) {}

    private BigDecimal defaultIroningPrice(Long articleTypeId) {
        return pricingRepository.findBySiteIdOrderByIdDesc(domainSupport.siteId())
                .stream().filter(p -> p.getArticleType().getId().equals(articleTypeId)).findFirst()
                .map(p -> p.getUnitPrice()).orElse(BigDecimal.ZERO);
    }

    private void applyIroningLines(IroningInvoice entity, List<IroningLineRequest> lineReqs) {
        if (lineReqs == null || lineReqs.isEmpty()) {
            throw ApiException.badRequest("Au moins un article de repassage est requis");
        }
        entity.getLines().clear();
        for (IroningLineRequest lineReq : lineReqs) {
            ArticleType type = articleTypeRepository.findByIdAndSiteId(lineReq.articleTypeId(), domainSupport.siteId())
                    .orElseThrow(() -> ApiException.notFound("Catégorie introuvable"));
            Article article = articleRepository.findByIdAndSiteId(lineReq.articleId(), domainSupport.siteId())
                    .orElseThrow(() -> ApiException.notFound("Article introuvable"));
            BigDecimal unit = lineReq.unitPrice() != null ? lineReq.unitPrice() : defaultIroningPrice(type.getId());

            IroningInvoiceLine line = new IroningInvoiceLine();
            line.setIroningInvoice(entity);
            line.setArticleType(type);
            line.setArticle(article);
            line.setQuantity(lineReq.quantity());
            line.setUnitPrice(unit);
            line.setLineTotal(unit.multiply(BigDecimal.valueOf(lineReq.quantity())));
            line.setObservation(lineReq.observation());
            entity.getLines().add(line);
        }
        recalculateIroning(entity);
    }

    private void recalculateIroning(IroningInvoice entity) {
        BigDecimal total = entity.getLines().stream()
                .map(IroningInvoiceLine::getLineTotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        entity.setAmountTtc(total);
        if (entity.getAmountPaid() == null) {
            entity.setAmountPaid(BigDecimal.ZERO);
        }
        refreshIroningPayment(entity);
    }

    private void refreshIroningPayment(IroningInvoice entity) {
        if (entity.getAmountPaid().compareTo(BigDecimal.ZERO) <= 0) {
            entity.setPaymentStatus(PaymentStatus.UNPAID);
        } else if (entity.getAmountPaid().compareTo(entity.getAmountTtc()) >= 0) {
            entity.setPaymentStatus(PaymentStatus.PAID);
        } else {
            entity.setPaymentStatus(PaymentStatus.DEPOSIT);
        }
        entity.setAmountDue(entity.getAmountTtc().subtract(entity.getAmountPaid()).max(BigDecimal.ZERO));
    }

    @Transactional
    public IroningResponse createIroning(IroningRequest req) {
        IroningInvoice entity = new IroningInvoice();
        entity.setSiteId(domainSupport.siteId());
        entity.setInvoiceNumber(domainSupport.nextNumber("REP", ironingRepository.count() + 1));
        entity.setInvoiceDateTime(LocalDateTime.now());
        entity.setNotes(req.notes());
        entity.setStatus("VALIDATED");
        if (req.firstName() != null && req.lastName() != null) {
            entity.setCustomer(customerService.findOrCreate(
                    new CustomerRequest(req.firstName(), req.lastName(), req.phone(), null, null, null, false, null),
                    req.customerId()));
        }
        applyIroningLines(entity, req.lines());
        IroningInvoice saved = ironingRepository.save(entity);
        auditService.log("IRONING_INVOICE", "Facture repassage " + saved.getInvoiceNumber(), "IroningInvoice", saved.getId());
        return toIroning(saved);
    }

    @Transactional
    public IroningResponse updateIroning(Long id, IroningRequest req) {
        IroningInvoice entity = ironingRepository.findByIdAndSiteIdFetchLines(id, domainSupport.siteId())
                .orElseThrow(() -> ApiException.notFound("Repassage introuvable"));
        entity.setNotes(req.notes());
        applyIroningLines(entity, req.lines());
        return toIroning(ironingRepository.save(entity));
    }

    @Transactional(readOnly = true)
public PageResponse<IroningResponse> ironingHistory(String q, LocalDate from, LocalDate to, int page, int size) {
    Long siteId = domainSupport.siteId();
    Specification<IroningInvoice> spec = (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        predicates.add(cb.equal(root.get("siteId"), siteId));
        if (from != null) {
            predicates.add(cb.greaterThanOrEqualTo(root.get("invoiceDateTime"), from.atStartOfDay()));
        }
        if (to != null) {
            predicates.add(cb.lessThan(root.get("invoiceDateTime"), to.plusDays(1).atStartOfDay()));
        }
        if (q != null && !q.isBlank()) {
            String like = "%" + q.toLowerCase() + "%";
            Join<Object, Object> line = root.join("lines", JoinType.LEFT);
            Join<Object, Object> article = line.join("article", JoinType.LEFT);
            Join<Object, Object> customer = root.join("customer", JoinType.LEFT);
            predicates.add(cb.or(
                    cb.like(cb.lower(root.get("invoiceNumber")), like),
                    cb.like(cb.lower(article.get("label")), like),
                    cb.like(cb.lower(customer.get("lastName")), like),
                    cb.like(cb.lower(customer.get("firstName")), like)
            ));
        }
        query.distinct(true);
        return cb.and(predicates.toArray(Predicate[]::new));
    };

    return PageResponse.from(
        ironingRepository.findAll(spec, PageRequest.of(page, size, Sort.by("invoiceDateTime").descending()))
            .map(this::toIroning)
    );
}

@Transactional(readOnly = true)
public IroningResponse getIroning(Long id) {
    IroningInvoice entity = ironingRepository.findByIdAndSiteIdFetchLines(id, domainSupport.siteId())
            .orElseThrow(() -> ApiException.notFound("Facture de repassage introuvable"));
    return toIroning(entity);
}
    @Transactional
    public void deleteIroning(Long id) {
        IroningInvoice entity = ironingRepository.findByIdAndSiteId(id, domainSupport.siteId())
                .orElseThrow(() -> ApiException.notFound("Facture de repassage introuvable"));
        if (entity.getAmountPaid().compareTo(BigDecimal.ZERO) > 0) {
            throw ApiException.badRequest("Impossible de supprimer une facture de repassage déjà réglée, même partiellement");
        }
        ironingRepository.delete(entity);
        auditService.log("DELETE_IRONING_INVOICE", "Suppression facture repassage " + entity.getInvoiceNumber(), "IroningInvoice", id);
    }

    
// Recouvrement unifié : un règlement porte soit sur une facture de lavage (Invoice),
// soit sur un reçu de repassage (IroningInvoice). documentType vaut "LAVAGE" ou "REPASSAGE".
public record PaymentReceiptDetailsDto(
        Long paymentId,
        String receiptNumber,
        LocalDateTime paymentDateTime,
        BigDecimal amountPaidThisTime,
        String method,
        String reference,
        String operatorName,
        String notes,

        // Infos document réglé (facture lavage ou reçu repassage)
        String documentType,
        String documentNumber,
        BigDecimal totalDocumentAmount,
        BigDecimal totalAmountPaidSoFar,
        BigDecimal remainingAmountDue,

        // Infos Client
        String customerName,
        String customerPhone
) {}

    public record PaymentUpdateRequest(
            @NotNull BigDecimal amount,
            LocalDateTime paymentDateTime,
            String method,
            String reference,
            String notes
    ) {}

    public record PayableLineDto(
            String designation,
            int quantity,
            BigDecimal unitPrice,
            BigDecimal lineTotal
    ) {}

    public record PayableDocumentDto(
            String documentType,   // "LAVAGE" ou "REPASSAGE"
            Long documentId,
            String documentNumber,
            String customerName,
            String customerPhone,
            BigDecimal amountTtc,
            BigDecimal amountPaid,
            BigDecimal amountDue,
            String paymentStatus,
            List<PayableLineDto> lines,
            List<PaymentResponse> existingPayments
    ) {}

    public record PaymentRequest(
            @NotNull String documentType,   // "LAVAGE" ou "REPASSAGE"
            @NotNull Long documentId,
            @NotNull BigDecimal amount,
            String method,
            String reference,
            String notes
    ) {
    }

    public record PaymentResponse(
            Long id,
            LocalDateTime paymentDateTime,
            String receiptNumber,
            String documentType,
            String documentNumber,
            String customerName,
            BigDecimal amount,
            String operatorName,
            String method,
            String reference
    ) {
    }



    @Transactional
    public PaymentResponse createPayment(PaymentRequest req) {
        Payment payment = new Payment();
        payment.setSiteId(domainSupport.siteId());
        payment.setReceiptNumber(domainSupport.nextNumber("REC", paymentRepository.count() + 1));
        payment.setPaymentDateTime(LocalDateTime.now());
        payment.setAmount(req.amount());
        payment.setMethod(req.method() == null ? "CASH" : req.method());
        payment.setReference(req.reference());
        payment.setOperatorName(domainSupport.operatorName());
        payment.setNotes(req.notes());

        String documentNumber;
        if ("REPASSAGE".equals(req.documentType())) {
            IroningInvoice ironing = ironingRepository.findByIdAndSiteIdFetchLines(req.documentId(), domainSupport.siteId())
                    .orElseThrow(() -> ApiException.notFound("Facture de repassage introuvable"));
            if (req.amount().compareTo(ironing.getAmountDue()) > 0) {
                throw ApiException.badRequest("Le montant encaissé dépasse le reste à payer");
            }
            payment.setIroningInvoice(ironing);
            payment.setCustomer(ironing.getCustomer());
            ironing.setAmountPaid(ironing.getAmountPaid().add(req.amount()));
            refreshIroningPayment(ironing);
            documentNumber = ironing.getInvoiceNumber();
        } else {
            Invoice invoice = invoiceService.find(req.documentId());
            if (req.amount().compareTo(invoice.getAmountDue()) > 0) {
                throw ApiException.badRequest("Le montant encaissé dépasse le reste à payer");
            }
            payment.setInvoice(invoice);
            payment.setCustomer(invoice.getCustomer());
            invoice.setAmountPaid(invoice.getAmountPaid().add(req.amount()));
            invoiceService.refreshPayment(invoice);
            documentNumber = invoice.getInvoiceNumber();
        }

        Payment saved = paymentRepository.save(payment);
        auditService.log("PAYMENT", "Règlement " + saved.getReceiptNumber() + " " + documentNumber, "Payment", saved.getId());
        return toPayment(saved);
    }


    // 1. Recherche unifiée de documents (factures lavage + reçus repassage) pour règlement
@Transactional(readOnly = true)
public List<PayableDocumentDto> searchInvoicesForPayment(String q) {
    List<PayableDocumentDto> results = new ArrayList<>();

    List<Invoice> invoices = invoiceService.searchInvoices(q);
    for (Invoice invoice : invoices) {
        List<PayableLineDto> lines = invoice.getLines().stream().map(line -> new PayableLineDto(
                line.getDesignation(), line.getQuantity(), line.getUnitPrice(), line.getLineTotal()
        )).toList();
        List<PaymentResponse> existingPayments = paymentRepository.findByInvoiceIdOrderByPaymentDateTimeAsc(invoice.getId())
                .stream().map(this::toPayment).toList();
        results.add(new PayableDocumentDto(
                "LAVAGE",
                invoice.getId(),
                invoice.getInvoiceNumber(),
                invoice.getCustomer().getFirstName() + " " + invoice.getCustomer().getLastName(),
                invoice.getCustomer().getPhone(),
                invoice.getAmountTtc(),
                invoice.getAmountPaid(),
                invoice.getAmountDue(),
                invoice.getPaymentStatus().name(),
                lines,
                existingPayments
        ));
    }

    if (q != null && !q.isBlank()) {
        List<IroningInvoice> ironingInvoices = ironingRepository.searchForPayment(domainSupport.siteId(), q);
        for (IroningInvoice ironing : ironingInvoices) {
            List<PayableLineDto> lines = ironing.getLines().stream().map(line -> new PayableLineDto(
                    line.getArticle().getLabel(), line.getQuantity(), line.getUnitPrice(), line.getLineTotal()
            )).toList();
            List<PaymentResponse> existingPayments = paymentRepository.findByIroningInvoiceIdOrderByPaymentDateTimeAsc(ironing.getId())
                    .stream().map(this::toPayment).toList();
            String customerName = ironing.getCustomer() == null ? "" : ironing.getCustomer().getFirstName() + " " + ironing.getCustomer().getLastName();
            String customerPhone = ironing.getCustomer() == null ? "" : ironing.getCustomer().getPhone();
            results.add(new PayableDocumentDto(
                    "REPASSAGE",
                    ironing.getId(),
                    ironing.getInvoiceNumber(),
                    customerName,
                    customerPhone,
                    ironing.getAmountTtc(),
                    ironing.getAmountPaid(),
                    ironing.getAmountDue(),
                    ironing.getPaymentStatus().name(),
                    lines,
                    existingPayments
            ));
        }
    }

    return results;
}

// 2. Détails du reçu de versement
@Transactional(readOnly = true)
public PaymentReceiptDetailsDto getPaymentReceipt(Long paymentId) {
    Payment payment = paymentRepository.findByIdAndSiteId(paymentId, domainSupport.siteId())
            .orElseThrow(() -> ApiException.notFound("Reçu de règlement introuvable"));

    String documentType;
    String documentNumber;
    BigDecimal totalDocumentAmount;
    BigDecimal totalAmountPaidSoFar;
    BigDecimal remainingAmountDue;

    if (payment.getIroningInvoice() != null) {
        IroningInvoice ironing = payment.getIroningInvoice();
        documentType = "REPASSAGE";
        documentNumber = ironing.getInvoiceNumber();
        totalDocumentAmount = ironing.getAmountTtc();
        totalAmountPaidSoFar = ironing.getAmountPaid();
        remainingAmountDue = ironing.getAmountDue();
    } else {
        Invoice invoice = payment.getInvoice();
        documentType = "LAVAGE";
        documentNumber = invoice.getInvoiceNumber();
        totalDocumentAmount = invoice.getAmountTtc();
        totalAmountPaidSoFar = invoice.getAmountPaid();
        remainingAmountDue = invoice.getAmountDue();
    }

    return new PaymentReceiptDetailsDto(
            payment.getId(),
            payment.getReceiptNumber(),
            payment.getPaymentDateTime(),
            payment.getAmount(),
            payment.getMethod(),
            payment.getReference(),
            payment.getOperatorName(),
            payment.getNotes(),

            documentType,
            documentNumber,
            totalDocumentAmount,
            totalAmountPaidSoFar,
            remainingAmountDue,

            payment.getCustomer().getFirstName() + " " + payment.getCustomer().getLastName(),
            payment.getCustomer().getPhone()
    );
}



    @Transactional(readOnly = true)
    public PageResponse<PaymentResponse> payments(String q, LocalDate from, LocalDate to, int page, int size) {
        Long siteId = domainSupport.siteId();
        Specification<Payment> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            predicates.add(cb.equal(root.get("siteId"), siteId));
            if (from != null) {
                predicates.add(cb.greaterThanOrEqualTo(root.get("paymentDateTime"), from.atStartOfDay()));
            }
            if (to != null) {
                predicates.add(cb.lessThan(root.get("paymentDateTime"), to.plusDays(1).atStartOfDay()));
            }
            if (q != null && !q.isBlank()) {
                String like = "%" + q.toLowerCase() + "%";
                Join<Object, Object> invoice = root.join("invoice", JoinType.LEFT);
                Join<Object, Object> ironingInvoice = root.join("ironingInvoice", JoinType.LEFT);
                Join<Object, Object> customer = root.join("customer", JoinType.LEFT);
                predicates.add(cb.or(
                        cb.like(cb.lower(root.get("receiptNumber")), like),
                        cb.like(cb.lower(invoice.get("invoiceNumber")), like),
                        cb.like(cb.lower(ironingInvoice.get("invoiceNumber")), like),
                        cb.like(cb.lower(customer.get("lastName")), like),
                        cb.like(cb.lower(customer.get("firstName")), like)
                ));
            }
            return cb.and(predicates.toArray(Predicate[]::new));
        };
        return PageResponse.from(paymentRepository.findAll(spec, PageRequest.of(page, size, Sort.by("paymentDateTime").descending()))
                .map(this::toPayment));
    }


// 2. Modifier un reçu (montant, date, mode, référence, notes) — gère facture lavage ou repassage
@Transactional
public PaymentResponse updatePayment(Long id, PaymentUpdateRequest req) {
    Payment payment = paymentRepository.findByIdAndSiteId(id, domainSupport.siteId())
            .orElseThrow(() -> ApiException.notFound("Règlement introuvable"));

    BigDecimal oldAmount = payment.getAmount();
    BigDecimal newAmount = req.amount();

    if (payment.getIroningInvoice() != null) {
        IroningInvoice ironing = ironingRepository.findByIdAndSiteIdFetchLines(payment.getIroningInvoice().getId(), domainSupport.siteId())
                .orElseThrow(() -> ApiException.notFound("Facture de repassage introuvable"));
        BigDecimal adjustedAmountPaid = ironing.getAmountPaid().subtract(oldAmount).add(newAmount);
        if (adjustedAmountPaid.compareTo(ironing.getAmountTtc()) > 0) {
            throw ApiException.badRequest("Le montant total payé dépasserait le montant TTC de la facture de repassage");
        }
        ironing.setAmountPaid(adjustedAmountPaid);
        refreshIroningPayment(ironing);
    } else {
        Invoice invoice = payment.getInvoice();
        BigDecimal adjustedAmountPaid = invoice.getAmountPaid().subtract(oldAmount).add(newAmount);
        if (adjustedAmountPaid.compareTo(invoice.getAmountTtc()) > 0) {
            throw ApiException.badRequest("Le montant total payé dépasserait le montant TTC de la facture");
        }
        invoice.setAmountPaid(adjustedAmountPaid);
        invoiceService.refreshPayment(invoice);
    }

    payment.setAmount(newAmount);
    if (req.paymentDateTime() != null) {
        payment.setPaymentDateTime(req.paymentDateTime());
    }
    if (req.method() != null) {
        payment.setMethod(req.method());
    }
    if (req.reference() != null) {
        payment.setReference(req.reference());
    }
    if (req.notes() != null) {
        payment.setNotes(req.notes());
    }

    Payment saved = paymentRepository.save(payment);
    auditService.log("UPDATE_PAYMENT", "Modification reçu " + saved.getReceiptNumber(), "Payment", saved.getId());

    return toPayment(saved);
}

// 3. Supprimer un reçu — gère facture lavage ou repassage
@Transactional
public void deletePayment(Long id) {
    Payment payment = paymentRepository.findByIdAndSiteId(id, domainSupport.siteId())
            .orElseThrow(() -> ApiException.notFound("Règlement introuvable"));

    String documentNumber;
    if (payment.getIroningInvoice() != null) {
        IroningInvoice ironing = ironingRepository.findByIdAndSiteIdFetchLines(payment.getIroningInvoice().getId(), domainSupport.siteId())
                .orElseThrow(() -> ApiException.notFound("Facture de repassage introuvable"));
        ironing.setAmountPaid(ironing.getAmountPaid().subtract(payment.getAmount()).max(BigDecimal.ZERO));
        refreshIroningPayment(ironing);
        documentNumber = ironing.getInvoiceNumber();
    } else {
        Invoice invoice = payment.getInvoice();
        invoice.setAmountPaid(invoice.getAmountPaid().subtract(payment.getAmount()).max(BigDecimal.ZERO));
        invoiceService.refreshPayment(invoice);
        documentNumber = invoice.getInvoiceNumber();
    }

    paymentRepository.delete(payment);
    auditService.log("DELETE_PAYMENT", "Annulation reçu " + payment.getReceiptNumber() + " " + documentNumber, "Payment", id);
}




    /* PORTEFEUILLE CLIENT */

   public record WalletRequest(
        @NotNull Long customerId,
        @NotNull BigDecimal amountGiven,     // Montant initial dû au client (ex: 2000 FCFA)
        BigDecimal changeReturned,            // Montant déjà rendu/remboursé (ex: 500 FCFA)
        String notes
) {}

public record WalletResponse(
        Long id,
        String referenceNumber,
        Long customerId,
        String customerName,
        String customerPhone,
        LocalDateTime entryDateTime,
        BigDecimal amountGiven,       // Dû initialement
        BigDecimal changeReturned,   // Déjà rendu au client
        BigDecimal remainingToReturn,// Reste à rendre (amountGiven - changeReturned)
        String operatorName,
        String notes
) {}



// 1. Créer une entrée portefeuille (ex: Monnaie non rendue lors d'un passage)
@Transactional
public WalletResponse createWallet(WalletRequest req) {
    Customer customer = customerService.find(req.customerId());

    BigDecimal amountGiven = req.amountGiven();
    BigDecimal changeReturned = req.changeReturned() != null ? req.changeReturned() : BigDecimal.ZERO;
    
    if (changeReturned.compareTo(amountGiven) > 0) {
        throw ApiException.badRequest("Le montant rendu ne peut pas dépasser le montant dû initial");
    }

    WalletEntry entry = new WalletEntry();
    entry.setSiteId(domainSupport.siteId());
    entry.setReferenceNumber(domainSupport.nextNumber("WAL", walletRepository.count() + 1));
    entry.setCustomer(customer);
    entry.setEntryDateTime(LocalDateTime.now());
    entry.setAmountGiven(amountGiven);
    entry.setChangeReturned(changeReturned);
    entry.setNetCollected(amountGiven.subtract(changeReturned)); // Reste net dû au client
    entry.setOperatorName(domainSupport.operatorName());
    entry.setNotes(req.notes());

    WalletEntry saved = walletRepository.save(entry);
    auditService.log("CREATE_WALLET", "Portefeuille " + saved.getReferenceNumber() + " pour client " + customer.getPhone(), "WalletEntry", saved.getId());
    return toWallet(saved);
}

// 2. Modifier le montant remboursé / rendu au client
@Transactional
public WalletResponse updateWallet(Long id, WalletRequest req) {
    WalletEntry entry = walletRepository.findByIdAndSiteId(id, domainSupport.siteId())
            .orElseThrow(() -> ApiException.notFound("Entrée portefeuille introuvable"));

    BigDecimal newChangeReturned = req.changeReturned() != null ? req.changeReturned() : entry.getChangeReturned();
    BigDecimal amountGiven = req.amountGiven() != null ? req.amountGiven() : entry.getAmountGiven();

    if (newChangeReturned.compareTo(amountGiven) > 0) {
        throw ApiException.badRequest("Le montant rendu ne peut pas dépasser le montant dû");
    }

    entry.setAmountGiven(amountGiven);
    entry.setChangeReturned(newChangeReturned);
    entry.setNetCollected(amountGiven.subtract(newChangeReturned));
    if (req.notes() != null) {
        entry.setNotes(req.notes());
    }

    WalletEntry saved = walletRepository.save(entry);
    auditService.log("UPDATE_WALLET", "Mise à jour portefeuille " + saved.getReferenceNumber(), "WalletEntry", saved.getId());
    return toWallet(saved);
}

// 3. Annulation / Suppression
@Transactional
public void deleteWallet(Long id) {
    WalletEntry entry = walletRepository.findByIdAndSiteId(id, domainSupport.siteId())
            .orElseThrow(() -> ApiException.notFound("Entrée portefeuille introuvable"));

    walletRepository.delete(entry);
    auditService.log("DELETE_WALLET", "Suppression portefeuille " + entry.getReferenceNumber(), "WalletEntry", id);
}

// 4. Recherche et Liste paginée
@Transactional(readOnly = true)
public PageResponse<WalletResponse> wallets(String q, int page, int size) {
    Long siteId = domainSupport.siteId();

    Specification<WalletEntry> spec = (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        predicates.add(cb.equal(root.get("siteId"), siteId));

        if (q != null && !q.isBlank()) {
            String like = "%" + q.toLowerCase() + "%";
            Join<Object, Object> customer = root.join("customer", JoinType.LEFT);

            predicates.add(cb.or(
                    cb.like(cb.lower(root.get("referenceNumber")), like),
                    cb.like(cb.lower(customer.get("lastName")), like),
                    cb.like(cb.lower(customer.get("firstName")), like),
                    cb.like(customer.get("phone"), like)
            ));
        }
        return cb.and(predicates.toArray(Predicate[]::new));
    };

    return PageResponse.from(walletRepository.findAll(spec, PageRequest.of(page, size, Sort.by("entryDateTime").descending()))
            .map(this::toWallet));
}

private WalletResponse toWallet(WalletEntry w) {
    Customer c = w.getCustomer();
    BigDecimal remaining = w.getAmountGiven().subtract(w.getChangeReturned());

    return new WalletResponse(
            w.getId(),
            w.getReferenceNumber(),
            c.getId(),
            c.getFirstName() + " " + c.getLastName(),
            c.getPhone(),
            w.getEntryDateTime(),
            w.getAmountGiven(),
            w.getChangeReturned(),
            remaining,
            w.getOperatorName(),
            w.getNotes()
    );
}


   


    /*EMPLACEMENT */
   public record StorageRequest(
        @NotNull Long invoiceId,
        Long invoiceLineId, // Facultatif : si null, s'applique à toute la facture
        @NotNull Long locationId
    ) {}

    @Transactional
    public void assignStorage(StorageRequest req) {
        Invoice invoice = invoiceService.find(req.invoiceId());
        StorageLocation location = locationRepository.findByIdAndSiteId(req.locationId(), domainSupport.siteId())
                .orElseThrow(() -> ApiException.notFound("Emplacement introuvable"));

        if (req.invoiceLineId() != null) {
            // Assignation à une ligne spécifique
            InvoiceLine line = invoice.getLines().stream()
                    .filter(l -> l.getId().equals(req.invoiceLineId()))
                    .findFirst()
                    .orElseThrow(() -> ApiException.notFound("Ligne introuvable"));
            line.setStorageLocation(location);
        } else {
            // Assignation à toutes les lignes de la facture
            invoice.getLines().forEach(line -> line.setStorageLocation(location));
        }

        auditService.log("STORAGE", "Rangement " + location.getLabel() + " pour facture " + invoice.getInvoiceNumber(), "Invoice", invoice.getId());
    }


    private LocationResponse toLocation(StorageLocation l) {
    return new LocationResponse(
            l.getId(),
            l.getRowCode(),
            l.getLabel(),
            l.isActive()
    );
}
  
    @Transactional(readOnly = true)
    public List<LocationResponse> getLocations() {
    return locationRepository.findBySiteIdAndActiveTrue(domainSupport.siteId())
            .stream()
            .map(this::toLocation)
            .toList();
}



    /*to */
    private List<PickupHistoryRow> toHistory(Pickup pickup) {
        String customer = pickup.getInvoice().getCustomer().getFirstName() + " " + pickup.getInvoice().getCustomer().getLastName();
        if (pickup.getLines() == null || pickup.getLines().isEmpty()) {
            return List.of();
        }
        return pickup.getLines().stream().map(line -> new PickupHistoryRow(
                pickup.getId(),
                pickup.getPickupDateTime(),
                pickup.getInvoice().getInvoiceNumber(),
                line.getDesignation(),
                line.getQuantity(),
                customer,
                pickup.getOperatorName()
        )).toList();
    }

    private IroningLineResponse toIroningLine(IroningInvoiceLine l) {
        return new IroningLineResponse(
                l.getId(),
                l.getArticleType().getId(),
                l.getArticleType().getLabel(),
                l.getArticle().getId(),
                l.getArticle().getLabel(),
                l.getQuantity(),
                l.getUnitPrice(),
                l.getLineTotal(),
                l.getObservation()
        );
    }

    private IroningResponse toIroning(IroningInvoice e) {
    String customerName = e.getCustomer() == null ? "" : e.getCustomer().getFirstName() + " " + e.getCustomer().getLastName();
    String customerPhone = e.getCustomer() == null ? "" : e.getCustomer().getPhone();
    Long customerId = e.getCustomer() == null ? null : e.getCustomer().getId();
    List<IroningLineResponse> lineResponses = e.getLines().stream().map(this::toIroningLine).toList();
    int articleCount = e.getLines().stream().mapToInt(IroningInvoiceLine::getQuantity).sum();

    return new IroningResponse(
            e.getId(),
            e.getInvoiceNumber(),
            e.getInvoiceDateTime(),
            customerId,
            customerName,
            customerPhone,
            articleCount,
            e.getAmountTtc(),
            e.getAmountPaid(),
            e.getAmountDue(),
            e.getPaymentStatus().name(),
            e.getStatus(),
            e.getCreatedBy(),
            e.getNotes(),
            lineResponses
    );
}

    private PaymentResponse toPayment(Payment p) {
        boolean isIroning = p.getIroningInvoice() != null;
        String documentType = isIroning ? "REPASSAGE" : "LAVAGE";
        String documentNumber = isIroning ? p.getIroningInvoice().getInvoiceNumber() : p.getInvoice().getInvoiceNumber();
        return new PaymentResponse(p.getId(), p.getPaymentDateTime(), p.getReceiptNumber(), documentType, documentNumber,
                p.getCustomer().getFirstName() + " " + p.getCustomer().getLastName(), p.getAmount(), p.getOperatorName(),
                p.getMethod(), p.getReference());
    }

   

  
}
