package com.pingcap.community.blogserver.service;

import com.pingcap.community.blogserver.config.BlogConfig;
import com.pingcap.community.blogserver.entity.Post;
import com.pingcap.community.blogserver.entity.PostInfo;
import com.pingcap.community.blogserver.entity.User;
import com.pingcap.community.blogserver.repository.PostAdvancedRepository;
import com.pingcap.community.blogserver.repository.PostInfoRepository;
import com.pingcap.community.blogserver.repository.PostRepository;
import com.pingcap.community.blogserver.support.remark.TiRemark;
import com.pingcap.community.blogserver.util.JsonUtils;
import com.pingcap.community.blogserver.util.PostContentProcessor;
import com.pingcap.community.blogserver.vo.job.PostStatistics;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.stereotype.Service;

import javax.persistence.LockModeType;
import javax.transaction.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Optional;

@Slf4j(topic = "post-info-service")
@AllArgsConstructor
@Service
public class PostInfoService {

    private final TiRemark tiRemark;

    private final BlogConfig blogConfig;

    private final PostRepository postRepository;

    private final PostInfoRepository postInfoRepository;

    private final PostAdvancedRepository postAdvancedRepository;

    private final PostContentProcessor postContentProcessor;

    // Generating other formats for new posts.
    @Transactional
    public void batchGeneratePostRawText() {
        // Batch refresh post raw text.
        List<Post> postsNeedRefreshRawText = postRepository.findPostsNeededToGenerateRawText();
        for (Post post : postsNeedRefreshRawText) {
            refreshPostRawText(post);
        }
    }

    public void refreshPostRawText(Post post) {
        Optional<PostInfo> postInfoOptional = generatePostRawText(post);
        if (postInfoOptional.isPresent()) {
            PostInfo postInfo = postInfoOptional.get();
            postInfoRepository.updatePostRawText(postInfo.getRawText(), postInfo.getRawTextVersion(), postInfo.getPostID());
        }
    }

    public Optional<PostInfo> generatePostRawText(Post post) {
        if (post == null) {
            return Optional.empty();
        }

        Long postID = post.getId();
        Long postVersion = post.getVersion();
        String content = post.getContent();

        log.info("Generating raw text for post#{}.", postID);

        // Skip the out-of-date raw text.
        if (post.getRawTextVersion() != null && post.getVersion() < post.getRawTextVersion()) {
            log.info("Skipping because the content of post#{} is expired.", postID);
            return Optional.empty();
        }

        // Skip the invalid JSON.
        if (content.isEmpty() || !JsonUtils.isValidJSON(content)) {
            log.warn("Skipping because the content of post#{} is not a valid json.", postID);
            return Optional.empty();
        }

        try {
            String rawText = tiRemark.fragmentToText(content);
            if (rawText != null && !rawText.isEmpty()) {
                PostInfo newPostInfo = new PostInfo();
                rawText = postContentProcessor.cleanFragmentFlags(rawText);
                newPostInfo.setPostID(postID);
                newPostInfo.setRawText(rawText);
                newPostInfo.setRawTextVersion(postVersion);
                log.info("Generate raw text for post#{} successfully.", postID);
                return Optional.of(newPostInfo);
            }
        } catch (Exception e) {
            log.warn("Failed to generate raw text for post#{}: {}", postID, e.getMessage());
        }

        return Optional.empty();
    }

    // Batch refresh post Markdown text.
    @Transactional
    public void batchGeneratePostMarkdownText() {
        List<Post> postsNeedRefreshMarkdownText = postRepository.findPostsNeededToGenerateMarkdownText();
        for (Post post : postsNeedRefreshMarkdownText) {
            refreshPostMarkdownText(post);
        }
    }

    public void refreshPostMarkdownText(Post post) {
        Optional<PostInfo> postInfoOptional = generatePostMarkdownText(post);
        if (postInfoOptional.isPresent()) {
            PostInfo postInfo = postInfoOptional.get();
            postInfoRepository.updatePostMarkdownText(postInfo.getMarkdownText(), postInfo.getMarkdownTextVersion(), postInfo.getPostID());
        }
    }

    public Optional<PostInfo> generatePostMarkdownText(Post post) {
        if (post == null) {
            return Optional.empty();
        }

        Long postID = post.getId();
        Long postVersion = post.getVersion();
        String content = post.getContent();
        String slug = post.getSlug();
        String author = Optional.of(post).map(Post::getAuthor).map(User::getUsername).orElse("");

        log.info("Generating markdown text for post#{}.", postID);

        // Skip the out-of-date raw text.
        if (post.getMarkdownTextVersion() != null && post.getVersion() < post.getMarkdownTextVersion()) {
            log.info("Skipping because the content of post#{} is expired.", postID);
            return Optional.empty();
        }

        // Skip the invalid JSON.
        if (content.isEmpty() || !JsonUtils.isValidJSON(content)) {
            log.warn("Skipping because the content of post#{} is not a valid json.", postID);
            return Optional.empty();
        }

        try {
            String markdownText = tiRemark.fragmentToMarkdown(content);
            if (markdownText != null && !markdownText.isEmpty()) {
                PostInfo newPostInfo = new PostInfo();
                markdownText = postContentProcessor.wrapMarkdownWithHeader(slug, author, markdownText);
                newPostInfo.setPostID(postID);
                newPostInfo.setMarkdownText(markdownText);
                newPostInfo.setMarkdownTextVersion(postVersion);
                log.info("Generate markdown text for post#{} successfully.", postID);
                return Optional.of(newPostInfo);
            }
        } catch (Exception e) {
            log.warn("Failed to generate markdown text for post#{}: {}", postID, e.getMessage());
        }

        return Optional.empty();
    }

    // Batch refresh post HTML text.
    @Transactional
    public void batchGeneratePostHTMLText() {
        List<Post> postsNeedRefreshHTMLText = postRepository.findPostsNeededToGenerateHTMLText();
        for (Post post : postsNeedRefreshHTMLText) {
            refreshPostHTMLText(post);
        }
    }

    public void refreshPostHTMLText(Post post) {
        Optional<PostInfo> postInfoOptional = generatePostHTMLText(post);
        if (postInfoOptional.isPresent()) {
            PostInfo postInfo = postInfoOptional.get();
            postInfoRepository.updatePostHTMLText(postInfo.getHtmlText(), postInfo.getHtmlTextVersion(), postInfo.getPostID());
        }
    }

    public Optional<PostInfo> generatePostHTMLText(Post post) {
        if (post == null) {
            return Optional.empty();
        }

        Long postID = post.getId();
        Long postVersion = post.getVersion();
        String content = post.getContent();
        String slug = post.getSlug();
        String author = Optional.of(post).map(Post::getAuthor).map(User::getUsername).orElse("");

        log.info("Generating HTML text for post#{}.", postID);

        // Skip the out-of-date raw text.
        if (post.getHtmlTextVersion() != null && post.getVersion() < post.getHtmlTextVersion()) {
            log.info("Skipping because the content of post#{} is expired.", postID);
            return Optional.empty();
        }

        // Skip the invalid JSON.
        if (content.isEmpty() || !JsonUtils.isValidJSON(content)) {
            log.warn("Skipping because the content of post#{} is not a valid json.", postID);
            return Optional.empty();
        }

        try {
            String htmlText = tiRemark.fragmentToHTML(content);
            if (htmlText != null && !htmlText.isEmpty()) {
                PostInfo newPostInfo = new PostInfo();
                htmlText = postContentProcessor.wrapHTMLWithHeader(slug, author, htmlText);
                newPostInfo.setPostID(postID);
                newPostInfo.setHtmlText(htmlText);
                newPostInfo.setHtmlTextVersion(postVersion);
                log.info("Generate HTML text for post#{} successfully.", postID);
                return Optional.of(newPostInfo);
            }
        } catch (Exception e) {
            log.warn("Failed to generate HTML text for post#{}: {}", postID, e);
        }

        return Optional.empty();
    }

    // Calc post scores.
    @Transactional
    public int calcPostsScore() {
        int page = 0;
        int pageSize = 100;
        int count = 0;

        while (true) {
            Pageable pageable = PageRequest.of(page, pageSize);
            Page<PostStatistics> postPage = postAdvancedRepository.getPublishedPostStatistics(pageable);
            for (PostStatistics postStatistics : postPage.getContent()) {
                calcPostScore(postStatistics);
            }
            count += postPage.getNumberOfElements();
            if (postPage.hasNext()) {
                page++;
            } else {
                break;
            }
        }

        return count;
    }

    @Lock(LockModeType.PESSIMISTIC_READ)
    public void calcPostScore(PostStatistics statistics) {
        PostInfo postInfo = new PostInfo();
        postInfo.setPostID(statistics.getPostID());

        long likes = statistics.getLikes();
        long comments = statistics.getComments();
        long visits = statistics.getVisits();
        long timestamp = Optional.ofNullable(statistics.getPublishedAt()).map(Date::getTime).orElse(0L) / 1000;

        int likesWeight = blogConfig.getPostLikesWeight();
        int commentsWeight = blogConfig.getPostCommentsWeight();
        int visitsWeight = blogConfig.getPostVisitsWeight();

        postInfo.setScore(timestamp + likesWeight * likes + commentsWeight * comments + visitsWeight * visits);
        postInfoRepository.updatePostScore(postInfo.getPostID(), postInfo.getScore());
    }

}
