package com.apobates.forum.core.impl.service;

import com.apobates.forum.core.dao.BoardActionCollectionDao;
import com.apobates.forum.core.dao.BoardConfigDao;
import com.apobates.forum.core.dao.BoardDao;
import com.apobates.forum.core.dao.BoardGroupDao;
import com.apobates.forum.core.dao.BoardStatsDao;
import com.apobates.forum.core.entity.Board;
import com.apobates.forum.core.entity.BoardActionCollection;
import com.apobates.forum.core.entity.BoardConfig;
import com.apobates.forum.core.entity.BoardGroup;
import com.apobates.forum.core.entity.BoardStats;
import com.apobates.forum.core.entity.ForumEntityStatusEnum;
import com.apobates.forum.core.entity.proxy.BoardActionCollectionReplica;
import com.apobates.forum.core.entity.proxy.BoardReplica;
import com.apobates.forum.core.impl.ActionDescriptor;
import com.apobates.forum.core.impl.event.BoardCreateEvent;
import com.apobates.forum.core.service.BoardService;
import com.apobates.forum.event.elderly.ActionEventCulpritor;
import com.apobates.forum.event.elderly.ForumActionEnum;
import com.apobates.forum.utils.Commons;
import com.apobates.forum.utils.persistence.Page;
import com.apobates.forum.utils.persistence.Pageable;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.github.davidmarquis.redisq.producer.MessageProducer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 *
 * @author xiaofanku
 * @since 20200512
 */
@Service
@CacheConfig(cacheNames = "boardCache")
public class BoardServiceImpl implements BoardService{
    @Autowired
    private BoardDao boardDao;
    @Autowired
    private BoardGroupDao boardGroupDao;
    @Autowired
    private BoardStatsDao boardStatsDao;
    @Autowired
    private BoardConfigDao boardConfigDao;
    @Autowired
    private BoardActionCollectionDao boardActionCollectionDao;
    @Autowired @Qualifier("boardCreateProducer")
    private MessageProducer<BoardCreateEvent> boardCreateProducer;
    private final static Logger logger = LoggerFactory.getLogger(BoardServiceImpl.class);
    
    @Cacheable(key = "#id", unless="#result==null")
    @Override
    public Optional<Board> get(long id) {
        if(id>0){
            return boardDao.findOne(id);
        }
        return Optional.empty();
    }
    
    @ActionDescriptor(action=ForumActionEnum.BOARD_LOCK)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> lock(long id, int boardGroupId, ActionEventCulpritor culpritor) { //ASP-B1
        return boardDao.edit(id, ForumEntityStatusEnum.LOCKED);
    }
    
    @ActionDescriptor(action=ForumActionEnum.BOARD_UNLOCK)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> releaseLock(long id, int boardGroupId, ActionEventCulpritor culpritor) { //ASP-B2
        return boardDao.edit(id, ForumEntityStatusEnum.ACTIVE);
    }
    
    @ActionDescriptor(action=ForumActionEnum.BOARD_REMOVED)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> remove(long id, int boardGroupId, ActionEventCulpritor culpritor) { //ASP-B3
        return boardDao.edit(id, ForumEntityStatusEnum.DELETE);
    }
    
    @ActionDescriptor(action=ForumActionEnum.BOARD_EDIT)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> edit(long id, int boardGroupId, String description, ForumEntityStatusEnum status, ActionEventCulpritor culpritor)throws IllegalArgumentException { //ASP-B6
        Board b = get(id).orElseThrow(()->new IllegalArgumentException("版块不存在"));
        b.setDescription(description);
        b.setStatus(status);
        
        return boardDao.edit(b);
    }
    
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> edit(long id, Board updateBoard)throws IllegalArgumentException {
        Board b = get(id).orElseThrow(() -> new IllegalArgumentException("版块不存在"));
        b.setTitle(updateBoard.getTitle());
        b.setDescription(updateBoard.getDescription());
        b.setStatus(updateBoard.getStatus());
        b.setRanking(updateBoard.getRanking());
        //b.setVolumesId(updateBoard.getVolumesId()); 这不在合并版块吗?
        if (Commons.isNotBlank(updateBoard.getImageAddr())) {
            b.setImageAddr(updateBoard.getImageAddr());
        }
        return boardDao.edit(b);
    }
    
    @ActionDescriptor(action=ForumActionEnum.BOARD_CONFIG_EDIT)
    @CacheEvict(key="'board_config_'+#id")
    @Override
    public Optional<Boolean> editBoardConfig(long id, int boardGroupId, long configId, BoardConfig updateConfig, ActionEventCulpritor culpritor)throws IllegalArgumentException { //ASP-B7
        BoardConfig bc = boardConfigDao.findOne(configId).orElseThrow(() -> new IllegalArgumentException("版块配置文件不存在"));
        bc.setReadWrite(updateConfig.isReadWrite());
        //写
        bc.setEditMinute(updateConfig.getEditMinute());
        bc.setWriteMinInterrupt(updateConfig.getWriteMinInterrupt());
        bc.setWriteMinScore(updateConfig.getWriteMinScore());
        bc.setWriteLowMemberGroup(updateConfig.getWriteLowMemberGroup());
        bc.setWriteLowMemberRole(updateConfig.getWriteLowMemberRole());
        bc.setWriteLowMemberLevel(updateConfig.getWriteLowMemberLevel());
        //读
        bc.setReadMinScore(updateConfig.getReadMinScore());
        bc.setReadLowMemberGroup(updateConfig.getReadLowMemberGroup());
        bc.setReadLowMemberRole(updateConfig.getReadLowMemberRole());
        bc.setReadLowMemberLevel(updateConfig.getReadLowMemberLevel());
        //不分读写
        bc.setIpFilter(updateConfig.isIpFilter());
        return boardConfigDao.edit(bc);
    }
    
    @Cacheable(key = "'volumes_'+#boardGroupId")
    @Override
    public List<BoardReplica> getAllUsedByVolumesId(int boardGroupId) {
        return boardDao.findUsed(boardGroupId).map(BoardReplica.copyBoard).filter(Objects::nonNull).sorted(Comparator.comparing(Board::getRanking)).collect(Collectors.toList());
    }
    
    @CacheEvict(key="'volumes_'+#boardGroupId")
    @Override
    public Optional<Board> create(int boardGroupId, String title, String description, String encodeImageAddr, ForumEntityStatusEnum status, int ranking) {
        Board board = new Board(title, description, encodeImageAddr, boardGroupId, status, ranking);
        try {
            boardDao.save(board);
            if (board.getId() > 0) {
                boardCreateProducer.create(new BoardCreateEvent(board)).submit();
                return Optional.of(board);
            }
        } catch (Exception e) {
            if(logger.isDebugEnabled()){
                logger.debug("版块创建失败", e);
            }
        }
        return Optional.empty();
    }
    
    @Override
    public boolean isFavorited(long id, long memberId) {
        return boardActionCollectionDao.isFavorited(id, memberId).orElse(false);
    }
    
    @ActionDescriptor(action=ForumActionEnum.BOARD_FAVORITE)
    @Override
    public Optional<Boolean> favorite(long id, ActionEventCulpritor culpritor)throws IllegalStateException { //ASP-B4 | 20200709
        if (!isFavorited(id, culpritor.getMemberId())) {
            throw new IllegalStateException("收藏记录已经存在");
        }
        //更新版块统计的fac
        return boardStatsDao.plusFavorites(id, culpritor.getMemberId());
    }
    
    @ActionDescriptor(action=ForumActionEnum.BOARD_FAVORITE_CANCEL)
    @Override
    public Optional<Boolean> removeFavorite(long id, ActionEventCulpritor culpritor)throws IllegalStateException { //ASP-B5
        long data = boardActionCollectionDao.countMemberAction(culpritor.getMemberId(), id, ForumActionEnum.BOARD_FAVORITE_CANCEL);
        if (data > 0) {
            throw new IllegalStateException("已经取消收藏");
        }
        //更新版块统计的fac
        return boardStatsDao.negateFavorites(id, culpritor.getMemberId());
    }
    
    @Override
    public List<Long> removeFavorites(Set<Long> idSet, ActionEventCulpritor culpritor)throws IllegalStateException {
        if (null == idSet || idSet.isEmpty()) {
            return Collections.emptyList();
        }
        return idSet.stream().filter((id)->removeFavorite(id, culpritor).orElse(false)).collect(Collectors.toList());
    }
    
    @Override
    public Optional<Boolean> editTerm(long id, String title, String description, String directoryNames, ForumEntityStatusEnum status)throws IllegalArgumentException {
        Board term = getTermById(id).orElseThrow(() -> new IllegalArgumentException("子栏目不存在或暂时不可用"));
        term.setTitle(title);
        term.setDescription(description);
        term.setDirectoryNames(directoryNames);
        term.setStatus(status);
        return boardDao.edit(term);
    }
    
    @Override
    public long buildOrigin(int sectionId, String title, String description, String directoryNames, ForumEntityStatusEnum status)throws IllegalArgumentException,IllegalStateException {
        if (!checkTermDirectNameUnique(directoryNames).orElse(false)) {
            throw new IllegalArgumentException("子栏目目录名称唯一性检查失败");
        }
        Board board = new Board();
        board.setTitle(title);
        board.setDescription(description);
        board.setVolumesId(sectionId);
        board.setStatus(status);
        board.setOrigin(true);
        board.setDirectoryNames(directoryNames); //需要保证唯一
        board.setEntryDateTime(LocalDateTime.now());
        board.setRanking(0);
        //
        try {
            boardDao.save(board);
            if (board.getId() > 0) {
                boardCreateProducer.create(new BoardCreateEvent(board, BoardConfig.originConfig(board.getId()))).submit();
                return board.getId();
            }
        } catch (Exception e) {
            if(logger.isDebugEnabled()){
                logger.debug("版块创建失败", e);
            }
        }
        return 0L;
    }
    
    @Override
    public Stream<BoardReplica> getMemberFavorites(final long memberId, final int size) {
        if (memberId < 1) {
            return Stream.empty();
        }
        final BiFunction<Board,BoardStats,BoardReplica> bi = (b, bs)->{
            BoardReplica br = Optional.ofNullable(b).map(BoardReplica.copyBoard).orElseGet(BoardReplica::new);
            if(null != bs){
                br.setStats(bs);
            }
            return br;
        };
        CompletableFuture<Set<Long>> boardIdSet = CompletableFuture.supplyAsync(()->boardActionCollectionDao.findAllMemberFavoriteBoard(memberId, size))
                                            .thenApply((Stream<BoardActionCollection> rs) -> rs.map(BoardActionCollection::getBoardId).collect(Collectors.toSet()))
                                            .completeOnTimeout(Collections.emptySet(), 1, TimeUnit.SECONDS);
        CompletableFuture<Map<Long,Board>> boardMapCF = boardIdSet.thenCompose(bIdSet-> CompletableFuture.supplyAsync(()->boardDao.findAllById(bIdSet)))
                                            .thenApply((Stream<Board> bs) -> bs.collect(Collectors.toMap(Board::getId, Function.identity())))
                                            .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS);
        CompletableFuture<Map<Long,BoardStats>> boardStatsMapCF = boardIdSet.thenCompose(bIdSet-> CompletableFuture.supplyAsync(()->boardStatsDao.findAllByBoardId(bIdSet)))
                                            .thenApply((Stream<BoardStats> bss)->bss.collect(Collectors.toMap(BoardStats::getBoardId, Function.identity())))
                                            .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS);
        return boardMapCF.thenCombine(boardStatsMapCF, (bs, bss)->{
            return bs.values().stream().map(board->bi.apply(board, bss.get(board.getId())));
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<Boolean> checkTermDirectNameUnique(String directoryNames)throws IllegalArgumentException,IllegalStateException {
        if (!Commons.isNotBlank(directoryNames)) {
            throw new IllegalArgumentException("子栏目的目录名称不可用");
        }
        long termId = boardDao.existsTermDirectoryNames(directoryNames);
        if (termId > 0) {
            throw new IllegalStateException("子栏目的目录名称已被使用");
        }
        return Optional.of(true);
    }
    
    @Override
    public Optional<Boolean> checkTermDirectNameUnique(String directoryNames, long termId)throws IllegalArgumentException,IllegalStateException {
        if (termId <= 0) {
            return checkTermDirectNameUnique(directoryNames);
        }
        //
        if (!Commons.isNotBlank(directoryNames)) {
            throw new IllegalArgumentException("子栏目的目录名称不可用");
        }
        long count = boardDao.existsTermDirectoryNames(directoryNames, termId);
        if (count > 0){
            throw new IllegalStateException("子栏目的目录名称已被使用");
        }
        return Optional.of(true);
    }
    
    @Override
    public Stream<Board> getAll() {
        return boardDao.findAll();
    }
    
    @Override
    public Stream<Board> getAllUsed() {
        return boardDao.findUsed();
    }
    
    @Override
    public Stream<Board> getAllByVolumesId(int boardGroupId) {
        return boardDao.findAllByBoardGroup(boardGroupId);
    }
    
    @Override
    public Stream<Board> getAllTermById(Collection<Long> idList) {
        if (null == idList || idList.isEmpty()) {
            return Stream.empty();
        }
        return boardDao.findAllTermById(idList);
    }
    
    @Override
    public List<Board> getAllTermByOriginId(int sectionId) {
        return (sectionId>0)?boardDao.findAllTermByOriginId(sectionId):getAllTerm();
    }
    
    @Override
    public List<Board> getAllTerm() {
        return boardDao.findAllTerm();
    }
    
    @Override
    public Optional<Board> getTermByDirectoryNames(String directoryNames) {
        return boardDao.findOneTermByDirectoryNames(directoryNames);
    }
    
    @Override
    public Optional<Board> getTermById(long termId) {
        if (termId > 0) {
            return boardDao.findOneTermById(termId);
        }
        return Optional.empty();
    }
    
    @Override
    public Optional<BoardReplica> get(final long id, final int boardGroupId){
        CompletableFuture<BoardGroup> bg = CompletableFuture.supplyAsync(()->boardGroupDao.findOneById(boardGroupId)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getBoardAsync(id).thenCombineAsync(bg, (board, boardGroup)->{
            Optional<BoardReplica> br = Optional.ofNullable(board).map(BoardReplica.copyBoard);
            br.ifPresent(boardReplica->boardReplica.copyVolumes(boardGroup));
            return br;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<BoardReplica> getTermRelateSection(final String directoryNames){ 
        CompletableFuture<Optional<BoardReplica>> br = CompletableFuture.supplyAsync(()->boardDao.findOneTermAssSection(directoryNames).map(BoardReplica.copyBoard));
        return br.thenCompose(boardReplica->
                CompletableFuture.supplyAsync(()->
                            boardGroupDao.findOneSectionById(boardReplica.get().getVolumesId()).orElse(null))
                        .thenApply(boardGroup->{
                            boardReplica.ifPresent(brObj->brObj.copyVolumes(boardGroup));
                            return boardReplica;
                        }).completeOnTimeout(null, 1, TimeUnit.SECONDS))
             .orTimeout(1, TimeUnit.SECONDS)
             .join();
    }
    
    @Override
    public Optional<BoardReplica> getBoardConfig(final long id){ 
        CompletableFuture<BoardConfig> bc = CompletableFuture.supplyAsync(()->boardConfigDao.findOneByBoard(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getBoardAsync(id).thenCombineAsync(bc, (board, boardConfig)->{
            Optional<BoardReplica> br = Optional.ofNullable(board).map(BoardReplica.copyBoard);
            br.ifPresent(boardReplica->boardReplica.setConfigure(boardConfig));
            return br;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<BoardReplica> getBoardStats(final long id){ 
        CompletableFuture<BoardStats> bs = CompletableFuture.supplyAsync(()->boardStatsDao.findOneByBoard(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getBoardAsync(id).thenCombineAsync(bs, (board, boardStats)->{
            Optional<BoardReplica> br = Optional.ofNullable(board).map(BoardReplica.copyBoard);
            br.ifPresent(boardReplica->boardReplica.setStats(boardStats));
            return br;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Map<Long, String> getAllById(Collection<Long> idList) {
        if (null == idList || idList.isEmpty()) {
            return Collections.emptyMap();
        }
        return boardDao.findAllById(idList).collect(Collectors.toMap(Board::getId, Board::getTitle));
    }
    
    @Override
    public Page<BoardActionCollectionReplica> getFavoritesForMember(long memberId, Pageable pageable) {
        Page<BoardActionCollection> data = boardActionCollectionDao.findAllByMember(memberId, ForumActionEnum.BOARD_FAVORITE, pageable);
        //
        final long total = data.getTotalElements();
        final Stream<BoardActionCollectionReplica> result = associatedBoardAsync(data.getResult().collect(Collectors.toList()));
        //
        return new Page<BoardActionCollectionReplica>() {
            @Override
            public long getTotalElements() {
                return total;
            }
            
            @Override
            public Stream<BoardActionCollectionReplica> getResult() {
                return result;
            }
        };
    }
    
    @Override
    public long countFavoritesForMember(long memberId) {
        return boardActionCollectionDao.findAllByMemberCount(memberId, ForumActionEnum.BOARD_FAVORITE);
    }
    
    //[异步]级联话题统计
    private Stream<BoardActionCollectionReplica> associatedBoardAsync(final List<BoardActionCollection> data) {
        if (null == data || data.isEmpty()) {
            return Stream.empty();
        }
        BiFunction<BoardActionCollection, Board, BoardActionCollectionReplica> bi = (bac, b)->{
            BoardActionCollectionReplica br = Optional.ofNullable(bac).map(BoardActionCollectionReplica.copyBoardAction).orElseGet(BoardActionCollectionReplica::new);
            if(null != b){
                br.setBoard(b);
            }
            return br;
        };
        Map<Long, Board> boards = CompletableFuture.supplyAsync(()->data.stream().map(BoardActionCollection::getBoardId).collect(Collectors.toSet()))
                                .thenCompose(boardIdSet-> CompletableFuture.supplyAsync(()->boardDao.findAllById(boardIdSet).collect(Collectors.toMap(Board::getId, Function.identity())))
                                                        .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS))
                                .orTimeout(1, TimeUnit.SECONDS)
                                .join();
        return data.parallelStream().map(bac->bi.apply(bac, boards.get(bac.getBoardId())));
    }
    private CompletableFuture<Board> getBoardAsync(final long id){
        return CompletableFuture.supplyAsync(()->boardDao.findOneById(id));
    }
}