package com.pingcap.community.blogserver.service;

import com.pingcap.community.blogserver.config.AwardConfig;
import com.pingcap.community.blogserver.entity.AwardRecord;
import com.pingcap.community.blogserver.entity.Post;
import com.pingcap.community.blogserver.entity.User;
import com.pingcap.community.blogserver.enums.AwardType;
import com.pingcap.community.blogserver.repository.AwardRecordRepository;
import com.pingcap.community.blogserver.repository.PostAdvancedRepository;
import com.pingcap.community.blogserver.repository.PostRepository;
import com.pingcap.community.blogserver.support.sso.ManualAwardRequestParams;
import com.pingcap.community.blogserver.support.sso.SSOClient;
import com.pingcap.community.blogserver.util.TimeUtils;
import com.pingcap.community.blogserver.vo.job.AwardAgainResultVO;
import com.pingcap.community.blogserver.vo.job.AwardResultVO;
import com.pingcap.community.blogserver.vo.job.PostNeedAward;
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.stereotype.Service;
import org.springframework.util.StopWatch;

import javax.transaction.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Objects;

import static com.pingcap.community.blogserver.enums.AwardType.*;

@Slf4j(topic = "award-service")
@Service
@AllArgsConstructor
public class AwardService {

  private final AwardConfig config;

  private final PostRepository postRepository;

  private final PostAdvancedRepository postAdvancedRepository;

  private final AwardRecordRepository awardRecordRepository;

  private NotificationService notificationService;

  private final SSOClient ssoClient;

  private static final String POST_BASE_AWARD_REASON = "发布文章 [%s] - 基础分";
  private static final String POST_EXTRA_AWARD_REASON = "发布文章 [%s] - 加成分";
  private static final String POST_REVERT_AWARD_DELETED_REASON = "文章 [%s] 被删除";
  private static final String POST_AWARD_LIKED_REASON = "文章 [%s] 被点赞";
  private static final String POST_REVERT_AWARD_LIKE_CANCELED_REASON = "文章 [%s] 点赞被取消";
  private static final String POST_AWARD_FAVORITED_REASON = "文章 [%s] 被收藏";
  private static final String POST_REVERT_AWARD_FAVORITE_CANCELED_REASON = "文章 [%s] 收藏被取消";
  private static final String POST_AWARD_RECOMMENDED_REASON = "文章 [%s] 被管理员置顶";

  @Transactional
  public AwardResultVO addAward(
    User user, Long postID, AwardType type, String digest,
    String reason, int points, int exps
  ) {
    return addAward(user, postID, type, digest, reason, points, exps, 0);
  }

  @Transactional
  public AwardResultVO addAward(
    User user, Long postID, AwardType type,
    String digest, String reason, int points, int exps,
    int publishedPosts
  ) {
    Objects.requireNonNull(user);
    log.info(
      "Award {} points and {} exps to user(id:{}, openid:{}) for {}.",
      points, exps, user.getId(), user.getOpenid(), type
    );

    AwardRecord record = new AwardRecord();
    record.setType(type);
    record.setPostID(postID);
    record.setUserID(user.getId());
    record.setDupUsername(user.getUsername());
    record.setDupOpenID(user.getOpenid());
    record.setDigest(digest);
    record.setReason(reason);
    record.setPoints(points);
    record.setExps(exps);
    record.setAwardedAt(new Date());
    record.setPublishedPosts(publishedPosts);
    awardRecordRepository.save(record);

    ssoClient.manualAward(user.getOpenid(), new ManualAwardRequestParams(digest, reason, points, exps));
    return new AwardResultVO(record.getPoints(), record.getExps());
  }

  @Transactional
  public AwardResultVO awardPostBePublished(Post post, User author, Integer publishedPosts) {
    int points = 0;
    int exps = 0;

    if (publishedPosts > 0 && publishedPosts <= 3) {
      points = config.getPostAwardLevel1();
      exps = config.getPostAwardLevel1();
    } else if (publishedPosts >= 4 && publishedPosts <= 5) {
      points = config.getPostAwardLevel2();
      exps = config.getPostAwardLevel2();
    } else if (publishedPosts >= 6) {
      points = config.getPostAwardLevel3();
      exps = config.getPostAwardLevel3();
    }

    String reason = String.format(POST_BASE_AWARD_REASON, post.getShortTitle(35));
    return addAward(author, post.getId(), POST_PUBLISH_BASE, reason, reason, points, exps, publishedPosts);
  }

  public void awardPublishedPostsAgain() {
    int pageSize = 10;
    Pageable pageable = PageRequest.of(0, pageSize);

    while (pageable != null) {
      Page<PostNeedAward> page = postAdvancedRepository.getPostsNeedToAward(pageable);
      List<PostNeedAward> results = page.stream().toList();
      log.info("Batch extra award {} posts (page {}).", results.size(), pageable.getPageNumber());

      results.forEach(postNeedAward -> {
        try {
          AwardAgainResultVO awardAgainResultVO = extraAwardPublishedPost(postNeedAward);
          if (awardAgainResultVO != null) {
            notificationService.sendPostAwardedAgainNotice(awardAgainResultVO);
          }
        } catch (Exception e) {
          log.error("Failed to extra award post {}.", postNeedAward.getPostID(), e);
        }
      });

      if (page.hasNext()) {
        pageable = page.nextPageable();
      } else {
        pageable = null;
      }
    }
  }

  @Transactional
  public AwardAgainResultVO extraAwardPublishedPost(PostNeedAward postNeedAward) {
    long postID = postNeedAward.getPostID();

    boolean awarded = awardRecordRepository.existsByPostIDAndType(postID, POST_PUBLISH_EXTRA);
    if (awarded) {
      log.warn("Post has been awarded extra points, skip it.");
      return null;
    }

    int visits = postNeedAward.getVisits().intValue();
    int favorites = postNeedAward.getFavorites().intValue();
    int likes = postNeedAward.getLikes().intValue();

    Post post = postRepository.getById(postID);
    int extraAwards = visits + favorites * 10 + likes * 10;
    if (extraAwards > config.getMaxPostAdditionalAwards()) {
      extraAwards = config.getMaxPostAdditionalAwards();
    }

    String reason = String.format(POST_EXTRA_AWARD_REASON, post.getShortTitle(35));
    addAward(
      post.getAuthor(), post.getId(), POST_PUBLISH_EXTRA,
      reason, reason, extraAwards, extraAwards
    );

    return new AwardAgainResultVO(post, visits, favorites, likes, extraAwards, extraAwards);
  }

  @Transactional
  public void revertAwardPostBeDeleted(Post post, User author) {
    Long postID = post.getId();
    String authorOpenID = author.getOpenid();

    Objects.requireNonNull(postID);
    Objects.requireNonNull(authorOpenID);

    List<AwardRecord> records = awardRecordRepository.findAllByPostIDAndDupOpenID(postID, author.getOpenid());
    int pointsTotal = 0;
    int expsTotal = 0;
    for (AwardRecord record : records) {
      pointsTotal = pointsTotal + record.getPoints();
      expsTotal = expsTotal + record.getExps();
    }

    int revertPoints = Math.max(pointsTotal, 0);
    int revertExps = Math.max(expsTotal, 0);

    String reason = String.format(POST_REVERT_AWARD_DELETED_REASON, post.getShortTitle(35));
    addAward(author, postID, DELETE, reason, reason, -revertPoints, -revertExps);
  }

  @Transactional
  public void awardPostBeLiked(Post post, User author) {
    Long postID = post.getId();
    String authorOpenID = author.getOpenid();

    Objects.requireNonNull(postID);
    Objects.requireNonNull(authorOpenID);

    int todayAwards = getTodayAwards(postID, LIKE);
    Integer maxAwards = config.getMaxPostLikeAwardPerDay();
    if (todayAwards >= maxAwards) {
      log.error("Failed to award post be liked, because today awards {} >= max {}.", todayAwards, maxAwards);
      return;
    }

    String reason = String.format(POST_AWARD_LIKED_REASON, post.getShortTitle(35));
    addAward(
      author, postID, LIKE, reason, reason,
      config.getPostLikeAwardPoints(), config.getPostLikeAwardExps()
    );
  }

  public void revertAwardPostBeLikeCanceled(Post post, User author) {
    String reason = String.format(POST_REVERT_AWARD_LIKE_CANCELED_REASON, post.getShortTitle(35));
    addAward(
      author, post.getId(), LIKE_CANCEL, reason, reason,
      -config.getPostLikeAwardPoints(), -config.getPostLikeAwardExps()
    );
  }

  @Transactional
  public void awardPostBeFavorited(Post post, User author) {
    Long postID = post.getId();
    int todayAwards = getTodayAwards(postID, FAVORITE);

    Integer maxAwards = config.getMaxPostFavoriteAwardPerDay();
    if (todayAwards >= maxAwards) {
      log.error("Failed to award post be favorited, because today awards {} >= max {}.", todayAwards, maxAwards);
      return;
    }

    String reason = String.format(POST_AWARD_FAVORITED_REASON, post.getShortTitle(35));
    addAward(
      author, postID, FAVORITE, reason, reason,
      config.getPostFavoriteAwardPoints(), config.getPostFavoriteAwardExps()
    );
  }

  public void revertAwardPostBeFavoriteCanceled(Post post, User author) {
    String reason = String.format(POST_REVERT_AWARD_FAVORITE_CANCELED_REASON, post.getShortTitle(35));
    addAward(
      author, post.getId(), FAVORITE_CANCEL, reason, reason,
      -config.getPostFavoriteAwardPoints(), -config.getPostFavoriteAwardExps()
    );
  }

  @Transactional
  public void awardPostBeRecommended(Post post, User author) {
    Long postID = post.getId();

    Objects.requireNonNull(postID);
    Objects.requireNonNull(author.getOpenid());

    boolean hasBeenAwards = getAwardsByType(postID, RECOMMEND) > 0;
    if (hasBeenAwards) {
      log.error("Failed to award post {}, because it has been awarded for recommend.", postID);
      return;
    }

    String reason = String.format(POST_AWARD_RECOMMENDED_REASON, post.getShortTitle(30));
    addAward(
      author, postID, RECOMMEND, reason, reason,
      config.getPostRecommendAwardPoints(), config.getPostRecommendAwardExps()
    );
  }

  public int getTodayAwards(Long postID, AwardType type) {
    Date from = TimeUtils.getTodayStartTimeWithoutOffset();
    Date to = TimeUtils.getTodayEndTimeWithoutOffset();
    return awardRecordRepository.countByPostIDAndTypeAndAwardedAtBetween(postID, type, from, to);
  }

  public int getAwardsByType(Long postID, AwardType type) {
    return awardRecordRepository.countByPostIDAndType(postID, type);
  }

}
