package com.pingcap.community.blogserver.service;

import com.pingcap.community.blogserver.config.BlogUserDetailsService;
import com.pingcap.community.blogserver.dto.CreateUserAccessTokenDTO;
import com.pingcap.community.blogserver.dto.ModifyUserDTO;
import com.pingcap.community.blogserver.entity.Authority;
import com.pingcap.community.blogserver.entity.User;
import com.pingcap.community.blogserver.entity.UserAccessToken;
import com.pingcap.community.blogserver.enums.PostStatus;
import com.pingcap.community.blogserver.enums.authority.PostAuthority;
import com.pingcap.community.blogserver.exception.user.InvalidUsername;
import com.pingcap.community.blogserver.exception.user.UserNotFound;
import com.pingcap.community.blogserver.repository.*;
import com.pingcap.community.blogserver.support.security.AccessTokenConfiguration;
import com.pingcap.community.blogserver.support.sso.SSOClient;
import com.pingcap.community.blogserver.support.sso.TidbCommunitySSOResponse;
import com.pingcap.community.blogserver.support.sso.TidbCommunityUserInfo;
import com.pingcap.community.blogserver.vo.user.UserDetailVO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.web.client.RestClientException;

import javax.persistence.EntityManager;
import java.util.*;

@Slf4j(topic = "user-service")
@Service
@AllArgsConstructor
public class UserService {

  private final UserRepository userRepository;

  private final UserAccessTokenRepository userAccessTokenRepository;

  private final AuthorityRepository authorityRepository;

  private final PostRepository postRepository;

  private final LikeRepository likeRepository;

  private final FavoriteRepository favoriteRepository;

  private final CommentRepository commentRepository;

  private final EntityManager em;

  private final AccessTokenConfiguration accessTokenConfiguration;

  private final BlogUserDetailsService blogUserDetailsService;

  private final SSOClient ssoClient;

  public UserDetailVO getUserDetail(User user, User viewer) {
    return wrapperUserDetailVO(user, viewer);
  }

  public UserDetailVO getUserDetailByUsername(String username, User viewer) throws UserNotFound {
    User user = getUserByUsername(username);
    return wrapperUserDetailVO(user, viewer);
  }

  public User getUserByUsername(String username) throws InvalidUsername, UserNotFound {
    if (username == null || username.isEmpty()) {
      throw new InvalidUsername();
    }

    Optional<User> userByUsername = userRepository.findByUsername(username);
    if (userByUsername.isPresent()) {
      return userByUsername.get();
    }

    // Try to get user from SSO.
    Optional<TidbCommunityUserInfo> userFromSSO = getUserFromSSOByUsername(username);
    if (userFromSSO.isEmpty()) {
      throw new UserNotFound();
    }

    Optional<User> userByOpenID = userRepository.findByOpenid(userFromSSO.get().getOpenid());
    if (userByOpenID.isPresent()) {
      // The username of user maybe changed, we should update the info.
      return blogUserDetailsService.updateUser(userByOpenID.get(), userFromSSO.get());
    } else {
      // The user has not been registered.
      return blogUserDetailsService.createUser(userFromSSO.get());
    }
  }

  public Optional<TidbCommunityUserInfo> getUserFromSSOByUsername(String username) {
    try {
      if (username == null || username.isEmpty()) {
        return Optional.empty();
      }

      return Optional.ofNullable(ssoClient.getUserInfoByUsername(username)).map(TidbCommunitySSOResponse::getData);
    } catch (RestClientException e) {
      log.error("Failed to get sso userinfo by username {}", username);
      return Optional.empty();
    }
  }

  public UserDetailVO getUserDetailByID(Long userID) {
    Optional<User> userOptional = userRepository.findById(userID);
    if (userOptional.isEmpty()) {
      throw new UserNotFound();
    }
    User user = userOptional.get();
    resolveAuthorities(user);

    return wrapperUserDetailVO(user, null);
  }

  private void resolveAuthorities(User user) {
    em.detach(user);
    Set<Authority> authorities =
        new HashSet<>(Optional.ofNullable(user.getAuthorities()).orElse(Collections.emptySet()));
    authorities.addAll(authorityRepository.findByRolesIn(user.getRoles()));
    user.setAuthorities(authorities);
  }

  private UserDetailVO wrapperUserDetailVO(User user, User viewer) {
    UserDetailVO detailVO = new UserDetailVO();
    detailVO.setUser(user);

    long posts;
    boolean isAuthor = user.equals(viewer);
    boolean canReadOthersPost = viewer != null && viewer.can(PostAuthority.READ_OTHERS_POST);
    if (isAuthor || canReadOthersPost) {
      posts = postRepository.countByAuthor(user);
    } else {
      posts = postRepository.countByStatusAndAuthor(PostStatus.PUBLISHED, user);
    }

    Long comments = commentRepository.countByCommenterAndDeletedAtIsNull(user);
    Long likes = likeRepository.countByUser(user);
    Long favorites = favoriteRepository.countByUser(user);

    detailVO.setPosts(posts);
    detailVO.setComments(comments);
    detailVO.setLikes(likes);
    detailVO.setFavorites(favorites);

    return detailVO;
  }

  public Page<User> getUsers(Pageable pageable) {
    return null;
  }

  public User createUser(User user) {
    user.setId(null);
    return userRepository.save(user);
  }

  public String createUserAccessToken(
      User user, CreateUserAccessTokenDTO createUserAccessTokenDTO) {
    String label = "new-access-token";
    if (createUserAccessTokenDTO.getLabel() != null) {
      label = createUserAccessTokenDTO.getLabel();
    }

    String salt = Optional.ofNullable(accessTokenConfiguration.getSalt()).orElse("salt");
    String newAccessToken = UUID.randomUUID().toString();
    String encodedAccessToken =
        DigestUtils.md5DigestAsHex(String.format("%s-%s", newAccessToken, salt).getBytes());

    UserAccessToken userAccessToken = new UserAccessToken();
    userAccessToken.setUser(user);
    userAccessToken.setLabel(label);
    userAccessToken.setAccessToken(encodedAccessToken);

    userAccessTokenRepository.save(userAccessToken);
    return newAccessToken;
  }

  public User modifyUser(User user, ModifyUserDTO modifyUserDTO) {
    user.setAuthorities(modifyUserDTO.getAuthorities());
    user.setRoles(modifyUserDTO.getRoles());
    user.setEnable(modifyUserDTO.getEnable());
    return userRepository.save(user);
  }

  public void deleteUser(User user) {
    userRepository.delete(user);
  }
}
