package me.qwq.doghouse.service.post;

import lombok.extern.slf4j.Slf4j;
import me.qwq.doghouse.annotation.CacheEvictTags;
import me.qwq.doghouse.annotation.RequireSession;
import me.qwq.doghouse.annotation.UpdatePosts;
import me.qwq.doghouse.cache.CacheConstants;
import me.qwq.doghouse.component.PostTypeComponent;
import me.qwq.doghouse.dao.PostMapper;
import me.qwq.doghouse.entity.Asset;
import me.qwq.doghouse.entity.Cate;
import me.qwq.doghouse.entity.Comment;
import me.qwq.doghouse.entity.Meta;
import me.qwq.doghouse.entity.Relationship;
import me.qwq.doghouse.entity.Tag;
import me.qwq.doghouse.entity.User;
import me.qwq.doghouse.entity.config.SiteConfig;
import me.qwq.doghouse.entity.Post;
import me.qwq.doghouse.enums.CommentStatusEnum;
import me.qwq.doghouse.enums.FuncsEnum;
import me.qwq.doghouse.enums.MetaTypeEnum;
import me.qwq.doghouse.enums.PostStatusEnum;
import me.qwq.doghouse.enums.PostTypeEnum;
import me.qwq.doghouse.exception.PageNotFoundException;
import me.qwq.doghouse.exception.RException;
import me.qwq.doghouse.interfaces.IPostTypeConfig;
import me.qwq.doghouse.pojo.bo.PostPassJar;
import me.qwq.doghouse.pojo.dto.PageQuery;
import me.qwq.doghouse.service.AssetService;
import me.qwq.doghouse.service.BaseServiceImpl;
import me.qwq.doghouse.service.MarkdownService;
import me.qwq.doghouse.service.MetaService;
import me.qwq.doghouse.service.RelationshipService;
import me.qwq.doghouse.util.SessionUtils;
import me.qwq.doghouse.util.SpringContextHolder;

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

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ResolvableType;
import org.springframework.util.ObjectUtils;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 文章服务
 * @param <S> 继承本类的文章服务
 * @param <C> 继承本类的文章类型对应的配置
 * @see IPostTypeConfig
 */
@Slf4j
public abstract class AbstractPostService<S extends AbstractPostService<S, C>, C extends IPostTypeConfig<C>> extends BaseServiceImpl<PostMapper, Post> {

    static final String LIMIT_ = "LIMIT ";
    static final String LIMIT_1 = LIMIT_ + "1";
    
    public static final String KEY_ID = "#id";
    public static final String KEY_NAME = "#name";
    public static final String KEY_PREVIOUS_ID = "#previousId";
    public static final String KEY_NEXT_ID = "#nextId";

    @Autowired
    AssetService assetService;
    
    @Autowired
    SiteConfig siteConfig;
    
    @Autowired
    MarkdownService markdownService;
    
    @Autowired
    MetaService metaService;
    
    @Autowired
    RelationshipService relationshipService;
    
    @Autowired
    CacheManager cacheManager;
    
    @Autowired
    Reflections reflections;
    
    @Autowired
    PostTypeComponent postTypeComponent;

    C pageConfig;
    
    /** 
     * 注意本方法不可信，尤其是某些 PostTypeEnum 未重写本抽象类内的方法的时候。能用 postTypeComponent.getPostTypeConfig(postTypeEnum)
     * 来获取的时候尽量不用本方法，本方法仅在无法确认 postTypeEnum 的时候，默认其类型是继承了本抽象类的 PostTypeService 对应的类型
     * @return
     */
    C getPostTypeConfig() {
        if (pageConfig != null) return pageConfig;
        ResolvableType type = ResolvableType.forClass(this.getClass()).as(AbstractPostService.class);
        ResolvableType iPageConfigType = type.getGeneric(1);
        @SuppressWarnings("unchecked")
        Class<C> iPageConfigClass = (Class<C>) iPageConfigType.resolve();
        pageConfig = SpringContextHolder.getBean(iPageConfigClass);
        return pageConfig;
    }

    /**
     * 获取通用查询 Post 的查询 Wrapper<br>
     * 注意该方法并未指定 PostTypeEnum、是否登录，也未指定排序方式，如有需要，则由使用者指定
     * @return
     */
    public MPJLambdaWrapper<Post> getPostCommonWrapper(){
        return new MPJLambdaWrapper<Post>(Post.class)
                .selectAll(Post.class)
                .selectFunc(FuncsEnum.COUNT_DISTINCT, Comment::getId, Post::getCommentCount)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Tag::getName, Tag::getNo, Tag::getMid), Post::getTagsName)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Tag::getMid, Tag::getNo, Tag::getMid), Post::getTagsId)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Tag::getFriendlyName, Tag::getNo, Tag::getMid), Post::getTagsFriendlyName)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Cate::getName, Cate::getNo, Cate::getMid), Post::getCategoriesName)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Cate::getMid, Cate::getNo, Cate::getMid), Post::getCategoriesId)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Cate::getFriendlyName, Cate::getNo, Cate::getMid), Post::getCategoriesFriendlyName)
                .selectAs(User::getNickname, Post::getCreatorNickname)
                .selectAs(User::getEmail, Post::getEmail)
                .leftJoin(User.class, User::getUserId, Post::getCreatedBy)
                .leftJoin(Relationship.class, Relationship::getPostId, Post::getPostId)
                .leftJoin(Cate.class,
                        i -> i.eq(Cate::getMid, Relationship::getMid)
                              .eq(Cate::getType, MetaTypeEnum.CATEGORY))
                .leftJoin(Tag.class,
                        i -> i.eq(Tag::getMid, Relationship::getMid)
                              .eq(Tag::getType, MetaTypeEnum.TAG))
                .leftJoin(Comment.class,
                        i -> i.eq(Comment::getPostId, Post::getPostId)
                             .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED)
                             .eq(Comment::getIsDeleted, false))
                .groupBy(Post::getPostId);
    }
    
    /**
     * 获取 PostPreviewWrapper<br>
     * 注意该方法并未指定 PostTypeEnum，也未指定排序方式，和是否删除。如有需要，则由使用者指定
     * @param isLogin
     * @return
     */
    public MPJLambdaWrapper<Post> getPostPreviewWrapper(Boolean isLogin) {
        return proxy().getPostCommonWrapper()
                .eq(Post::getIsDeleted, false)
                .and(i -> i
                        .eq(Post::getPostStatus, PostStatusEnum.RELEASED)
                        .or(isLogin != null && isLogin)
                        .eq(isLogin != null && isLogin, Post::getPostStatus, PostStatusEnum.PRIVATE))
                ;
    }
    
    /**
     * 类内调用需要 proxy()
     */
    @Override
    @Cacheable(value=CacheConstants.Posts.GET_BY_ID, key=KEY_ID, condition="#id != null")
    public Post getById(Serializable id) {
        if (id == null) return null;
        return proxy().getOne(proxy().getPostCommonWrapper().eq(Post::getPostId, id).last(LIMIT_1));
    }
    
    /**
     * 通过 postId 或 postName 任选其一查询 Post
     * @param postId
     * @param postName
     * @return
     */
    public Post getByIdOrName(Long postId, String postName) {
        return proxy().getByIdOrName(postId, postName, isLogin());
    }
    
    /**
     * 通过 postId 或 postName 任选其一查询 Post
     * @param postId
     * @param postName
     * @param isLogin
     * @return
     */
    public Post getByIdOrName(Long postId, String postName, Boolean isLogin) {
        if (postId == null && StringUtils.isEmpty(postName)) {
            return null;
        }
        
        if (postId != null) {
            return isLogin ? proxy().cacheableGetByIdLogined(postId) : proxy().cacheableGetByIdUnlogined(postId);
        }
        else if (!StringUtils.isEmpty(postName)) {
            return isLogin ? proxy().cacheableGetByNameLogined(postName) : proxy().cacheableGetByNameUnlogined(postName);
        }
        else {
            return null;
        }
    }
    
    /**
     * 类内调用须 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_BY_ID_UNLOGINED, key=KEY_ID)
    protected Post cacheableGetByIdUnlogined(Long id) {
        return proxy().getOne(
                getPostCommonWrapper()
                .in(Post::getPostStatus,
                    PostStatusEnum.RELEASED, PostStatusEnum.HIDDEN)
                .eq(Post::getIsDeleted, false)
                .eq(Post::getPostId, id)
                .last("LIMIT 1"));
    }

    /**
     * 类内调用须 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_BY_ID_LOGINED, key=KEY_ID)
    protected Post cacheableGetByIdLogined(Long id) {
        return proxy().getOne(
                getPostCommonWrapper()
                .eq(Post::getIsDeleted, false)
                .eq(Post::getPostId, id)
                .last("LIMIT 1"));
    }

    /**
     * 类内调用须 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_BY_NAME_UNLOGINED, key=KEY_NAME)
    protected Post cacheableGetByNameUnlogined(String name) {
        return proxy().getOne(
                getPostCommonWrapper()
                .in(Post::getPostStatus,
                    PostStatusEnum.RELEASED, PostStatusEnum.HIDDEN)
                .eq(Post::getIsDeleted, false)
                .eq(Post::getPostPageName, name)
                .last("LIMIT 1"));
    }

    /**
     * 类内调用须 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_BY_NAME_LOGINED, key=KEY_NAME)
    protected Post cacheableGetByNameLogined(String name) {
        return proxy().getOne(
                getPostCommonWrapper()
                .eq(Post::getIsDeleted, false)
                .eq(Post::getPostPageName, name)
                .last("LIMIT 1"));
    }
    
    /**
     * 获取指定 Post ID 的同类型的上一条 Post
     * @param id
     * @return
     */
    public Post getPrevious(Long id) {
        return isLogin() ? 
                proxy().cacheableGetPreviousLogined(id) : 
                    proxy().cacheableGetPreviousUnlogined(id);
    }
    
    /**
     * 缓存用，类内调用需 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_PREVIOUS_LOGINED, key=KEY_PREVIOUS_ID)
    public Post cacheableGetPreviousLogined(Long previousId) {
        return proxy().getPrevious(previousId, true);
    }
    
    /**
     * 缓存用，类内调用需 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_PREVIOUS_UNLOGINED, key=KEY_PREVIOUS_ID)
    public Post cacheableGetPreviousUnlogined(Long previousId) {
        return proxy().getPrevious(previousId, false);
    }
    
    /**
     * 递归用
     * @param id
     * @param isLogin
     * @return
     */
    public Post getPrevious(Long id, Boolean isLogin) {
        Post curr = proxy().getById(id);
        Post prev = proxy().getOne(proxy().getPostPreviewWrapper(isLogin)
                .eq(Post::getType, curr.getType())
                .lt(Post::getCreateTime, curr.getCreateTime())
                .orderByDesc(Post::getCreateTime)
                .last(LIMIT_1));
        return prev;
    }

    /**
     * 获取指定 Post ID 的同类型的下一条 Post
     * @param id
     * @return
     */
    public Post getNext(Long id) {
        return isLogin() ? 
                proxy().cacheableGetNextLogined(id) : 
                    proxy().cacheableGetNextUnlogined(id);
    }
    
    /**
     * 缓存用，类内调用需 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_NEXT_LOGINED, key=KEY_NEXT_ID)
    public Post cacheableGetNextLogined(Long nextId) {
        return proxy().getNext(nextId, true, true);
    }

    /**
     * 缓存用，类内调用需 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_NEXT_UNLOGINED, key=KEY_NEXT_ID)
    public Post cacheableGetNextUnlogined(Long nextId) {
        return proxy().getNext(nextId, true, false);
    }
    
    
    /**
     * getNext 最终方法, 对 POST 类型可能敏感
     * @param id
     * @param getFirstChild
     * @param isLogin
     * @return
     */
    public Post getNext(Long id, Boolean getFirstChild, Boolean isLogin) {
        Post curr = proxy().getById(id);
        Post next = proxy().getOne(proxy().getPostPreviewWrapper(isLogin)
                .eq(Post::getType, curr.getType())
                .gt(Post::getCreateTime, curr.getCreateTime())
                .orderByAsc(Post::getCreateTime)
                .last(LIMIT_1));
        return next;
    }
    
    /**
     * 获取最新的 limit 篇文章，limit 为 null 时获取所有文章<br>
     * 当前仅限 PostTypeEnum.POST
     * @param limit
     * @return
     */
    @UpdatePosts(postViews = true, commentsCount = true)
    public List<Post> getLatestPosts(Long limit) {
        return isLogin() ? 
                proxy().cacheableGetLatestPostsLogined(limit) : 
                    proxy().cacheableGetLatestPostsUnlogined(limit);
    }


    /**
     * 获取最新的 limit 篇文章 已登录 缓存用，类内调用需 proxy()
     * @param limit
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_LATEST_POSTS_LOGINED)
    protected List<Post> cacheableGetLatestPostsLogined(Long limit) {
        return proxy().getLatestPosts(limit, true);
    }

    /**
     * 获取最新的 limit 篇文章 未登录 缓存用，类内调用需 proxy()
     * @param limit
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_LATEST_POSTS_UNLOGINED)
    protected List<Post> cacheableGetLatestPostsUnlogined(Long limit) {
        return getLatestPosts(limit, false);
    }
    
    /**
     * 获取最新的 limit 篇文章 最终方法
     * @param limit
     * @param isLogin
     * @return
     */
    List<Post> getLatestPosts(Long limit, Boolean isLogin) {
        return list(getPostPreviewWrapper(isLogin)
                .eq(Post::getType, PostTypeEnum.POST)
                .orderByDesc(Post::getCreateTime)
                .last(limit != null, LIMIT_ + limit));
    }

    /**
     * 获取指定类型的最受欢迎文章列表 公共方法
     * @param limit
     * @param isLogin
     * @return
     */
    public List<Post> getPopularPosts(Long limit, PostTypeEnum postTypeEnum) {
        return isLogin() ? 
                proxy().cacheableGetPopularPostLogined(limit, postTypeEnum) : 
                    proxy().cacheableGetPopularPostUnlogined(limit, postTypeEnum);
    }

    /**
     * 缓存用，类内调用需 proxy()
     * @param limit
     * @param postTypeEnum
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_POPULAR_POSTS_LOGINED)
    protected List<Post> cacheableGetPopularPostLogined(Long limit, PostTypeEnum postTypeEnum) {
        return proxy().getPopularPosts(limit, postTypeEnum, true);
    }

    /**
     * 缓存用，类内调用需 proxy()
     * @param limit
     * @param postTypeEnum
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_POPULAR_POSTS_UNLOGINED)
    protected List<Post> cacheableGetPopularPostUnlogined(Long limit, PostTypeEnum postTypeEnum) {
        return proxy().getPopularPosts(limit, postTypeEnum, false);
    }
    
    /**
     * 获取指定类型的最受欢迎文章列表 最终方法
     * @param limit
     * @param postTypeEnum
     * @param isLogin
     * @return
     */
    protected List<Post> getPopularPosts(Long limit, PostTypeEnum postTypeEnum, Boolean isLogin) {
        return this.selectJoinList(Post.class, new MPJLambdaWrapper<Post>(Post.class)
                .selectAll(Post.class)
                .selectFunc(FuncsEnum.COUNT_DISTINCT, Comment::getId, LambdaUtils.getName(Post::getCommentCount))
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Tag::getName, Tag::getNo, Tag::getMid), Post::getTagsName)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Tag::getMid, Tag::getNo, Tag::getMid), Post::getTagsId)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Tag::getFriendlyName, Tag::getNo, Tag::getMid), Post::getTagsFriendlyName)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Cate::getName, Cate::getNo, Cate::getMid), Post::getCategoriesName)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Cate::getMid, Cate::getNo, Cate::getMid), Post::getCategoriesId)
                .selectFunc(FuncsEnum.SELECT_META.getSql(), args -> args.accept(Cate::getFriendlyName, Cate::getNo, Cate::getMid), Post::getCategoriesFriendlyName)
                .leftJoin(Relationship.class, Relationship::getPostId, Post::getPostId)
                .leftJoin(Cate.class, "mc", on -> on
                        .eq(Cate::getMid, Relationship::getMid)
                        .eq(Cate::getType, MetaTypeEnum.CATEGORY))
                .leftJoin(Tag.class, "mt", on -> on
                        .eq(Tag::getMid, Relationship::getMid)
                        .eq(Tag::getType, MetaTypeEnum.TAG))
                .innerJoin(Comment.class, on -> on
                        .eq(Comment::getPostId, Post::getPostId)
                        .eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED)
                        .eq(Comment::getIsDeleted, false))
                .groupBy(Post::getPostId)
                .eq(Post::getIsDeleted, false)
                .and(wrapper -> wrapper
                        .eq(Post::getPostStatus, PostStatusEnum.RELEASED)
                        .or(isLogin != null && isLogin)
                        .eq(isLogin != null && isLogin, Post::getPostStatus, PostStatusEnum.PRIVATE))
                .eq(Post::getType, postTypeEnum)
                .orderByDesc(LambdaUtils.getName(Post::getCommentCount))
                .orderByDesc(Post::getCreateTime)
                .last(limit != null, LIMIT_ + limit)
                );
    }

    /**
     * 获取给定子项集合的对应父项的 postId 集合（已去重）
     * @param childrenIds 给定子项集合
     * @return 对应父项的 postId 集合（已去重）
     */
    public List<Long> getParentIds(Collection<Long> childrenIds) {
        return baseMapper.getParentIds(childrenIds);
    }

    /**
     * 前端分页条件查询，适用普通浏览、搜索、分类分页和标签分页<br>
     * 对 POST 类型不敏感
     * @param pageQueryCondition 前端分页查询条件
     * @return
     */
    @UpdatePosts(postViews = true, commentsCount = true)
    public <R> PageQuery<R> conditionPage(PageQuery<R> pageQueryCondition) {
        PageQuery<R> result = proxy().cacheableConditionPage(pageQueryCondition, isLogin());
        // result 可能是缓存的，title 又可能是和 siteConfig... 有关的
        // 获取到结果以后要把与查询无关的内容更新上去
        result.setTitle(pageQueryCondition.getTitle());
        return result;
    }

    /**
     * 前端分页条件查询，适用普通浏览、搜索、分类分页和标签分页，用于缓存<br>
     * 类内调用需 proxy(), 
     * 对 POST 类型不敏感
     * @param pageQueryCondition 前端分页查询条件
     * @param isLogin 是否登录
     * @return
     */
    @SuppressWarnings("unchecked")
    @Cacheable(value=CacheConstants.Posts.CONDITION_PAGE, key = "#pageQueryCondition.hashCode() + '.' + #isLogin")
    protected <R> PageQuery<R> cacheableConditionPage(PageQuery<R> pageQueryCondition, Boolean isLogin) {
        Cache cache = cacheManager.getCache(CacheConstants.Posts.CONDITION_PAGE);
        String cacheKey = pageQueryCondition.hashCode() + "." + isLogin;
        ValueWrapper vw = cache.get(cacheKey);
        if (vw != null && vw.get() != null) return (PageQuery<R>)vw.get();
        if (Objects.isNull(pageQueryCondition.getCurrent()) || pageQueryCondition.getCurrent() < 1L) {
            pageQueryCondition.setCurrent(1L);
        }
        
        Long size = postTypeComponent.getPostTypeConfig(pageQueryCondition.getPostType()).getPageSize();
        
        if (Objects.isNull(pageQueryCondition.getSize()) || pageQueryCondition.getSize() < 1L) {
            pageQueryCondition.setSize(Math.max(1, size));
        }
        
        String type = pageQueryCondition.getType();
        
        // 查询条件
        if (PageQuery.TAG.equals(type)) {
            Tag query = 
                metaService.getOneJoin(
                    Tag.class, 
                    metaService.getCountDefaultWrapperWithPostTypes(
                            MetaTypeEnum.TAG, isLogin, pageQueryCondition.getPostType())
                        .eq(Tag::getFriendlyName, pageQueryCondition.getFriendlyName()));
            
            if (query == null) {
                throw new PageNotFoundException();
            }
            pageQueryCondition.setQuery((R) query);
        }
        else if (PageQuery.CATEGORY.equals(type)) {
            Cate query =
                metaService.getOneJoin(
                    Cate.class, 
                    metaService.getCountDefaultWrapperWithPostTypes(
                            MetaTypeEnum.CATEGORY, isLogin, pageQueryCondition.getPostType())
                        .eq(Cate::getFriendlyName, pageQueryCondition.getFriendlyName()));
            if (query == null) {
                throw new PageNotFoundException();
            }
            pageQueryCondition.setQuery((R) query);
        }

        MPJLambdaWrapper<Post> ew = 
                pageQueryCondition.setQueryCondition(getPostCommonWrapper())
                .and(i -> i
                    .eq(Post::getPostStatus, PostStatusEnum.RELEASED)
                    .or(isLogin != null && isLogin)
                    .eq(isLogin != null && isLogin, Post::getPostStatus, PostStatusEnum.PRIVATE))
                .eq(Post::getIsDeleted, false)
                .eq(!isLogin, Post::getSearchable, true)
                .eq(Post::getType, pageQueryCondition.getPostType())
                .groupBy(Post::getPostId)
                .orderByDesc(Post::getCreateTime);
        IPage<Post> page = page(pageQueryCondition.toPage(), ew);
        PageQuery<R> result = pageQueryCondition.setResult(page);
        
        cache.put(cacheKey, result);
        return result;
    }
    
    /**
     * 更新浏览量，不走缓存驱逐
     * @param post
     * @return
     */
    public boolean updateViews(Post post) {
        Long newViews = post.getPostViews() + 1;
        cacheManager.getCache(CacheConstants.Posts.GET_POST_VIEWS)
            .put(post.getPostId(), newViews);
        
        // 获取所有 Post 类的单个缓存
        Set<Method> singlePostMethods = reflections.getMethodsAnnotatedWith(Cacheable.class);
        methodsLoop:
        for (Method method : singlePostMethods) {
            Cacheable ca = method.getAnnotation(Cacheable.class);
            String[] cacheNames = ca.value();
            String cacheKey = ca.key();
            for (String cacheName : cacheNames) {
                if (method.getReturnType() != Post.class || !cacheName.contains("PostService")) {
                    continue methodsLoop;
                }
                
                Cache cache = cacheManager.getCache(cacheName);
                Object key = null;
                
                if (KEY_ID.equals(cacheKey)) {
                    key = post.getPostId();
                }
                else if (KEY_NAME.equals(cacheKey)) {
                    key = post.getPostPageName();
                }
                else if (
                        KEY_PREVIOUS_ID.equals(cacheKey) ||
                        KEY_NEXT_ID.equals(cacheKey)) {
                    
                    ValueWrapper vw = cache.get(post.getPostId());
                    if (vw != null) {
                        Post opposite = (Post) vw.get();
                        if (opposite != null) {
                            cache = cacheManager.getCache(
                                    getOppositeCacheName(cacheName));
                            key = opposite.getPostId();
                        }
                    }
                }
                if (key != null) {
                    ValueWrapper vw = cache.get(key);
                    if (vw != null) {
                        Post ofThis = (Post) vw.get();
                        if (ofThis != null) {
                            ofThis.setPostViews(newViews);
                            cache.put(key, ofThis);
                        }
                    }
                }
            }
        }
        return super.update(new LambdaUpdateWrapper<Post>()
                .eq(Post::getPostId, post.getPostId())
                .set(Post::getPostViews, newViews));
    }
    
    /**
     * 反向缓存名称
     * @param cacheName
     * @return
     */
    private static String getOppositeCacheName(String cacheName) {
        switch(cacheName) {
        case CacheConstants.Posts.GET_PREVIOUS_LOGINED:
            return CacheConstants.Posts.GET_NEXT_LOGINED;
        case CacheConstants.Posts.GET_PREVIOUS_UNLOGINED:
            return CacheConstants.Posts.GET_NEXT_UNLOGINED;
        case CacheConstants.Posts.GET_NEXT_LOGINED:
            return CacheConstants.Posts.GET_PREVIOUS_LOGINED;
        case CacheConstants.Posts.GET_NEXT_UNLOGINED:
            return CacheConstants.Posts.GET_PREVIOUS_UNLOGINED;
        default:
            return null;
        }
    }
    
    public String getJumpToPagesUrl(Long id) {
        return proxy().cacheableJumpToPagesUrl(id, isLogin());
    }
    
    /**
     * 获取 jumpToPagesUrl 缓存用
     * 类内调用需 proxy()
     * @param id
     * @param logined
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_JUMP_TO_PAGES_URL)
    protected String cacheableJumpToPagesUrl(Long id, boolean logined) {

        Post post = proxy().getById(id);
        if (post == null) {
            throw new PageNotFoundException();
        }
        IPostTypeConfig<?> postTypeConfig = postTypeComponent.getPostTypeConfig(post.getType());
        Long pageSize = postTypeConfig.getPageSize();
        if (pageSize == null || pageSize <= 0L) {
            return post.getLink();
        }
        
        // 这里的 ew 应该和分页模型查询 ew 基本一致
        MPJLambdaWrapper<Post> ew = 
                new MPJLambdaWrapper<Post>()
                .and(i -> i
                    .eq(Post::getPostStatus, PostStatusEnum.RELEASED)
                    .or(logined)
                    .eq(logined, Post::getPostStatus, PostStatusEnum.PRIVATE))
                .eq(Post::getIsDeleted, false)
                .eq(!logined, Post::getSearchable, true)
                .eq(Post::getType, post.getType())
                .gt(Post::getCreateTime, post.getCreateTime());
        
        Long count = (long) count(ew);
        Long onPage = count / pageSize + 1;
        return String.format("%s/page/%s#post-%d",  
                postTypeConfig.getMappingPrefix(), 
                onPage, post.getPostId());
    }
    
    /**
     * 获取浏览量，类内调用需 proxy()
     * @param id
     * @return
     */
    @Cacheable(value=CacheConstants.Posts.GET_POST_VIEWS, key=KEY_ID)
    public Long getPostViews(Long id) {
        return proxy().getById(id).getPostViews();
    }

    /**
     * 重写以启用缓存驱逐，类内调用需 proxy()
     */
    @Override
    @CacheEvictTags({
        CacheEvictTags.Tag.PostId,
        CacheEvictTags.Tag.Posts,
        CacheEvictTags.Tag.PostName,
        CacheEvictTags.Tag.Comments,
        CacheEvictTags.Tag.Prev,
        CacheEvictTags.Tag.Next
    })
    public boolean update(Wrapper<Post> updateWrapper) {
        return super.update(updateWrapper);
    }

    /**
     * 重写以启用缓存驱逐，类内调用需要 proxy()
     */
    @CacheEvictTags({
        CacheEvictTags.Tag.PostId,
        CacheEvictTags.Tag.Posts,
        CacheEvictTags.Tag.PostName,
        CacheEvictTags.Tag.Comments,
        CacheEvictTags.Tag.Prev,
        CacheEvictTags.Tag.Next
    })
    public boolean saveOrUpdate(Post post) {
        return super.saveOrUpdate(post);
    }

    /**
     * 重写以启用缓存驱逐，类内调用需要 proxy()
     */
    @Override
    @CacheEvictTags({
        CacheEvictTags.Tag.PostId,
        CacheEvictTags.Tag.Posts,
        CacheEvictTags.Tag.PostName,
        CacheEvictTags.Tag.Comments,
        CacheEvictTags.Tag.Prev,
        CacheEvictTags.Tag.Next
    })
    public boolean saveOrUpdateBatch(Collection<Post> entityList) {
        return super.saveOrUpdateBatch(entityList);
    }
    
    @SuppressWarnings("unchecked")
    public S proxy() {
        return (S)AopContext.currentProxy();
    }
    
    /**
     * 存储前的通用方法, 该方法必须由 Controller 链调用, 否则获取不到 session
     * @param post
     */
    @RequireSession
    public void beforeSaving(
            Post post,
            Long[] assets,
            String[] tagsName,
            Long[] categoriesId,
            Boolean isUpdate) {
        
        PostTypeEnum postType = getPostTypeConfig().getPostType();
        if (ObjectUtils.isEmpty(post)) {
            throw new NullPointerException("Bad Request");
        }
        Post existsPost = post.getPostId() != null ? getById(post.getPostId()) : null;
        isUpdate = existsPost != null;
        
        // 检查页面重名情况
        String postPageName = post.getPostPageName();
        if (StringUtils.isNotBlank(postPageName)) {
            if (exists(new MPJLambdaWrapper<Post>()
                    .eq(Post::getPostPageName, postPageName)
                    .ne(isUpdate, Post::getPostId, post.getPostId())
                )) {
                throw RException.badRequest(
                    new StringBuilder("当前自定义页面名称\"")
                    .append(postPageName)
                    .append("\"与其他 Post 自定义名称重名").toString());
            }
        }

        // 关键词：半角逗号替代全角逗号
        if (!StringUtils.isEmpty(post.getKeywords())) {
            post.setKeywords(post.getKeywords().replace('，', ','));
        }
        
        // 设置 Post 类型
        post.setType(postType);

        if (!isUpdate) {
            post.setCreatedBy(SessionUtils.getUser(session).getUserId());
            if (post.getCreateTime() == null) {
                post.setCreateTime(LocalDateTime.now());
            }
        }
        else {
            if (existsPost.getType() != postType) {
                throw new RuntimeException("提交修改的数据类型非" + postType.getNote() + "，请到对应页面编辑");
            }
            if (post.getCreateTime() == null) {
                post.setCreateTime(existsPost.getCreateTime());
            }
        }
        
        post.setUpdateTime(LocalDateTime.now());
        if (StringUtils.isEmpty(post.getPassword())) {
            post.setPassword("");
        }
        post.setSearchable(post.getPostStatus() == PostStatusEnum.RELEASED);
        
    }
    
    /**
     * 
     * @param post 被执行存储操作的 Post 实例
     * @param assets 与当前 Post 有关的资产 ID
     * @param tagsName 与当前 Post 有关的标签名称
     * @param categoriesId 与当前 Post 有关的分类 ID
     * @param isUpdate 被执行存储操作的 Post 是否是更新
     */
    public void afterSaving(
            Post post,
            Long[] assets,
            String[] tagsName,
            Long[] categoriesId,
            Boolean isUpdate) {

        Long postId = post.getPostId();
        boolean assetNonNull = Objects.nonNull(assets) && assets.length > 0;
        if (assetNonNull) {
            LambdaUpdateWrapper<Asset> uw = new LambdaUpdateWrapper<>();
            uw.set(Asset::getFromPostId, post.getPostId())
              .in(Asset::getAssetId, (Object[])assets);
            if (!assetService.update(uw)) {
                throw RException.internalServerError("更新文件从属关系失败");
            }
        }
        boolean cateNonNull = ArrayUtils.isNotEmpty(categoriesId);
        boolean tagNonNull = ArrayUtils.isNotEmpty(tagsName);
        
        
        // 不去判断 true/false 是因为可能关系表里原本就没有 Relationship
        // 如果没有还去清除就会返回 false
        relationshipService.clearRelationshipByPostId(post.getPostId());
        
        if (cateNonNull) {
            if (!relationshipService.saveOrUpdateBatch(
                Arrays.asList(categoriesId).stream().map(cateId -> 
                    new Relationship()
                        .setMid(cateId)
                        .setPostId(postId))
                    .collect(Collectors.toList()))) {
                throw RException.internalServerError("更新分类失败");
            }
        }
        if (tagNonNull) {
            List<String> givenTagsName = Arrays.asList(tagsName);
            List<Tag> existsTags = metaService.listJoin(
                    Tag.class, new MPJLambdaWrapper<Tag>(Tag.class)
                        .selectAll(Tag.class)
                        .eq(Tag::getType, MetaTypeEnum.TAG)
                        .in(Tag::getName, givenTagsName)
                    );
            List<Meta> finalTags = givenTagsName
                    .parallelStream()
                    .filter(name -> !existsTags.parallelStream().anyMatch(i -> i.getName().equals(name)))
                    .map(name -> {
                        Meta m = new Meta();
                        m.setName(name);
                        m.setFriendlyName(name
                                .toLowerCase(Locale.ENGLISH)
                                .replace("/", "／")
                                .replace("\\", "＼")
                                );
                        m.setType(MetaTypeEnum.TAG);
                        return m;
                    })
                    .collect(Collectors.toList());
            // 先更新新增的 Tag
            if (finalTags.size() > 0) {
                
                if (metaService.exists(new LambdaQueryWrapper<Meta>()
                        .in(Meta::getFriendlyName, finalTags.stream().map(Meta::getFriendlyName).toList()))) {
                    throw RException.badRequest("本次新增标签自动生成的友好名称与已存在的标签重复，请检查或到标签管理中手动新增再使用");
                }
                
                if(!metaService.saveOrUpdateBatch(finalTags)) {
                    throw RException.internalServerError("新增标签失败");
                }
                finalTags = metaService.list(new MPJLambdaWrapper<Meta>(Meta.class)
                        .eq(Meta::getType, MetaTypeEnum.TAG)
                        .in(Meta::getName, finalTags.parallelStream().map(meta -> meta.getName()).collect(Collectors.toList()))
                    );
            }
            finalTags.addAll(existsTags);
            // 更新 Tag 的 Relationship
            if (!relationshipService.saveOrUpdateBatch(
                    finalTags.stream().map(meta -> 
                        new Relationship()
                            .setMid(meta.getMid())
                            .setPostId(postId))
                        .collect(Collectors.toList()))) {
                throw RException.internalServerError("更新文章标签失败");
            }
        }
        post = getById(postId);
    }
    
    /**
     * 渲染对应 Post 的前端显示标题
     * @param post
     * @return
     */
    public String getFrontEndPostTitle(Post post) {
        if (StringUtils.isEmpty(post.getPassword())) {
            return post.getPostTitle();
        }
        else {
            String note = post.getType().getNote();
            StringBuilder sb = new StringBuilder("加密");
            if (note.matches("^\\w")) sb.append(" ");
            sb.append(note);
            sb.append(" ");
            sb.append(post.getPostId());
            return sb.toString();
        }
    }
    
    /**
     * 详情页 Post 检查. 并返回可能的跳转路径. 该方法必须由 Controller 链调用, 否则获取不到 session
     * @param post [in/out]欲检查的文章, 其加密状态会被修改
     * @param session [in/out]session
     * @return Optional 有权限（无加密、管理员或文章密码已验证）则返回 Optional.empty(), 否则返回跳转路径
     */
    @RequireSession
    public Optional<String> detailCheckAndJump(Post post) {
        
        // 文章加密状态密码校验、清除
        boolean unlocked = PostPassJar.validFromSession(session, post);
        if (unlocked) {
            Boolean containsPostId = SessionUtils.containsPostId(session, post.getPostId());
            if (!containsPostId)
                proxy().updateViews(post);
            SessionUtils.addPostId(session, post.getPostId());
        }
        
        return Optional.empty();
        
    }
    
    
}
