package me.qwq.doghouse.service;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.toolkit.LambdaUtils;
import com.github.yulichang.wrapper.MPJLambdaWrapper;

import me.qwq.doghouse.annotation.CacheEvictTags;
import me.qwq.doghouse.annotation.CacheEvictTags.Tag;
import me.qwq.doghouse.cache.CacheConstants;
import me.qwq.doghouse.component.Rk;
import me.qwq.doghouse.dao.CommentMapper;
import me.qwq.doghouse.entity.Comment;
import me.qwq.doghouse.entity.Post;
import me.qwq.doghouse.entity.config.MailConfig;
import me.qwq.doghouse.entity.config.SiteConfig;
import me.qwq.doghouse.enums.CommentStatusEnum;
import me.qwq.doghouse.enums.FuncsEnum;
import me.qwq.doghouse.enums.PostStatusEnum;
import me.qwq.doghouse.enums.PostTypeEnum;
import me.qwq.doghouse.service.post.PostService;
import me.qwq.doghouse.util.SessionUtils;

import java.io.Serializable;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

/**
 * 评论服务
 */
@Service
public class CommentService extends BaseServiceImpl<CommentMapper, Comment> {

    @Autowired
    MailConfig mailConfig;
    
    @Autowired
    SiteConfig siteConfig;
    
    @Autowired
    PostService postService;
    
    @Autowired
    CacheManager cacheManager;

    @Autowired
    JdbcTemplate jdbcTemplate;
    
    /**
     * 重写以启用缓存，类内调用需要 proxy()
     */
    @Override
    @Cacheable(value=CacheConstants.Comments.GET_BY_ID)
    public Comment getById(Serializable id) {
        return super.getById(id);
    }

    public IPage<Comment> getPostCommentPage(Long page, Long limit, Long postId){
        if (Objects.isNull(page) || page <= 0) {
            page = 1L;
        }
        if (Objects.isNull(limit) || limit <= 0) {
            limit = 10L;
        }
        Page<Comment> result = new Page<Comment>(page, limit);
        Set<Long> sessionCommentIdList = SessionUtils.getCommentIdSet(session);
        Boolean sessionHasCommentId = !CollectionUtils.isEmpty(sessionCommentIdList);
        
        this.page(result, new QueryWrapper<Comment>()
                .lambda()
                .eq(Comment::getPostId, postId)
                .nested(i -> i
                        .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED)
                        .or(sessionHasCommentId)
                        .in(sessionHasCommentId, Comment::getId, sessionCommentIdList))
                .eq(Comment::getIsDeleted, false)
                .eq(Comment::getReplyTo, -1)
                .orderByAsc(Comment::getCreateTime));
        for(Comment comment : result.getRecords()) {
            getFullRelativeComment(comment, -1);
        }
        return result;
    }
    
    /**
     * 获取完整的有关的评论，并设置子评论的 IPostMess 属性（文章相关属性）、可编辑属性
     * @param comment 父评论
     * @param maxLevel 递归的最大层级，当起始 maxLevel > 0 时，每递归一级则递减，
     * 直到 0 时终止；当起始 maxLevel == 0 时，不进行递归；当起始 maxLevel &lt; 0
     * 时，一直递归至没有子级为止。
     */
    public void getFullRelativeComment(Comment comment, int maxLevel) {
        if(comment == null) return;
        if(maxLevel == 0) return;
        
        Set<Long> sessionCommentIdList = SessionUtils.getCommentIdSet(session);
        Boolean sessionHasCommentId = !CollectionUtils.isEmpty(sessionCommentIdList);
        
        List<Comment> subComments = list(new QueryWrapper<Comment>()
                .lambda()
                .eq(Comment::getPostId, comment.getPostId())
                .nested(i -> i
                        .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED)
                        .or(sessionHasCommentId)
                        .in(sessionHasCommentId, Comment::getId, sessionCommentIdList))
                .eq(Comment::getIsDeleted, false)
                .eq(Comment::getReplyTo, comment.getId())
                .orderByAsc(Comment::getCreateTime));
        if (subComments != null) {
            for(Comment sub : subComments) {
                sub.setPassword(comment.getPassword());
                sub.setType(comment.getType());
                sub.setPostPageName(comment.getPostPageName());
                sub.setPostTitle(comment.getPostTitle());
                getFullRelativeComment(sub, maxLevel--);
            }
        }

        // 设置可编辑属性
        if (isLogin()) {
            // 管理员直接可编辑，无视其他限制
            comment.setEditable(true);
        }
        else if (subComments == null || subComments.size() == 0) {
            // 没有子评论才允许编辑
            
            if (!comment.isMailSent() && SessionUtils.containsCommentId(session, comment.getId())) {
                // 非管理员，但用户 session 表明该评论是该用户提交的
                // 则判断时间间隔是否在规定的可编辑时间以内
                LocalDateTime now = LocalDateTime.now();
                LocalDateTime commentDate = comment.getCreateTime();
                
                Duration duration = 
                    Duration.between(now, commentDate).abs();
                
                // 邮件发送延迟内允许
                comment.setEditable(duration.compareTo(Duration.ofMinutes(
                        mailConfig.getSendingDelay())) < 0);
            }
            return;
        }
        comment.setSubComments(subComments);
    }
    
    /**
     * 获取指定数目和指定文章类型的最新评论列表
     * @param limit 指定数目
     * @param postTypes 指定文章类型枚举列表
     * @return
     */
    public List<Comment> getLatestComments(Long limit, PostTypeEnum...postTypes) {
        return proxy().cacheableGetLatestComments(limit, isLogin(), postTypes);
    }
    
    /**
     * 最终缓存方法，不应直接使用，应使用 proxy()
     * @param limit
     * @param isLogin
     * @param postTypes
     * @return
     */
    @Cacheable(value=CacheConstants.Comments.GET_LATEST_COMMENTS)
    protected List<Comment> cacheableGetLatestComments(Long limit, Boolean isLogin, PostTypeEnum...postTypes) {
        return list(new MPJLambdaWrapper<Comment>()
                .selectAll(Comment.class)
                .selectAs(Post::getPostTitle, LambdaUtils.getName(Comment::getPostTitle))
                .selectAs(Post::getType, LambdaUtils.getName(Comment::getType))
                .selectAs(Post::getPassword, LambdaUtils.getName(Comment::getPassword))
                .selectAs(Post::getPostPageName, LambdaUtils.getName(Comment::getPostPageName))
                .innerJoin(Post.class, Post::getPostId, Comment::getPostId)
                .in(ArrayUtils.isNotEmpty(postTypes), Post::getType, (Object[])postTypes)
                .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED)
                .eq(Comment::getIsDeleted, false)
                .eq(isLogin == null || !isLogin, Post::getPostStatus, PostStatusEnum.RELEASED)
                .orderByDesc(Comment::getCreateTime)
                .last(limit != null, "LIMIT " + limit));
    }
    
    /**
     * 获取指定数目、指定起始层级、指定最大层级和指定文章类型的最新评论列表
     * @param limit 指定数目
     * @param level 指定起始层级
     * @param maxLevel 指定最大层级
     * @param postTypes 指定文章类型枚举列表
     * @return
     */
    public List<Comment> getLatestCommentsWithLevel(Long limit, Integer level, Integer maxLevel, PostTypeEnum...postTypes) {
        return proxy().cacheableGetLatestCommentsWithLevel(limit, level, maxLevel, isLogin(), postTypes);
    }

    /**
     * 最终缓存方法，不应直接使用
     * @param limit
     * @param level
     * @param maxLevel
     * @param isLogin
     * @param postTypes
     * @return
     */
    @Cacheable(value=CacheConstants.Comments.GET_LATEST_COMMENTS_WITH_LEVEL)
    protected List<Comment> cacheableGetLatestCommentsWithLevel(Long limit, Integer level, Integer maxLevel, Boolean isLogin, PostTypeEnum...postTypes) {
        List<Comment> list = list(new MPJLambdaWrapper<Comment>()
                .selectAll(Comment.class)
                .selectAs(Post::getPostTitle, LambdaUtils.getName(Comment::getPostTitle))
                .selectAs(Post::getType, LambdaUtils.getName(Comment::getType))
                .selectAs(Post::getPassword, LambdaUtils.getName(Comment::getPassword))
                .selectAs(Post::getPostPageName, LambdaUtils.getName(Comment::getPostPageName))
                .innerJoin(Post.class, Post::getPostId, Comment::getPostId)
                .in(ArrayUtils.isNotEmpty(postTypes), Post::getType, (Object[])postTypes)
                .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED)
                .eq(Comment::getIsDeleted, false)
                .eq(Comment::getLevel, level == null ? 0 : level)
                .eq(isLogin == null || !isLogin, Post::getPostStatus, PostStatusEnum.RELEASED)
                .orderByDesc(Comment::getCreateTime)
                .last(limit != null, "LIMIT " + limit));
        for (Comment comment : list) {
            proxy().getFullRelativeComment(comment, maxLevel);
        }
        return list;
    }
    
    /**
     * 按照从子到父的顺序，递归获取从指定 commentId 到父级的所有 Comment
     * <p>类内调用需要 proxy()</p>
     * @param commentId 指定的 commentId
     * @param includeSelf 返回列表是否包含本级
     * @return
     */
    @Cacheable(value=CacheConstants.Comments.LIST_FROM_CHILD_TO_PARENT)
    public List<Comment> listFromChildToParent(Long commentId, Boolean includeSelf) {
        List<Comment> list = new ArrayList<>();
        Comment post = getOne(
                new LambdaQueryWrapper<Comment>()
                .eq(Comment::getId, commentId)
                .eq(Comment::getIsDeleted, false)
                .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED));
        if (post == null) {
            return list;
        }
        if (includeSelf) {
            list.add(post);
        }
        Long parentId = post.getReplyTo();
        while (!parentId.equals(-1L)) {
            post = getOne(
                    new LambdaQueryWrapper<Comment>()
                    .eq(Comment::getId, parentId)
                    .eq(Comment::getIsDeleted, false)
                    .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED));
            
            if (post == null) {
                return list;
            }
            parentId = post.getReplyTo();
            list.add(post);
        }
        return list;
    }

    /**
     * 获取跳转到的相对连接。如果需要绝对链接，需要手动拼接 siteConfig.getAddress()
     * <p>类内调用需要 proxy() </p>
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Comments.GET_JUMP_TO_URL, key="#id")
    public String getJumpToUrl(Long id) {
        Comment comment = proxy().getById(id);
        while (comment.getReplyTo() != -1) {
            comment = proxy().getById(comment.getReplyTo());
        }
        
        Long commentPageSize = siteConfig.getCommentPageSize();

        Post post = postService.getById(comment.getPostId());
        
        Long count = (long) count(new LambdaQueryWrapper<Comment>()
                .eq(Comment::getPostId, comment.getPostId())
                .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED)
                .eq(Comment::getIsDeleted, false)
                .eq(Comment::getReplyTo, -1)
                .lt(Comment::getCreateTime, comment.getCreateTime())
                .orderByDesc(Comment::getCreateTime));
        Long onPage = count / commentPageSize + 1;
        return String.format("%s/%s#comment-%s",  post.getLink(), onPage, id);
    }
    
    /**
     * 缓存获取指定 PostId 的评论数
     * @param postId
     * @return
     */
    @Cacheable(value=CacheConstants.Comments.GET_POST_COMMENT_COUNT, key="#postId")
    public Long getPostCommentCount(Long postId) {
        return count(new LambdaQueryWrapper<Comment>()
                .eq(Comment::getPostId, postId)
                .eq(Comment::getIsDeleted, false)
                .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED));
    }
    
    /**
     * 针对已有评论，重新生成其邮箱对应的 Doghash
     * @return 重新生成的邮箱数
     */
    public Long reprepareDogHash() {
        List<Comment> comments = list(
                new MPJLambdaWrapper<Comment>(Comment.class)
                .selectFunc(FuncsEnum.DISTINCT, Comment::getEmail)
                .eq(Comment::getIsDeleted, false)
        );
        List<String> distinctEmails = comments.stream()
                .map(comment -> comment.getEmail().toLowerCase(Locale.ENGLISH))
                .distinct()
                .collect(Collectors.toList());
        distinctEmails.forEach(email -> Rk.Gravatar.prepareDogHash(email));
        return (long) distinctEmails.size();
    }

    /**
     * 为指定评论生成新的邮件退订令牌。
     * @param commentId 评论 ID
     * @return 新生成的退订令牌
     */
    public String refreshUnsubscribeToken(Long commentId) {
        if (commentId == null) {
            return null;
        }
        String token = UUID.randomUUID().toString();
        boolean updated = update(new LambdaUpdateWrapper<Comment>()
                .eq(Comment::getId, commentId)
                .set(Comment::getUnsubscribeToken, token));
        return updated ? token : null;
    }

    /**
     * 消费指定退订令牌，并关闭该邮箱所有评论的回复订阅。
     * <p>只删除本次使用的令牌，其他评论上未使用的令牌会保留。</p>
     * @param token 退订令牌
     * @return 令牌对应的邮箱，令牌不存在时返回 null
     */
    @Transactional
    public String unsubscribeByToken(String token) {
        if (StringUtils.isBlank(token)) {
            return null;
        }

        Comment tokenComment = getUnsubscribeTokenComment(token);
        if (tokenComment == null || StringUtils.isBlank(tokenComment.getEmail())) {
            return null;
        }

        String email = tokenComment.getEmail().toLowerCase(Locale.ENGLISH).trim();
        update(new LambdaUpdateWrapper<Comment>()
                .eq(Comment::getId, tokenComment.getId())
                .set(Comment::getUnsubscribeToken, null));
        update(new LambdaUpdateWrapper<Comment>()
                .eq(Comment::getEmail, email)
                .set(Comment::isSubscribeReply, false));
        return email;
    }

    /**
     * 查询指定退订令牌对应的邮箱，不消费令牌。
     * @param token 退订令牌
     * @return 令牌对应的邮箱，令牌不存在时返回 null
     */
    public String getUnsubscribeEmail(String token) {
        Comment tokenComment = getUnsubscribeTokenComment(token);
        return tokenComment == null ? null : tokenComment.getEmail();
    }
    
    /**
     * 重写以启用缓存驱逐
     */
    @Override
    @CacheEvictTags({
        Tag.Posts,
        Tag.Comments
    })
    public boolean save(Comment entity) {
        prepareUnsubscribeToken(entity);
        Long replyTo = entity.getReplyTo();
        if (replyTo != -1L) {
            Comment replyToComment = proxy().getById(replyTo);
            entity.setLevel(replyToComment.getLevel() + 1);
        }
        return super.save(entity);
    }
    
    /**
     * 重写以启用缓存驱逐
     */
    @Override
    @CacheEvictTags({
        Tag.Posts,
        Tag.Comments
    })
    public boolean saveOrUpdate(Comment entity) {
        prepareUnsubscribeToken(entity);
        Long replyTo = entity.getReplyTo();
        if (replyTo == null) entity.setReplyTo(-1L);
        else if (replyTo != -1L) {
            Comment replyToComment = proxy().getById(replyTo);
            entity.setLevel(replyToComment.getLevel() + 1);
        }
        return super.saveOrUpdate(entity);
    }

    /**
     * 重写以启用缓存驱逐
     */
    @Override
    @CacheEvictTags({
        Tag.Posts,
        Tag.Comments
    })
    public boolean update(Wrapper<Comment> updateWrapper) {
        return super.update(updateWrapper);
    }
    
    private CommentService proxy() {
        return (CommentService) AopContext.currentProxy();
    }

    private void prepareUnsubscribeToken(Comment entity) {
        if (entity != null && entity.isSubscribeReply() && StringUtils.isBlank(entity.getUnsubscribeToken())) {
            entity.setUnsubscribeToken(UUID.randomUUID().toString());
        }
    }

    private Comment getUnsubscribeTokenComment(String token) {
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return getOne(new LambdaQueryWrapper<Comment>()
                .eq(Comment::getUnsubscribeToken, token)
                .last("LIMIT 1"));
    }
}
