package com.fsd.hellovelo.service;

import com.fsd.hellovelo.entity.*;
import com.fsd.hellovelo.exceptions.ResourceNotFoundException;
import com.fsd.hellovelo.payload.request.RentalRequest;
import com.fsd.hellovelo.payload.request.UserRentalRequest;
import com.fsd.hellovelo.repository.BikeRepository;
import com.fsd.hellovelo.repository.RentalRepository;
import com.fsd.hellovelo.repository.StationRepository;
import com.fsd.hellovelo.repository.UserRepository;
import com.fsd.hellovelo.repository.*;
import com.fsd.hellovelo.security.services.UserDetailsImpl;
import com.stripe.Stripe;
import com.stripe.exception.StripeException;
import com.stripe.model.UsageRecord;
import com.stripe.param.UsageRecordCreateOnSubscriptionItemParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Optional;

@Service
public class UserRentalService {

    private final RentalRepository rentalRepository;
    private final UserRepository userRepository;
    private final BikeRepository bikeRepository;
    private final StationRepository stationRepository;
    private final SubscriptionRepository subscriptionRepository;
    private final OverageChargeRepository overageChargeRepository;
    private final CouponRepository couponRepository;
    private final static int minutesBeforeOverage = 45;
    private final InvoiceRepository invoiceRepository;

    @Autowired
    private UserRentalRepoPagination UserRentalRepoPagination;


    @Autowired
    public UserRentalService(RentalRepository rentalRepository, UserRepository userRepository, BikeRepository bikeRepository, StationRepository stationRepository, SubscriptionRepository subscriptionRepository, OverageChargeRepository overageChargeRepository, CouponRepository couponRepository, InvoiceRepository invoiceRepository) {
        this.rentalRepository = rentalRepository;
        this.userRepository = userRepository;
        this.bikeRepository = bikeRepository;
        this.stationRepository = stationRepository;
        this.subscriptionRepository = subscriptionRepository;
        this.overageChargeRepository = overageChargeRepository;
        this.couponRepository = couponRepository;
        this.invoiceRepository = invoiceRepository;
    }

    //create a rental - user is renting a bike
    public void rentBike(RentalRequest rental) {
        UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (!userDetails.isProvisionAccess()) {
            throw new IllegalArgumentException("Error: User is not valid.");
        }

        User user = userRepository.findById(userDetails.getId()).orElseThrow(() -> new ResourceNotFoundException("Error: User could not be found."));

        if (rentalRepository.findFirstByEndStationNullAndUserEquals(user).isPresent()) {
            throw new IllegalArgumentException("Error: A bike is already in use by this user.");
        }

        Bike bike = bikeRepository.findById(rental.getBikeId()).orElseThrow(() -> new ResourceNotFoundException("Error: Bike with id " + rental.getUserId() + " could not be found."));
        if (bike.isCurrentlyInUse() || bike.getStation().getId() == 1 || bike.getStatus() != EStatus.ACTIVE) {
            throw new IllegalArgumentException("Error: Bike cannot be used. Try a different bike.");
        }

        if (bike.getStation().getStatus() != EStatus.ACTIVE) {
            throw new IllegalArgumentException("Error: Bikes cannot be rented from this station. Try a different station.");
        }

        Rental newRental = new Rental(LocalDateTime.now(), user, bike, bike.getStation());
        newRental.setDiscountedStartStation(isStationDiscounted(bike.getStation(), true));
        bike.setCurrentlyInUse(true);
        bike.setStation(null);

        bikeRepository.save(bike);
        rentalRepository.save(newRental);
    }

    public void returnBike(RentalRequest rental) throws StripeException {
        if (rental.getEndStationId() == null || rental.getId() == null) {
            throw new IllegalArgumentException("Error: rental id and end station id cannot be null.");
        }

        UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        Station endStation = stationRepository.findById(rental.getEndStationId()).orElseThrow(() -> new ResourceNotFoundException("Error: End station could not be found."));
        Rental rentalRecord = rentalRepository.findById(rental.getId()).orElseThrow(() -> new ResourceNotFoundException("Error: Rental not found."));
        User user = userRepository.findById(userDetails.getId()).orElseThrow(() -> new IllegalArgumentException("Error: User not found."));

        Bike bike = rentalRecord.getBike();
        if (bike == null) {
            throw new ResourceNotFoundException("Error: Bike not found.");
        }
        if (!rentalRecord.getUser().getId().equals(userDetails.getId())) {
            throw new IllegalArgumentException("Error: This user cannot modify this rental.");
        }

        if(endStation.getStatus() != EStatus.ACTIVE || endStation.getTotalSlots() <= bikeRepository.countAllByStation(endStation)){
            throw new IllegalArgumentException("Error: Bikes cannot be left at this station for now. Please try again later.");
        }

        rentalRecord.setEndStation(endStation);
        rentalRecord.setEndTime(LocalDateTime.now());

        setOverage(rentalRecord, user);
        setDiscount(rentalRecord, user);

        rentalRepository.save(rentalRecord);

        bike.setStation(endStation);
        bike.setCurrentlyInUse(false);
        bikeRepository.save(bike);
    }

    private boolean isStationDiscounted(Station station, boolean startStation) {
        Long bikesInStation = bikeRepository.countAllByStation(station);
        //if start station and station is over 90% capacity
        if (startStation && bikesInStation >= (station.getTotalSlots() * .9)) {
            return true;
        }
        //if end station and under 10% capacity or 2 or fewer bikes
        return !startStation && (bikesInStation <= (station.getTotalSlots() * .1) || bikesInStation <= 2);
    }

    private void setDiscount(Rental rentalRecord, User user) {
        if(!rentalRecord.isDiscountedStartStation() || !isStationDiscounted(rentalRecord.getEndStation(), false)){
            return;
        }
        Long discountAmountOff = couponRepository.findByTitle("earned_rental_discount").orElseThrow().getAmountOff();
        if(discountAmountOff == null){
            throw new IllegalArgumentException("Error: earned_rental_discount cannot be found!");
        }
        rentalRecord.setDiscountEarned(discountAmountOff);

        Subscription subscription = subscriptionRepository.findFirstByUserAndTypeEqualsAndStatusNot(user, EType.BIKE, EPaymentStatus.CANCELED).orElseThrow();
        Invoice invoice = invoiceRepository.findBySubscriptionAndInvoiceStatusNot(subscription, EInvoiceStatus.PAID).orElseThrow(() -> new ResourceNotFoundException("Error: Invoice not found."));
        long discountSum = invoice.getRentalDiscountSum() + discountAmountOff;
        invoice.setRentalDiscountSum(discountSum);
        invoice.setPaymentPrice(invoice.getPaymentPrice().subtract(BigDecimal.valueOf(discountAmountOff)));
        invoiceRepository.save(invoice);
    }

    public void setOverage(Rental rental, User user) throws StripeException {
        Duration duration = Duration.between(rental.getStartTime(), rental.getEndTime());
        long rentalMinutes = duration.toMinutes();
        if (rentalMinutes > minutesBeforeOverage) {
            Stripe.apiKey = "sk_test_51K6PnvAl7XnbPt50bAvU4L3nGTDI8nXn8a4nPhjTcbznNDfW3j7sBL1UqmL2x5yUVXygbNJ6o32Gg01juu76NkbJ00ereY9xEJ";

            long overageMinutes = rentalMinutes - minutesBeforeOverage;
            Subscription subscription = subscriptionRepository.findFirstByUserAndTypeEqualsAndStatusNot(user, EType.BIKE, EPaymentStatus.CANCELED).orElseThrow(() -> new ResourceNotFoundException("Error: Subscription not found."));
            UsageRecordCreateOnSubscriptionItemParams params =
                    UsageRecordCreateOnSubscriptionItemParams.builder()
                            .setQuantity(overageMinutes)
                            .setTimestamp(System.currentTimeMillis() / 1000L)
                            .setAction(UsageRecordCreateOnSubscriptionItemParams.Action.INCREMENT)
                            .build();

            UsageRecord.createOnSubscriptionItem(subscription.getStripeOverageItemId(), params, null);

            Invoice invoice = invoiceRepository.findBySubscriptionAndInvoiceStatusNot(subscription, EInvoiceStatus.PAID).orElseThrow(() -> new ResourceNotFoundException("Error: Invoice not found."));
            OverageCharge overageCharge = new OverageCharge(rental, user, overageMinutes, invoice);
            invoice.setOverageSum(invoice.getOverageSum() + overageMinutes);
            invoiceRepository.save(invoice);
            overageChargeRepository.save(overageCharge);
        }
    }

    public Rental getCurrentRental() {
        UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        User user = userRepository.findById(userDetails.getId()).orElseThrow(() -> new ResourceNotFoundException("Error: User could not be found."));
        Optional<Rental> rental = rentalRepository.findTopByEndStationNullOrEndTimeNullAndUserEquals(user);
        if (rental.isEmpty()) {
            throw new ResourceNotFoundException("You do not have a bike rental at the moment.");
        }
        return rental.get();
    }

    public Bike findBikeBySerialNumber(UserRentalRequest userRentalRequest) {
        Bike bike = bikeRepository.findBySerialNumber(userRentalRequest.getSerialNumber()).orElseThrow(() -> new ResourceNotFoundException("Error: Bike could not be found."));
        return bike;
    }


    /******************* FIND ALL RENTALS FOR A USER WITH PAGINATION AND SORTING ******************/

    public Page<Rental> listOfUserRentals(int pageNumber, int pageSize, String sortBy, String sortDir) {

        UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
//        if(!userDetails.isProvisionAccess()){
//            throw new IllegalArgumentException("Error: User is not valid.");
//        }
        User user = userRepository.findById(userDetails.getId()).orElseThrow(()-> new ResourceNotFoundException("Error: User could not be found."));

        Page<Rental> userRentalList= UserRentalRepoPagination.findAllByUserEquals(user,
                PageRequest.of(pageNumber, pageSize,
                        sortDir.equalsIgnoreCase("asc") ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending()
                ));

        for (Rental rental:
                userRentalList
        ) {
            OverageCharge overageCharge = overageChargeRepository.findByRentalId(rental.getId());
            if(overageCharge != null) {
                rental.setOverageMinutes(overageCharge.getMinutesOver());
            }
            if(overageCharge == null && rental.getEndStation() != null){
                rental.setOverageMinutes(0L);
            }
            if(overageCharge == null && rental.getEndStation() == null){
                Duration duration = Duration.between(rental.getStartTime(), LocalDateTime.now());
                Long rentalMinutes = duration.toMinutes();
                if(rentalMinutes > minutesBeforeOverage){
                    Long currOverage = (Long) (rentalMinutes - minutesBeforeOverage);
                    rental.setOverageMinutes(currOverage);
                    System.out.println(rentalMinutes);
                }else{
                    rental.setOverageMinutes(0L);
                }
            }
        }
        return userRentalList;
//        return UserRentalRepoPagination.findAllByUserEquals(user,
//                PageRequest.of(pageNumber, pageSize,
//                                sortDir.equalsIgnoreCase("asc") ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending()
//                        ));
    }
}
