package com.petshop.backend.service;

import com.petshop.backend.dto.sale.SaleRequest;
import com.petshop.backend.dto.sale.SaleResponse;
import com.petshop.backend.entity.*;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.event.SaleReceiptEvent;
import com.petshop.backend.repository.*;
import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
public class SaleService {

    private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10");
    private static final int LOYALTY_POINTS_PER_DOLLAR = 20;

    private final SaleRepository saleRepository;
    private final ProductRepository productRepository;
    private final StoreRepository storeRepository;
    private final InventoryRepository inventoryRepository;
    private final UserRepository userRepository;
    private final CouponRepository couponRepository;
    private final CartRepository cartRepository;
    private final ApplicationEventPublisher eventPublisher;

    public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) {
        this.saleRepository = saleRepository;
        this.productRepository = productRepository;
        this.storeRepository = storeRepository;
        this.inventoryRepository = inventoryRepository;
        this.userRepository = userRepository;
        this.couponRepository = couponRepository;
        this.cartRepository = cartRepository;
        this.eventPublisher = eventPublisher;
    }

    @Transactional(readOnly = true)
    public Page<SaleResponse> getAllSales(String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId, Pageable pageable) {
        Page<Sale> sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, isRefund, customerId, pageable);
        return sales.map(this::mapToResponse);
    }

    @Transactional(readOnly = true)
    public SaleResponse getSaleById(Long id) {
        Sale sale = saleRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Sale not found with id: " + id));
        return mapToResponse(sale);
    }

    @Transactional
    public SaleResponse createSale(SaleRequest request) {
        User actor = AuthenticationHelper.getAuthenticatedUser(userRepository);
        boolean websiteSale = request.getChannel() != null && request.getChannel().equalsIgnoreCase("WEBSITE");
        User employee = websiteSale ? resolveWebsiteSaleEmployee(request.getStoreId()) : actor;

        StoreLocation store = storeRepository.findById(request.getStoreId())
                .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));

        if (employee.getRole() == User.Role.STAFF) {
            Long assignedStoreId = employee.getPrimaryStore() != null ? employee.getPrimaryStore().getStoreId() : null;
            if (!store.getStoreId().equals(assignedStoreId)) {
                throw new BusinessException("Staff can only create sales for their assigned store");
            }
        }

        Sale sale = new Sale();
        sale.setSaleDate(LocalDateTime.now());
        sale.setEmployee(employee);
        sale.setStore(store);
        sale.setPaymentMethod(normalizePaymentMethod(request.getPaymentMethod()));
        sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false);
        sale.setChannel(request.getChannel() != null ? request.getChannel() : "IN_STORE");

        if (request.getCouponId() != null) {
            Coupon coupon = couponRepository.findByIdForUpdate(request.getCouponId())
                    .orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + request.getCouponId()));
            validateCoupon(coupon);
            sale.setCoupon(coupon);
        }

        if (request.getCartId() != null) {
            Cart cart = cartRepository.findById(request.getCartId())
                    .orElseThrow(() -> new ResourceNotFoundException("Cart not found with id: " + request.getCartId()));
            sale.setCart(cart);
        }

        User customer = null;
        if (request.getCustomerId() != null) {
            customer = userRepository.findByIdForUpdate(request.getCustomerId())
                    .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
            sale.setCustomer(customer);
        }

        if (websiteSale && customer == null) {
            customer = actor;
            sale.setCustomer(customer);
        }

        if (sale.getIsRefund() && request.getOriginalSaleId() != null) {
            Sale originalSale = saleRepository.findByIdForUpdate(request.getOriginalSaleId())
                    .orElseThrow(() -> new ResourceNotFoundException("Original sale not found with id: " + request.getOriginalSaleId()));
            sale.setOriginalSale(originalSale);
        }

        BigDecimal subtotalAmount = BigDecimal.ZERO;
        List<SaleItem> saleItems = new ArrayList<>();

        if (sale.getIsRefund() && sale.getOriginalSale() != null) {
            for (var itemRequest : request.getItems()) {
                Product product = productRepository.findById(itemRequest.getProdId())
                        .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId()));

                SaleItem originalItem = sale.getOriginalSale().getItems().stream()
                        .filter(item -> item.getProduct().getProdId().equals(itemRequest.getProdId()))
                        .findFirst()
                        .orElseThrow(() -> new BusinessException("Product " + itemRequest.getProdId() + " was not in the original sale"));

                if (itemRequest.getQuantity() > originalItem.getQuantity()) {
                    throw new BusinessException("Refund quantity " + itemRequest.getQuantity() +
                            " exceeds original quantity " + originalItem.getQuantity() +
                            " for product: " + product.getProdName());
                }

                int alreadyRefundedQuantity = saleRepository.findByOriginalSaleSaleId(sale.getOriginalSale().getSaleId()).stream()
                        .flatMap(existingRefund -> existingRefund.getItems().stream())
                        .filter(existingRefundItem -> existingRefundItem.getProduct().getProdId().equals(itemRequest.getProdId()))
                        .mapToInt(existingRefundItem -> Math.abs(existingRefundItem.getQuantity()))
                        .sum();

                int refundableQuantity = originalItem.getQuantity() - alreadyRefundedQuantity;
                if (itemRequest.getQuantity() > refundableQuantity) {
                    throw new BusinessException("Refund quantity " + itemRequest.getQuantity() +
                            " exceeds remaining refundable quantity " + refundableQuantity +
                            " for product: " + product.getProdName());
                }

                Inventory inventory = inventoryRepository.findByProductIdAndStoreIdForUpdate(itemRequest.getProdId(), store.getStoreId())
                        .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId() + " at store " + store.getStoreId()));

                inventory.setQuantity(inventory.getQuantity() + itemRequest.getQuantity());
                inventoryRepository.save(inventory);

                BigDecimal unitPrice = originalItem.getUnitPrice();
                BigDecimal itemTotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity()));

                SaleItem saleItem = new SaleItem();
                saleItem.setSale(sale);
                saleItem.setProduct(product);
                saleItem.setQuantity(-itemRequest.getQuantity());
                saleItem.setUnitPrice(unitPrice);

                saleItems.add(saleItem);
                subtotalAmount = subtotalAmount.add(itemTotal);
            }
            subtotalAmount = subtotalAmount.negate();
            sale.setSubtotalAmount(subtotalAmount);

            Sale originalSale = sale.getOriginalSale();
            BigDecimal originalSubtotal = originalSale.getSubtotalAmount();
            BigDecimal loyaltyDiscountRefunded = BigDecimal.ZERO;
            BigDecimal couponDiscountRefunded = BigDecimal.ZERO;
            BigDecimal refundTotal;

            if (originalSubtotal != null && originalSubtotal.compareTo(BigDecimal.ZERO) > 0) {
                BigDecimal ratio = subtotalAmount.abs().divide(originalSubtotal, 10, RoundingMode.HALF_UP);
                refundTotal = originalSale.getTotalAmount().abs().multiply(ratio).negate().setScale(2, RoundingMode.HALF_UP);
                if (originalSale.getLoyaltyDiscountAmount() != null) {
                    loyaltyDiscountRefunded = originalSale.getLoyaltyDiscountAmount().multiply(ratio).setScale(2, RoundingMode.HALF_UP);
                }
                if (originalSale.getCouponDiscountAmount() != null) {
                    couponDiscountRefunded = originalSale.getCouponDiscountAmount().multiply(ratio).setScale(2, RoundingMode.HALF_UP);
                }
                User refundCustomer = originalSale.getCustomer();
                if (refundCustomer != null) {
                    sale.setCustomer(refundCustomer);
                    int pointsToRestore = toPointsUsed(loyaltyDiscountRefunded);
                    int pointsEarnedToReverse = originalSale.getPointsEarned() != null
                            ? ratio.multiply(BigDecimal.valueOf(originalSale.getPointsEarned())).setScale(0, RoundingMode.FLOOR).intValue()
                            : 0;
                    int currentPoints = refundCustomer.getLoyaltyPoints() != null ? refundCustomer.getLoyaltyPoints() : 0;
                    refundCustomer.setLoyaltyPoints(Math.max(0, currentPoints + pointsToRestore - pointsEarnedToReverse));
                    userRepository.save(refundCustomer);
                }
            } else {
                refundTotal = subtotalAmount.negate();
            }

            sale.setTotalAmount(refundTotal);
            sale.setCouponDiscountAmount(couponDiscountRefunded);
            sale.setEmployeeDiscountAmount(BigDecimal.ZERO);
            sale.setLoyaltyDiscountAmount(loyaltyDiscountRefunded);
            sale.setPointsEarned(0);
            sale.setPointsUsed(0);
        } else {
            if (request.getItems() == null || request.getItems().isEmpty()) {
                throw new BusinessException("At least one item is required");
            }
            for (var itemRequest : request.getItems()) {
                Product product = productRepository.findById(itemRequest.getProdId())
                        .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId()));

                Inventory inventory = inventoryRepository.findByProductIdAndStoreIdForUpdate(itemRequest.getProdId(), store.getStoreId())
                        .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId() + " at store " + store.getStoreId()));

                if (inventory.getQuantity() < itemRequest.getQuantity()) {
                    throw new BusinessException("Insufficient stock for product: " + product.getProdName() +
                        ". Available: " + inventory.getQuantity() + ", requested: " + itemRequest.getQuantity());
                }

                inventory.setQuantity(inventory.getQuantity() - itemRequest.getQuantity());
                inventoryRepository.save(inventory);

                BigDecimal unitPrice = product.getProdPrice();
                BigDecimal itemTotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity()));

                SaleItem saleItem = new SaleItem();
                saleItem.setSale(sale);
                saleItem.setProduct(product);
                saleItem.setQuantity(itemRequest.getQuantity());
                saleItem.setUnitPrice(unitPrice);

                saleItems.add(saleItem);
                subtotalAmount = subtotalAmount.add(itemTotal);
            }
            sale.setSubtotalAmount(subtotalAmount);

            BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount);
            sale.setCouponDiscountAmount(couponDiscount);

            BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
            sale.setEmployeeDiscountAmount(employeeDiscount);

            BigDecimal remainingAfterDiscounts = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount);
            BigDecimal loyaltyDiscount;
            int pointsDeducted;
            if (request.getPointsUsed() != null && request.getPointsUsed() > 0) {
                loyaltyDiscount = BigDecimal.valueOf(request.getPointsUsed())
                        .divide(BigDecimal.valueOf(LOYALTY_POINTS_PER_DOLLAR), 2, RoundingMode.HALF_UP)
                        .min(remainingAfterDiscounts.max(BigDecimal.ZERO))
                        .setScale(2, RoundingMode.HALF_UP);
                pointsDeducted = request.getPointsUsed();
            } else {
                boolean useLoyaltyPoints = sale.getCart() != null && Boolean.TRUE.equals(sale.getCart().getPointsApplied());
                loyaltyDiscount = calculateLoyaltyDiscount(customer, remainingAfterDiscounts, useLoyaltyPoints);
                pointsDeducted = toPointsUsed(loyaltyDiscount);
            }
            sale.setLoyaltyDiscountAmount(loyaltyDiscount);
            sale.setPointsUsed(pointsDeducted);

            BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount);
            sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));

            sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue());
            if (customer != null) {
                int currentPoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0;
                int updatedPoints = currentPoints - pointsDeducted + sale.getPointsEarned();
                customer.setLoyaltyPoints(Math.max(updatedPoints, 0));
                userRepository.save(customer);
            }
        }

        sale.setItems(saleItems);

        Sale savedSale = saleRepository.save(sale);

        if (!Boolean.TRUE.equals(savedSale.getIsRefund()) && savedSale.getCustomer() != null) {
            eventPublisher.publishEvent(new SaleReceiptEvent(savedSale.getSaleId()));
        }

        return mapToResponse(savedSale);
    }

    private void validateCoupon(Coupon coupon) {
        if (!Boolean.TRUE.equals(coupon.getActive())) {
            throw new BusinessException("Coupon is not active");
        }
        LocalDateTime now = LocalDateTime.now();
        if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) {
            throw new BusinessException("Coupon has not started yet");
        }
        if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) {
            throw new BusinessException("Coupon has expired");
        }
        if (coupon.getUsageLimit() != null) {
            long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId());
            if (used >= coupon.getUsageLimit()) {
                throw new BusinessException("Coupon usage limit has been reached");
            }
        }
    }

    private BigDecimal calculateCouponDiscount(Coupon coupon, BigDecimal subtotal) {
        if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }

        if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) {
            return BigDecimal.ZERO;
        }

        BigDecimal discount = BigDecimal.ZERO;
        String type = coupon.getDiscountType().trim().toUpperCase();
        if ("PERCENTAGE".equals(type) || "PERCENT".equals(type)) {
            discount = subtotal.multiply(coupon.getDiscountValue().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP));
        } else if ("FIXED".equals(type)) {
            discount = coupon.getDiscountValue();
        }

        return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP);
    }

    private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) {
        if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }

        if (customer.getRole() == User.Role.STAFF || customer.getRole() == User.Role.ADMIN) {
            return remainingAmount.multiply(EMPLOYEE_DISCOUNT_PERCENT).setScale(2, RoundingMode.HALF_UP);
        }

        return BigDecimal.ZERO;
    }

    private BigDecimal calculateLoyaltyDiscount(User customer, BigDecimal remainingAmount, boolean useLoyaltyPoints) {
        if (!useLoyaltyPoints || customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }

        int availablePoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0;
        int wholeDollars = availablePoints / LOYALTY_POINTS_PER_DOLLAR;
        if (wholeDollars <= 0) {
            return BigDecimal.ZERO;
        }

        BigDecimal maxRedeemable = remainingAmount.setScale(0, RoundingMode.DOWN);
        return BigDecimal.valueOf(wholeDollars)
                .min(maxRedeemable)
                .setScale(2, RoundingMode.HALF_UP);
    }

    private int toPointsUsed(BigDecimal loyaltyDiscount) {
        if (loyaltyDiscount == null || loyaltyDiscount.compareTo(BigDecimal.ZERO) <= 0) {
            return 0;
        }
        return loyaltyDiscount.setScale(0, RoundingMode.DOWN).intValue() * LOYALTY_POINTS_PER_DOLLAR;
    }

    private User resolveWebsiteSaleEmployee(Long storeId) {
        return userRepository.findFirstByPrimaryStoreStoreIdAndRoleAndActiveTrueOrderByIdAsc(storeId, User.Role.STAFF)
                .or(() -> userRepository.findFirstByRoleAndActiveTrueOrderByIdAsc(User.Role.ADMIN))
                .orElseThrow(() -> new BusinessException("No active employee available for website sale"));
    }

    private SaleResponse mapToResponse(Sale sale) {
        SaleResponse response = new SaleResponse();
        response.setSaleId(sale.getSaleId());
        response.setSaleDate(sale.getSaleDate());
        response.setEmployeeId(sale.getEmployee().getId());
        response.setEmployeeName(sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName());

        if (sale.getStore() != null) {
            response.setStoreId(sale.getStore().getStoreId());
            response.setStoreName(sale.getStore().getStoreName());
        }

        if (sale.getCustomer() != null) {
            response.setCustomerId(sale.getCustomer().getId());
            response.setCustomerName(sale.getCustomer().getFirstName() + " " + sale.getCustomer().getLastName());
        }

        response.setTotalAmount(sale.getTotalAmount());
        response.setSubtotalAmount(sale.getSubtotalAmount());
        response.setCouponDiscountAmount(sale.getCouponDiscountAmount());
        response.setEmployeeDiscountAmount(sale.getEmployeeDiscountAmount());
        response.setLoyaltyDiscountAmount(sale.getLoyaltyDiscountAmount());
        response.setPointsEarned(sale.getPointsEarned());
        response.setPointsUsed(sale.getPointsUsed());
        response.setChannel(sale.getChannel());
        if (sale.getCoupon() != null) {
            response.setCouponId(sale.getCoupon().getCouponId());
        }
        if (sale.getCart() != null) {
            response.setCartId(sale.getCart().getCartId());
        }
        response.setPaymentMethod(sale.getPaymentMethod());
        response.setIsRefund(sale.getIsRefund());
        if (sale.getOriginalSale() != null) {
            response.setOriginalSaleId(sale.getOriginalSale().getSaleId());
        }
        response.setCreatedAt(sale.getCreatedAt());

        List<SaleResponse.SaleItemResponse> itemResponses = new ArrayList<>();
        for (SaleItem item : sale.getItems()) {
            SaleResponse.SaleItemResponse itemResponse = new SaleResponse.SaleItemResponse();
            itemResponse.setSaleItemId(item.getSaleItemId());
            itemResponse.setProdId(item.getProduct().getProdId());
            itemResponse.setProductName(item.getProduct().getProdName());
            itemResponse.setQuantity(item.getQuantity());
            itemResponse.setUnitPrice(item.getUnitPrice());
            itemResponses.add(itemResponse);
        }
        response.setItems(itemResponses);

        return response;
    }

    private String normalizeFilter(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    String normalizePaymentMethod(String paymentMethod) {
        if (paymentMethod == null) {
            return null;
        }

        String normalized = paymentMethod.trim();
        if (normalized.equalsIgnoreCase("Cash")) {
            return "Cash";
        }
        if (normalized.equalsIgnoreCase("Card")) {
            return "Card";
        }
        return normalized;
    }
}
