package vn.azteam.tpf.web.rest;

import com.google.common.collect.Lists;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.web.multipart.MultipartFile;
import vn.azteam.tpf.config.Constants;
import vn.azteam.tpf.domain.TBDetailUser;
import vn.azteam.tpf.domain.User;
import vn.azteam.tpf.repository.UserRepository;
import vn.azteam.tpf.repository.search.UserSearchRepository;
import vn.azteam.tpf.security.AuthoritiesConstants;
import vn.azteam.tpf.service.*;
import vn.azteam.tpf.service.dto.TBRoleDTO;
import vn.azteam.tpf.service.dto.UserDTO;
import vn.azteam.tpf.service.mapper.UserMapper;
import vn.azteam.tpf.service.util.UserRoleUtil;
import vn.azteam.tpf.web.rest.errors.*;
import vn.azteam.tpf.web.rest.util.HeaderUtil;
import vn.azteam.tpf.web.rest.util.PaginationUtil;
import com.codahale.metrics.annotation.Timed;
import io.github.jhipster.web.util.ResponseUtil;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * REST controller for managing users.
 * <p>
 * This class accesses the User entity, and needs to fetch its collection of authorities.
 * <p>
 * For a normal use-case, it would be better to have an eager relationship between User and Authority,
 * and send everything to the client side: there would be no View Model and DTO, a lot less code, and an outer-join
 * which would be good for performance.
 * <p>
 * We use a View Model and a DTO for 3 reasons:
 * <ul>
 * <li>We want to keep a lazy association between the user and the authorities, because people will
 * quite often do relationships with the user, and we don't want them to get the authorities all
 * the time for nothing (for performance reasons). This is the #1 goal: we should not impact our users'
 * application because of this use-case.</li>
 * <li> Not having an outer join causes n+1 requests to the database. This is not a real issue as
 * we have by default a second-level cache. This means on the first HTTP call we do the n+1 requests,
 * but then all authorities come from the cache, so in fact it's much better than doing an outer join
 * (which will get lots of data from the database, for each HTTP call).</li>
 * <li> As this manages users, for security reasons, we'd rather have a DTO layer.</li>
 * </ul>
 * <p>
 * Another option would be to have a specific JPA entity graph to handle this case.
 */
@RestController
@RequestMapping("/api")
public class UserResource {
    private static final String ENTITY_NAME = "User";

    private final Logger log = LoggerFactory.getLogger(UserResource.class);

    private final UserService userService;

    private final TBDetailUserQueryService tbDetailUserQueryService;

    private final UserRepository userRepository;

    private final MailService mailService;

    private final UserSearchRepository userSearchRepository;

    private final UserRoleUtil userRoleUtil;

    private final TBEquipmentOfCustomerQueryService tBEquipmentOfCustomerQueryService;

    private final UserMapper userMapper;

    private final TBRoleService tbRoleService;

    private final ElasticsearchOperations elasticsearchTemplate;

    public UserResource(@Lazy UserService userService,
                        UserRepository userRepository,
                        TBDetailUserQueryService tbDetailUserQueryService,
                        MailService mailService,
                        TBEquipmentOfCustomerQueryService tBEquipmentOfCustomerQueryService,
                        UserSearchRepository userSearchRepository,
                        UserRoleUtil userRoleUtil,
                        UserMapper userMapper,
                        TBRoleService tbRoleService, ElasticsearchOperations elasticsearchTemplate) {

        this.userService = userService;
        this.tbDetailUserQueryService = tbDetailUserQueryService;
        this.userRepository = userRepository;
        this.mailService = mailService;
        this.tBEquipmentOfCustomerQueryService = tBEquipmentOfCustomerQueryService;
        this.userSearchRepository = userSearchRepository;
        this.userRoleUtil = userRoleUtil;
        this.userMapper = userMapper;
        this.tbRoleService = tbRoleService;
        this.elasticsearchTemplate = elasticsearchTemplate;
    }

    /**
     * POST  /users  : Creates a new user.
     * <p>
     * Creates a new user if the login and email are not already used, and sends an
     * mail with an activation link.
     * The user needs to be activated on creation.
     *
     * @param userDTO the user to create
     * @return the ResponseEntity with status 201 (Created) and with body the new user, or with status 400 (Bad Request) if the login or email is already in use
     * @throws URISyntaxException if the Location URI syntax is incorrect
     * @throws BadRequestAlertException 400 (Bad Request) if the login or email is already in use
     */
    @PostMapping("/users")
    @Timed
    public ResponseEntity<User> createUser(@Valid @RequestBody UserDTO userDTO) throws URISyntaxException {
        log.debug("REST request to save User : {}", userDTO);

        if (userDTO.getId() != null) {
            throw new BadRequestAlertException("1005", "userManagement", "1005");
            // Lowercase the user login before comparing with database
        } else if (userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()).isPresent()) {
            throw new LoginAlreadyUsedException();
        } else if (userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()).isPresent()) {
            throw new EmailAlreadyUsedException();
        } else if(tbDetailUserQueryService.findOneByPhone(userDTO.getPhone()).isPresent()) {
            throw new PhoneAlreadyUsedException();
        } else {
            User newUser = userService.createUser(userDTO);
            mailService.sendCreationEmail(newUser);
            return ResponseEntity.created(new URI("/api/users/" + newUser.getLogin()))
                .headers(HeaderUtil.createAlert( "userManagement.created", newUser.getLogin()))
                .body(newUser);
        }
    }

    /**
     * PUT /users : Updates an existing User.
     *
     * @param userDTO the user to update
     * @return the ResponseEntity with status 200 (OK) and with body the updated user
     * @throws EmailAlreadyUsedException 400 (Bad Request) if the email is already in use
     * @throws LoginAlreadyUsedException 400 (Bad Request) if the login is already in use
     */
    @PutMapping("/users")
    @Timed
    public ResponseEntity<UserDTO> updateUser(@Valid @RequestBody UserDTO userDTO) {
        log.debug("REST request to update User : {}", userDTO);
        Optional<User> existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail());
        if (existingUser.isPresent() && (!existingUser.get().getId().equals(userDTO.getId()))) {
            throw new EmailAlreadyUsedException();
        }
        existingUser = userRepository.findOneByLogin(userDTO.getLogin().toLowerCase());
        if (existingUser.isPresent() && (!existingUser.get().getId().equals(userDTO.getId()))) {
            throw new LoginAlreadyUsedException();
        }
        Optional<TBDetailUser> existingTBDetailUser = tbDetailUserQueryService.findOneByPhone(userDTO.getPhone());
        if(existingTBDetailUser.isPresent() && (!existingTBDetailUser.get().getUser().getId().equals(userDTO.getId()))){
            throw new PhoneAlreadyUsedException();
        }
        Optional<UserDTO> updatedUser = userService.updateUser(userDTO);

        return ResponseUtil.wrapOrNotFound(updatedUser,
            HeaderUtil.createAlert("userManagement.updated", userDTO.getLogin()));
    }

    /**
     * PUT /users : Updates an existing User.
     *
     * @param userDTO the user to update
     * @return the ResponseEntity with status 200 (OK) and with body the updated user
     * @throws EmailAlreadyUsedException 400 (Bad Request) if the email is already in use
     * @throws LoginAlreadyUsedException 400 (Bad Request) if the login is already in use
     */
    @PutMapping("/usersNonAlert")
    @Timed
    public ResponseEntity<UserDTO> updateUserNonAlert(@Valid @RequestBody UserDTO userDTO) {
        log.debug("REST request to update User : {}", userDTO);
        Optional<User> existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail());
        if (existingUser.isPresent() && (!existingUser.get().getId().equals(userDTO.getId()))) {
            throw new EmailAlreadyUsedException();
        }
        existingUser = userRepository.findOneByLogin(userDTO.getLogin().toLowerCase());
        if (existingUser.isPresent() && (!existingUser.get().getId().equals(userDTO.getId()))) {
            throw new LoginAlreadyUsedException();
        }
        Optional<TBDetailUser> existingTBDetailUser = tbDetailUserQueryService.findOneByPhone(userDTO.getPhone());
        if(existingTBDetailUser.isPresent() && (!existingTBDetailUser.get().getUser().getId().equals(userDTO.getId()))){
            throw new PhoneAlreadyUsedException();
        }
        Optional<UserDTO> updatedUser = userService.updateUser(userDTO);

        return new ResponseEntity<>(updatedUser.get(), null, HttpStatus.OK);
    }

    /**
     * PUT /users : Updates an existing User.
     *
     * @param userDTO the user to update
     * @return the ResponseEntity with status 200 (OK) and with body the updated user
     * @throws EmailAlreadyUsedException 400 (Bad Request) if the email is already in use
     * @throws LoginAlreadyUsedException 400 (Bad Request) if the login is already in use
     */
   @PutMapping("/update-my-profile")
    @Timed
    public ResponseEntity<UserDTO> updateMyProfile(@Valid @RequestBody UserDTO userDTO) {
        return updateUser(userDTO);
    }

    /**F
     * GET /users : get all users.
     *
     * @param pageable the pagination information
     * @return the ResponseEntity with status 200 (OK) and with body all users
     */
    @GetMapping("/users")
    @Timed
    public ResponseEntity<List<UserDTO>> getAllUsers(Pageable pageable) {
        UserDTO currentUser = userService.getCurrentUserDTO().get();
        Page<UserDTO> page = userService.getAllManagedUsers(pageable);
        List<UserDTO> result = page.getContent();
        if(currentUser.getCustomerId() != null){
            result = result.stream()
                .filter(item -> item.getCustomerId() != null
                    && item.getCustomerId().equals(currentUser.getCustomerId()))
                .collect(Collectors.toList());
        }
        page = changeUserDTOToPageFromList(result,pageable);
        HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/users");
        return new ResponseEntity<>(result, headers, HttpStatus.OK);
    }

    private Page<UserDTO> changeUserDTOToPageFromList(List<UserDTO> userDTOS, Pageable pageable){
        int start = Math.toIntExact(pageable.getOffset());
        int end = Math.toIntExact((start + pageable.getPageSize()) > userDTOS.size() ? userDTOS.size() : (start + pageable.getPageSize()));
        if(userDTOS.size() > start) {
            return new PageImpl<>(userDTOS.subList(start, end), pageable, userDTOS.size());
        }
        return new PageImpl<>(Lists.newArrayList(), pageable, userDTOS.size());
    }

    /**
     * @return a string list of the all of the roles
     */
    @GetMapping("/users/authorities")
    @Timed
    @PreAuthorize("hasRole(\"" + AuthoritiesConstants.ADMIN + "\")")
    public List<String> getAuthorities() {
        return userService.getAuthorities();
    }

    /**
     * GET /users/:login : get the "login" user.
     *
     * @param login the login of the user to find
     * @return the ResponseEntity with status 200 (OK) and with body the "login" user, or with status 404 (Not Found)
     */
    @GetMapping("/users/{login:" + Constants.LOGIN_REGEX + "}")
    @Timed
    public ResponseEntity<UserDTO> getUser(@PathVariable String login) {
        log.debug("REST request to get User : {}", login);
        return ResponseUtil.wrapOrNotFound(
            userService.getUserWithAuthoritiesByLogin(login)
                .map(UserDTO::new));
    }


    /**
     * GET /usersbyid/:id : get the "id" user.
     *
     * @param id the id of the user to find
     * @return the ResponseEntity with status 200 (OK) and with body the "login" user, or with status 404 (Not Found)
     */
    @GetMapping("/usersbyid/{id}")
    @Timed
    public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
        log.debug("REST request to get User : {}", id);
        UserDTO currentUser = userService.getCurrentUserDTO().get();
        Optional<UserDTO> userSelected = userService.getUserWithAuthorities(id).map(UserDTO::new);
        if(currentUser.getCustomerId() != null && !currentUser.getCustomerId().equals(userSelected.get().getCustomerId())) {
            userSelected = Optional.empty();
        }
        return ResponseUtil.wrapOrNotFound(userSelected);
    }

    /**
     * DELETE /users/:login : delete the "login" User.
     *
     * @param login the login of the user to delete
     * @return the ResponseEntity with status 200 (OK)
     */
    @DeleteMapping("/users/{login:" + Constants.LOGIN_REGEX + "}")
    @Timed
    public ResponseEntity<Void> deleteUser(@PathVariable String login) {
        log.debug("REST request to delete User: {}", login);
        UserDTO currentUser = userService.getCurrentUserDTO().get();
        UserDTO deleteUser = userService.getUserWithAuthoritiesByLogin(login).map(UserDTO::new).get();
        if(currentUser.getCustomerId() != null && !currentUser.getCustomerId().equals(deleteUser.getCustomerId())) {
            throw new BadRequestAlertException("1019", ENTITY_NAME, "1019");
        }
        userService.deleteUser(login);
        return ResponseEntity.ok().headers(HeaderUtil.createAlert( "userManagement.deleted", login)).build();
    }

    /**
     * SEARCH /_search/users/:query : search for the User corresponding
     * to the query.
     *
     * @param query the query to search
     * @param pageable
     * @return the result of the search
     */
    @GetMapping("/_search/users")
    @Timed
    public ResponseEntity<List<UserDTO>> searchUser(@RequestParam(required = false) String query, Pageable pageable) {
        UserDTO currentUser = userService.getCurrentUserDTO().get();
        log.debug("REST request to search for a page of Users for query {}", query);
        Page<UserDTO> page;
        if(query != null && !query.isEmpty()) {
            page = userService.searchUser(query, pageable);
        } else {
            page = userService.getAllManagedUsers(pageable);
        }
        List<UserDTO> result = page.getContent();
        if(currentUser.getCustomerId() != null){
            result = result.stream()
                .filter(item -> item.getCustomerId() != null && item.getCustomerId() == currentUser.getCustomerId())
                .collect(Collectors.toList());
            page = changeUserDTOToPageFromList(result,pageable);
        }
        HttpHeaders headers = PaginationUtil.generateSearchPaginationHttpHeaders(query, page, "/api/_search/users");
        return ResponseEntity.ok().headers(headers).body(page.getContent());
    }

    /**
     * Upload images
     * @param userId
     * @param images
     * @return
     * @throws URISyntaxException
     */
    @PostMapping("/users/upload-avatar/{userId}")
    @Timed
    public ResponseEntity<String> uploadAvatar(@PathVariable Long userId,
                                               @RequestParam(value="images") final MultipartFile[] images)
        throws URISyntaxException {
        log.debug("REST request to upload avatar user : {}", userId);

        String result = userService.uploadImages(userId, images);
        return ResponseEntity.ok()
            .headers(HeaderUtil.createAlert("upload picture profile", userId.toString()))
            .body(result);
    }

    /**
     * PUT /users : Updates an existing User.
     *
     * @param fcmToken the user to update
     * @return the ResponseEntity with status 200 (OK) and with body the updated user
     * @throws EmailAlreadyUsedException 400 (Bad Request) if the email is already in use
     * @throws LoginAlreadyUsedException 400 (Bad Request) if the login is already in use
     */
    @PutMapping("/update-fcmToken")
    @Timed
    public ResponseEntity<UserDTO> updateFcmToken(@Valid @RequestBody String fcmToken) {
        UserDTO currentUserDTO = userService.getUserWithAuthorities()
            .map(UserDTO::new)
            .orElseThrow(() -> new InternalServerErrorException("1006"));
        if(currentUserDTO.getFcmToken() == null || currentUserDTO.getFcmToken() == "") {
            currentUserDTO.setFcmToken(fcmToken);
        } else if (currentUserDTO.getFcmToken().contains(fcmToken)) {
            currentUserDTO.setFcmToken(currentUserDTO.getFcmToken());
        } else {
            currentUserDTO.setFcmToken(currentUserDTO.getFcmToken() + vn.azteam.tpf.Constants.Constants.COMMA_DOT + fcmToken);
        }
        return updateUserNonAlert(currentUserDTO);
    }

    /**
     * PUT /users : Updates an existing User.
     *
     * @param fcmToken the user to update
     * @return the ResponseEntity with status 200 (OK) and with body the updated user
     * @throws EmailAlreadyUsedException 400 (Bad Request) if the email is already in use
     * @throws LoginAlreadyUsedException 400 (Bad Request) if the login is already in use
     */
    @PutMapping("/delete-fcmToken")
    @Timed
    public ResponseEntity<UserDTO> deleteFcmToken(@Valid @RequestBody String fcmToken) {
        UserDTO currentUserDTO = userService.getUserWithAuthorities()
            .map(UserDTO::new)
            .orElseThrow(() -> new InternalServerErrorException("1006"));
        if(currentUserDTO.getFcmToken() != null && !currentUserDTO.getFcmToken().isEmpty()
            && currentUserDTO.getFcmToken().contains(fcmToken)) {
            if(currentUserDTO.getFcmToken().contains(vn.azteam.tpf.Constants.Constants.COMMA_DOT)) {
                String remainingFcmToken = currentUserDTO.getFcmToken().replace(vn.azteam.tpf.Constants.Constants.COMMA_DOT + fcmToken,vn.azteam.tpf.Constants.Constants.EMPTY);
                currentUserDTO.setFcmToken(remainingFcmToken);
            } else {
                currentUserDTO.setFcmToken(null);
            }
        }
        return updateUserNonAlert(currentUserDTO);
    }

}
