package org.ros.chatto.service;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.ros.chatto.dto.ActiveUserDTO;
import org.ros.chatto.dto.UserRegistrationDTO;
import org.ros.chatto.logged.TokenCacheUtil;
import org.ros.chatto.model.ChatUser;
import org.ros.chatto.model.Role;
import org.ros.chatto.model.UserRole;
import org.ros.chatto.model.UserSession;
import org.ros.chatto.repository.RoleRepository;
import org.ros.chatto.repository.UserRepository;
import org.ros.chatto.repository.UserRoleRepository;
import org.ros.chatto.repository.UserSessionRepository;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Transactional
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
	private final UserRepository userRepository;

	private final UserRoleRepository userRoleRepository;

	private final PasswordEncoder passwordEncoder;

	private final RoleRepository roleRepository;

	private final UserSessionRepository userSessionRepository;

	private final UserTokenService userTokenService;

	@Override
	public ChatUser createUser(final UserRegistrationDTO userRegistrationDTO) {
		final ChatUser user = new ChatUser();
		user.setUserName(userRegistrationDTO.getUserName());
		user.setPassword(
				passwordEncoder.encode(userRegistrationDTO.getPassword()));
		final ChatUser changedUser = userRepository.save(user);
		final UserRole userRole = new UserRole();
		final Role role = roleRepository.findByName("USER");
		userRole.setRole(role);
		userRole.setUser(changedUser);
		return userRoleRepository.save(userRole).getUser();
	}

	@Override
	@Transactional(readOnly = true)
	public List<String> findAllOtherUsers(final String userName) {
		return userRepository.findAllOtherUserNames(userName);
	}

	@Transactional(readOnly = true)
	@Override
	public List<String> getAllRegularUsers() {
		return userRoleRepository.getAllRegularUser();
	}

	@Transactional(readOnly = true)
	public List<ActiveUserDTO> getOtherActiveUsers(final String userName) {
		final List<String> userList = findAllOtherUsers(userName);

		final List<UserSession> userSessionsList = userSessionRepository
				.findAllUserSessions();
		final Map<String, UserSession> lastActiveMap = convertToMap(
				userSessionsList);

		final List<ActiveUserDTO> activeUserDTOs = new ArrayList<ActiveUserDTO>();
		userList.forEach(u -> {
			final ActiveUserDTO activeUserDTO = new ActiveUserDTO();
			final UserSession us = lastActiveMap.get(u);

			activeUserDTO.setUserName(u);
			activeUserDTO.setOnline(false);
			activeUserDTO.setLastActive(null);
			if (us != null) {
				activeUserDTO.setOnline(us.isOnline());
				activeUserDTO.setLastActive(us.getTimeStamp());
			}

			activeUserDTOs.add(activeUserDTO);
		});

		return activeUserDTOs;
	}

	@Transactional(readOnly = true)
	@Override
	public Optional<ChatUser> getUser(final String userName) {
		return userRepository.findByUserName(userName);
	}

	private Map<String, UserSession> convertToMap(
			final List<UserSession> userSessionList) {
		final Map<String, UserSession> userMap = new HashMap<>();
		userSessionList.forEach(us -> {
			userMap.put(us.getUser().getUserName(), us);
		});
		return userMap;
	}

	private String toLastActiveString(final Instant lastActive) {
		if (lastActive == null)
			return null;
		final Duration duration = Duration.between(lastActive, Instant.now());
		final long hours = duration.toHours();
		final long minutes = duration.toMinutes();
		final long days = duration.toDays();
		if (minutes < 60) {
			return String.format("%d minutes ago", minutes);
		} else {
			if (hours < 24) {
				return String.format("%d hours ago", hours);
			} else {
				if (days < 30) {
					return String.format("%d days ago", days);
				} else if (days < 366) {
					return String.format("%d months ago", days / 30);
				} else {
					return String.format("%d years ago", days / 365);
				}
			}
		}
	}

	@Override
	public void incrementUserSession(final String userName) {
		final Optional<ChatUser> chatUser = getUser(userName);
		final Instant instant = Instant.now();

		chatUser.ifPresent(cu -> {
			UserSession userSession = userSessionRepository
					.findByUserName(userName);

			if (userSession == null) {
				userSession = new UserSession();
			}
			userSession.setUser(cu);
			userSession.setTimeStamp(instant);
			userSession.setOnline(true);
			userSession.setNumSessions(userSession.getNumSessions() + 1);
			userSessionRepository.save(userSession);
		});
	}

	@Override
	public UserSession decrementUserSession(final String userName) {
		final UserSession userSession = userSessionRepository
				.findByUserName(userName);
		final Instant instant = Instant.now();

		if (userSession == null) {
			log.error("User session is somehow null for user: " + userName);
			throw new InternalAuthenticationServiceException(
					"User session not found");
		}

		final ChatUser chatUser = userSession.getUser();

		int numSessions = userSession.getNumSessions();

		if (--numSessions == 0) {
			log.info("Num sessions is 0 so setting user to offline");
			log.info("Deleting token and evicting cache for user: "
					+ chatUser.getUserName());
			userSession.setOnline(false);
			userTokenService.deleteToken(chatUser.getUserName());
			TokenCacheUtil.evictSingleTokenValue(chatUser.getUserName());
		}

		userSession.setNumSessions(numSessions);
		userSession.setTimeStamp(instant);
		return userSessionRepository.save(userSession);
	}

	@Override
	public Set<Role> getRoles(final ChatUser user) {
		return user.getUserRoles().stream().map(ur -> ur.getRole())
				.collect(Collectors.toSet());
	}
}
