package com.pingcap.community.blogserver.controller;

import com.fasterxml.jackson.annotation.JsonView;
import com.pingcap.community.blogserver.dto.CreateCommentDTO;
import com.pingcap.community.blogserver.dto.CreatePostDTO;
import com.pingcap.community.blogserver.dto.RejectPostDTO;
import com.pingcap.community.blogserver.dto.UpdatePostDTO;
import com.pingcap.community.blogserver.entity.*;
import com.pingcap.community.blogserver.enums.PostStatus;
import com.pingcap.community.blogserver.event.*;
import com.pingcap.community.blogserver.exception.comment.CommentNotFound;
import com.pingcap.community.blogserver.exception.comment.CommentRepliedNotFound;
import com.pingcap.community.blogserver.exception.post.*;
import com.pingcap.community.blogserver.exception.SessionInvalidException;
import com.pingcap.community.blogserver.qo.PostAdvancedQuery;
import com.pingcap.community.blogserver.qo.PostQuery;
import com.pingcap.community.blogserver.service.*;
import com.pingcap.community.blogserver.vo.comment.CommentItemVO;
import com.pingcap.community.blogserver.vo.post.*;
import com.pingcap.community.blogserver.vo.share.SharePostVO;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

import static com.pingcap.community.blogserver.support.security.TidbCommunityAuthenticationFilter.ATTRIBUTE_SSID;

@Tag(name = "Post API")
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/api/posts")
public class PostController {

  private final PostService postService;

  private final LikeService likeService;

  private final FavoriteService favoriteService;

  private final CommentService commentService;

  private final ApplicationEventPublisher eventPublisher;

  @PreAuthorize("permitAll()")
  @GetMapping("/latest")
  public Page<PostItemBasicVO> getLatestPosts(PostQuery query, @ParameterObject Pageable pageable) {
    return postService.getLatestPosts(query, pageable);
  }

  @PreAuthorize("hasAuthority('READ_OTHERS_POST') and hasAuthority('READ_OTHER_USERS')")
  @GetMapping("/latest/view/search")
  public Page<PostItemRawTextVO> getLatestPostsInSearchView(PostQuery query, @ParameterObject Pageable pageable) {
    return postService.getLatestRawTextPosts(query, pageable);
  }

  @PreAuthorize("hasAuthority('READ_MARKDOWN_POSTS')")
  @GetMapping("/latest/view/markdown")
  public Page<PostItemMarkdownVO> getLatestPostsInMarkdownView(PostQuery query, @ParameterObject Pageable pageable) {
    return postService.getLatestMarkdownPosts(query, pageable);
  }

  @PreAuthorize("hasAuthority('READ_HTML_POSTS')")
  @GetMapping("/latest/view/html")
  public Page<PostItemHTMLVO> getLatestPostsInHTMLView(PostQuery query, @ParameterObject Pageable pageable) {
    return postService.getLatestHTMLPosts(query, pageable);
  }

  @PreAuthorize("permitAll()")
  @GetMapping("/recommend")
  public Page<PostItemBasicVO> getRecommendPosts(PostQuery query, @ParameterObject Pageable pageable) {
    return postService.getRecommendPosts(query, pageable);
  }

  @PreAuthorize("hasAuthority('READ_OTHERS_POST')")
  @GetMapping()
  @JsonView(Post.PostItemView.class)
  public Page<Post> getPosts(PostAdvancedQuery query, @ParameterObject Pageable pageable) {
    return postService.getPosts(query, pageable);
  }

  @PreAuthorize("permitAll()")
  @GetMapping("/{id}")
  @JsonView(Post.PostDetailView.class)
  public PostDetailVO getPostDetail(
    @PathVariable("id") Long postID,
    @AuthenticationPrincipal User viewer
  ) throws PostCanNotAccess, PostNotFound {
    return postService.getPostDetailByID(viewer, postID);
  }

  @PreAuthorize("permitAll()")
  @GetMapping("/{slug}/detail")
  @JsonView(Post.PostDetailView.class)
  public PostDetailVO getPostDetailBySlug(
      @PathVariable("slug") String slug,
      @Parameter(
        description = "当 visit 为 `true` 时，获取文章详情时会记录用户的访问记录，如果用户未登录，则通过 IP 地址进行区分。"
      ) @RequestParam(required = false) Boolean visit,
      @Parameter(
        schema = @Schema(implementation = String.class)
      ) @RequestParam(name = "shareId", required = false) Share share,
      @AuthenticationPrincipal User viewer,
      @Parameter(hidden = true) HttpServletRequest request
  ) throws PostCanNotAccess, PostNotFound {
    PostDetailVO postDetailVO = postService.getPostDetailBySlug(viewer, slug);
    Post post = postDetailVO.getPost();
    if (visit != null && visit && post.isPublic()) {
      String ipAddress = request.getRemoteHost();
      String sessionID = request.getSession().getId();
      String ssid = (String) request.getAttribute(ATTRIBUTE_SSID);
      boolean visited = postService.visitPost(post, viewer, ipAddress, sessionID, ssid, share);
      if (visited) {
        postDetailVO.setVisits(postDetailVO.getVisits() + 1);
      }
    }
    return postDetailVO;
  }

  @PreAuthorize("hasAuthority('READ_OTHERS_POST') and hasAuthority('READ_OTHER_USERS')")
  @GetMapping("/{slug}/detail/view/search")
  @JsonView(Post.PostDetailWebhookView.class)
  public PostDetailVO getPostDetailBySlugInSearchView(
    @PathVariable("slug") String slug,
    @AuthenticationPrincipal User viewer
  ) throws PostCanNotAccess, PostNotFound {
    return postService.getPostDetailBySlug(viewer, slug);
  }

  @PreAuthorize("hasAuthority('CREATE_POST')")
  @PostMapping
  @JsonView(Post.PostDetailView.class)
  public Post createPost(
    @RequestBody @Valid CreatePostDTO createPostDTO,
    @AuthenticationPrincipal User creator
  ) {
    Post postAfterSave = postService.createPost(creator, createPostDTO);
    eventPublisher.publishEvent(new PostCreateEvent(this, postAfterSave, creator));
    return postAfterSave;
  }

  @PreAuthorize("hasPermission(#post, 'PostAction:SAVE')")
  @PutMapping("/{id}")
  @JsonView(Post.PostDetailView.class)
  public Post savePost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable(name = "id") Post post,
    @RequestBody @Valid UpdatePostDTO updatePostDTO,
    @AuthenticationPrincipal User operator
  ) {
    Post postAfterSave = postService.savePost(post, operator, updatePostDTO);
    eventPublisher.publishEvent(new PostSaveEvent(this, postAfterSave, operator));
    return postAfterSave;
  }

  @PreAuthorize("hasPermission(#post, 'PostAction:SUBMIT')")
  @PatchMapping("/{id}/submit")
  public void submitPost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) {
    Date submittedAt = new Date();
    postService.submitPost(post);
    Post postAfterSave = postService.getPostByID(post.getId());
    eventPublisher.publishEvent(new PostSubmitEvent(this, postAfterSave, operator, submittedAt));
  }

  @PreAuthorize("hasPermission(#post, 'PostAction:SUBMIT_CANCEL')")
  @PatchMapping("/{id}/submit/cancel")
  public void cancelSubmitPost(
      @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post
  ) {
    postService.cancelSubmitPost(post);
  }

  @PreAuthorize("hasPermission(#post, 'PostAction:PUBLISH')")
  @PatchMapping("/{id}/publish")
  public void publishPost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) {
    boolean isFirstPublish = post.getPublishedAt() == null;
    postService.publishPost(operator, post);
    Post postAfterSave = postService.getPostByID(post.getId());
    eventPublisher.publishEvent(new PostPublishEvent(this, postAfterSave, operator, isFirstPublish));
  }

  // Notice: The author and the editor with REVIEW POST permission can cancel publish the post.
  @PreAuthorize("hasPermission(#post, 'PostAction:PUBLISH_CANCEL')")
  @PatchMapping("/{id}/publish/cancel")
  public void cancelPublishPost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) {
    postService.cancelPublishPost(post);
    Post postAfterSave = postService.getPostByID(post.getId());
    eventPublisher.publishEvent(new PostPublishCancelEvent(this, postAfterSave, operator));
  }

  @PreAuthorize("hasPermission(#post, 'PostAction:REJECT')")
  @PatchMapping("/{id}/reject")
  public void rejectPost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @Valid @RequestBody(required = false) RejectPostDTO rejectPostDTO,
    @AuthenticationPrincipal User operator
  ) {
    postService.rejectPost(post, rejectPostDTO);
    Post postAfterSave = postService.getPostByID(post.getId());
    eventPublisher.publishEvent(new PostRejectEvent(this, postAfterSave, operator, new Date()));

  }

  @PreAuthorize("hasPermission(#post, 'PostAction:DELETE')")
  @DeleteMapping("/{id}")
  public void deletePost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) throws PostNotFound {
    postService.deletePost(post);
    post.setDeletedAt(new Date());
    post.setStatus(PostStatus.DELETED);
    eventPublisher.publishEvent(new PostDeleteEvent(this, post, operator));
  }

  @PreAuthorize("hasPermission(#post, 'PostAction:RECOMMEND')")
  @PatchMapping("/{id}/recommend")
  public void recommendPost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User recommender
  ) throws PostHasBeenRecommend {
    postService.recommendPost(post, recommender);
    eventPublisher.publishEvent(new PostRecommendEvent(this, post, recommender, new Date()));
  }

  @PreAuthorize("hasPermission(#post, 'PostAction:RECOMMEND')")
  @PatchMapping("/{id}/recommend/cancel")
  public void cancelRecommendPost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) throws PostHasNotBeenRecommend {
    postService.cancelRecommendPost(post);
    eventPublisher.publishEvent(new PostRecommendCancelEvent(this, post, operator, new Date()));
  }

  @PreAuthorize("isAuthenticated()")
  @PatchMapping("/{id}/like")
  public void likePost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) throws PostNotPublished, PostHasBeenLiked {
    Like like = likeService.likePost(post, operator);
    eventPublisher.publishEvent(new PostLikeEvent(this, post, operator, like.getLikedAt()));
  }

  @PreAuthorize("isAuthenticated()")
  @PatchMapping("/{id}/like/cancel")
  public void cancelLikePost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) throws PostNotPublished, PostHasNotBeenLiked {
    likeService.unlikePost(post, operator);
    eventPublisher.publishEvent(new PostLikeCancelEvent(this, post, operator, new Date()));
  }

  @PreAuthorize("isAuthenticated()")
  @PatchMapping("/{id}/favorite")
  public void favoritePost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) throws PostNotPublished, PostHasBeenFavorited {
    Favorite favorite = favoriteService.favoritePost(post, operator);
    eventPublisher.publishEvent(new PostFavoriteEvent(this, post, operator, favorite.getFavoritedAt()));
  }

  @PreAuthorize("isAuthenticated()")
  @PatchMapping("/{id}/favorite/cancel")
  public void cancelFavoritePost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator
  ) {
    favoriteService.cancelFavoritePost(post, operator);
    eventPublisher.publishEvent(new PostFavoriteCancelEvent(this, post, operator, new Date()));
  }

  @PostMapping("/{id}/share")
  public SharePostVO sharePost(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") Post post,
    @AuthenticationPrincipal User operator,
    @Parameter(hidden = true) HttpSession session
  ) throws SessionInvalidException, PostNotPublished {
    String sessionId = session.getId();
    return postService.sharePost(post, operator, sessionId);
  }

  @GetMapping("/{id}/comments")
  @JsonView(CommentItemVO.PostCommentItemVO.class)
  public Page<CommentItemVO> getPostComments(
    @PathVariable("id") Long postID, @ParameterObject Pageable pageable
  ) {
    return commentService.getPostComments(postID, pageable);
  }

  @PreAuthorize("hasAuthority('CREATE_COMMENT')")
  @PostMapping("/{id}/comments")
  public void addPostComments(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") @Valid @NotNull Post post,
    @RequestBody @Valid CreateCommentDTO createCommentDTO,
    @AuthenticationPrincipal User commenter
  ) throws PostNotPublished, CommentRepliedNotFound {
    Comment comment = commentService.addComment(post, commenter, createCommentDTO);
    eventPublisher.publishEvent(new PostCommentEvent(this, post, commenter, comment));
  }

  @PreAuthorize("hasPermission(#comment, 'DELETE')")
  @DeleteMapping("/{id}/comments/{propertyId}")
  public void removePostComment(
    @PathVariable("id") Long postID,
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("propertyId") Comment comment
  ) throws CommentNotFound {
    commentService.removeComment(comment);
  }

  @GetMapping("/{id}/events")
  public Page<PostEventItemVO> getPostEvents(
    @Parameter(schema = @Schema(implementation = Long.class)) @PathVariable("id") @Valid @NotNull Post post,
    @ParameterObject Pageable pageable,
    @AuthenticationPrincipal User viewer
  ) throws PostCanNotAccess {
    return postService.getPostEvents(post, viewer, pageable);
  }

}
