package com.izouma.nineth.service;

import com.izouma.nineth.domain.*;
import com.izouma.nineth.domain.Collection;
import com.izouma.nineth.dto.CollectionDTO;
import com.izouma.nineth.dto.CreateBlindBox;
import com.izouma.nineth.dto.PageQuery;
import com.izouma.nineth.enums.CollectionType;
import com.izouma.nineth.exception.BusinessException;
import com.izouma.nineth.repo.*;
import com.izouma.nineth.utils.JpaUtils;
import com.izouma.nineth.utils.SecurityUtils;
import lombok.AllArgsConstructor;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.Predicate;
import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
public class CollectionService {

    private CollectionRepo   collectionRepo;
    private LikeRepo         likeRepo;
    private BlindBoxItemRepo blindBoxItemRepo;
    private AppointmentRepo  appointmentRepo;
    private UserRepo         userRepo;
    private AssetService     assetService;

    public Page<Collection> all(PageQuery pageQuery) {
        pageQuery.getQuery().put("del", false);
        String type = MapUtils.getString(pageQuery.getQuery(), "type", "DEFAULT");
        pageQuery.getQuery().remove("type");

        Specification<Collection> specification = JpaUtils.toSpecification(pageQuery, Collection.class);
        PageRequest pageRequest = JpaUtils.toPageRequest(pageQuery);
        if (pageRequest.getSort().stream().noneMatch(order -> order.getProperty().equals("createdAt"))) {
            pageRequest = PageRequest.of(pageRequest.getPageNumber(), pageQuery.getSize(),
                    pageRequest.getSort().and(Sort.by("createdAt").descending()));
        }

        specification = specification.and((Specification<Collection>) (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> and = new ArrayList<>();

            if (StringUtils.isNotEmpty(type) && !"all".equalsIgnoreCase(type)) {
                try {
                    if (type.contains(",")) {
                        and.add(root.get("type")
                                .in(Arrays.stream(type.split(",")).map(s -> Enum.valueOf(CollectionType.class, s))
                                        .collect(Collectors.toList())));
                    } else {
                        and.add(criteriaBuilder.equal(root.get("type"), Enum.valueOf(CollectionType.class, type)));
                    }
                } catch (Exception e) {

                }
            }
            return criteriaBuilder.and(and.toArray(new Predicate[0]));
        });
        return collectionRepo.findAll(specification, pageRequest);
    }

    public Collection create(Collection record) {
        User minter = userRepo.findById(record.getMinterId()).orElse(SecurityUtils.getAuthenticatedUser());
        record.setMinter(minter.getNickname());
        record.setMinterId(minter.getId());
        record.setMinterAvatar(minter.getAvatar());
        record.setOwner(minter.getNickname());
        record.setOwnerId(minter.getId());
        record.setOwnerAvatar(minter.getAvatar());
        record.setStock(record.getTotal());
        record.setSale(0);
        if (record.isScheduleSale()) {
            if (record.getStartTime() == null) {
                throw new BusinessException("请填写定时发布时间");
            }
            record.setOnShelf(record.getStartTime().isBefore(LocalDateTime.now()));
        }
        return collectionRepo.save(record);
    }

    public CollectionDTO toDTO(Collection collection) {
        return toDTO(collection, true);
    }

    public CollectionDTO toDTO(Collection collection, boolean join) {
        CollectionDTO collectionDTO = new CollectionDTO();
        BeanUtils.copyProperties(collection, collectionDTO);
        if (join) {
            if (SecurityUtils.getAuthenticatedUser() != null) {
                List<Like> list = likeRepo.findByUserIdAndCollectionId(SecurityUtils.getAuthenticatedUser().getId(),
                        collection.getId());
                collectionDTO.setLiked(!list.isEmpty());
                if (collection.getType() == CollectionType.BLIND_BOX) {
                    collectionDTO.setAppointment(appointmentRepo.findFirstByBlindBoxId(collection.getId()).isPresent());
                }
            }
        }
        return collectionDTO;
    }

    public List<CollectionDTO> toDTO(List<Collection> collections) {
        List<Like> likes = new ArrayList<>();
        List<Appointment> appointments = new ArrayList<>();
        if (SecurityUtils.getAuthenticatedUser() != null) {
            likes.addAll(likeRepo.findByUserId(SecurityUtils.getAuthenticatedUser().getId()));
            appointments.addAll(appointmentRepo.findByUserId(SecurityUtils.getAuthenticatedUser().getId()));
        }
        return collections.stream().parallel().map(collection -> {
            CollectionDTO dto = toDTO(collection, false);
            if (!likes.isEmpty()) {
                dto.setLiked(likes.stream().anyMatch(l -> l.getCollectionId().equals(collection.getId())));
            }
            if (!appointments.isEmpty()) {
                dto.setAppointment(appointments.stream().anyMatch(a -> a.getBlindBoxId().equals(collection.getId())));
            }
            return dto;
        }).collect(Collectors.toList());
    }

    public Page<CollectionDTO> toDTO(Page<Collection> collections) {
        List<CollectionDTO> userDTOS = toDTO(collections.getContent());
        return new PageImpl<>(userDTOS, collections.getPageable(), collections.getTotalElements());
    }

    @Transactional
    public Collection createBlindBox(CreateBlindBox createBlindBox) {
        Collection blindBox = createBlindBox.getBlindBox();

        List<Collection> list =
                collectionRepo.findAllById(createBlindBox.getItems().stream().map(BlindBoxItem::getCollectionId)
                        .collect(Collectors.toSet()));
        for (BlindBoxItem item : createBlindBox.getItems()) {
            Collection collection = list.stream().filter(i -> i.getId().equals(item.getCollectionId())).findAny()
                    .orElseThrow(new BusinessException("所选藏品不存在"));
            if (item.getTotal() > collection.getStock()) {
                throw new BusinessException("所选藏品库存不足:" + collection.getName());
            }
        }

        User user = userRepo.findById(blindBox.getMinterId()).orElse(SecurityUtils.getAuthenticatedUser());
        blindBox.setMinter(user.getNickname());
        blindBox.setMinterId(user.getId());
        blindBox.setMinterAvatar(user.getAvatar());
        blindBox.setOwner(user.getNickname());
        blindBox.setOwnerId(user.getId());
        blindBox.setOwnerAvatar(user.getAvatar());
        blindBox.setStock(blindBox.getTotal());
        blindBox.setSale(0);
        collectionRepo.save(blindBox);
        for (BlindBoxItem item : createBlindBox.getItems()) {
            Collection collection = list.stream().filter(i -> i.getId().equals(item.getCollectionId())).findAny()
                    .orElseThrow(new BusinessException("所选藏品不存在"));
            collection.setStock(collection.getStock() - item.getTotal());
            collectionRepo.save(collection);
            BlindBoxItem blindBoxItem = new BlindBoxItem();
            BeanUtils.copyProperties(collection, blindBoxItem);
            blindBoxItem.setId(null);
            blindBoxItem.setCollectionId(item.getCollectionId());
            blindBoxItem.setSale(0);
            blindBoxItem.setTotal(item.getTotal());
            blindBoxItem.setStock(item.getTotal());
            blindBoxItem.setRare(item.isRare());
            blindBoxItem.setBlindBoxId(blindBox.getId());
            blindBoxItemRepo.save(blindBoxItem);
        }

        return blindBox;
    }

    public void appointment(Long id, Long userId) {
        Collection collection = collectionRepo.findById(id).orElseThrow(new BusinessException("无记录"));
        if (collection.getType() != CollectionType.BLIND_BOX) {
            throw new BusinessException("非盲盒，无需预约");
        }

        if (collection.getStartTime().isBefore(LocalDateTime.now())) {
            throw new BusinessException("盲盒已开售，无需预约");
        }

        appointmentRepo.save(Appointment.builder()
                .userId(userId)
                .blindBoxId(id)
                .build());
    }

    @Scheduled(fixedRate = 60000)
    public void scheduleOnShelf() {
        List<Collection> collections = collectionRepo.findByScheduleSaleTrueAndOnShelfFalseAndStartTimeBeforeAndDelFalse(LocalDateTime.now());
        for (Collection collection : collections) {
            collection.setOnShelf(true);
        }
        collectionRepo.saveAll(collections);
    }


    public BlindBoxItem draw(Long collectionId) {
        List<BlindBoxItem> items = blindBoxItemRepo.findByBlindBoxId(collectionId);

        Map<BlindBoxItem, Range<Integer>> randomRange = new HashMap<>();
        int c = 0, sum = 0;
        for (BlindBoxItem item : items) {
            randomRange.put(item, Range.between(c, c + item.getStock()));
            c += item.getStock();
            sum += item.getStock();
        }

        int retry = 0;
        BlindBoxItem winItem = null;
        while (winItem == null) {
            retry++;
            int rand = RandomUtils.nextInt(0, sum + 1);
            for (Map.Entry<BlindBoxItem, Range<Integer>> entry : randomRange.entrySet()) {
                BlindBoxItem item = entry.getKey();
                Range<Integer> range = entry.getValue();
                if (rand >= range.getMinimum() && rand < range.getMaximum()) {
                    int total = items.stream().filter(i -> !i.isRare())
                            .mapToInt(BlindBoxItem::getTotal).sum();
                    int stock = items.stream().filter(i -> !i.isRare())
                            .mapToInt(BlindBoxItem::getStock).sum();
                    if (item.isRare()) {
                        double nRate = stock / (double) total;
                        double rRate = (item.getStock() - 1) / (double) item.getTotal();

                        if (Math.abs(nRate - rRate) < (1 / (double) item.getTotal()) || retry > 1 || rRate == 0) {
                            if (!(nRate > 0.1 && item.getStock() == 1)) {
                                winItem = item;
                            }
                        }
                    } else {
                        double nRate = (stock - 1) / (double) total;
                        double rRate = item.getStock() / (double) item.getTotal();

                        if (Math.abs(nRate - rRate) < 0.2 || retry > 1 || nRate == 0) {
                            winItem = item;
                        }
                    }
                }
            }

            if (retry > 100 && winItem == null) {
                throw new BusinessException("盲盒抽卡失败");
            }
        }
        winItem.setStock(winItem.getStock() - 1);
        winItem.setSale(winItem.getSale() + 1);
        blindBoxItemRepo.save(winItem);
        return winItem;
    }

    public void airDrop(Long collectionId, List<Long> userIds) {
        Collection collection = collectionRepo.findById(collectionId).orElseThrow(new BusinessException("藏品不存在"));
        for (Long userId : userIds) {
        }
    }
}
