package de.champonthis.buntspecht.businesslogic;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;

import de.champonthis.buntspecht.model.QUser;
import de.champonthis.buntspecht.model.User;
import de.champonthis.buntspecht.repository.UserRepository;
import de.champonthis.buntspecht.security.LocalUserDetails;
import jakarta.transaction.Transactional;

@Service
public class UserManager implements UserDetailsService, SmartInitializingSingleton {

	private Logger logger = LoggerFactory.getLogger(UserManager.class);

	@Autowired
	private UserRepository userRepository;
	@Autowired
	private PasswordEncoder passwordEncoder;
	@Autowired
	private JPAQueryFactory jpaQueryFactory;
	@Autowired
	private TurnoverManager turnoverManager;
	private QUser qUser = QUser.user;

	@Value("${admin.password:}")
	private String adminPassword;

	public QueryResults<User> fetch(long limit, long offset, String sortBy, boolean descending, String usernameFilter) {
		BooleanBuilder builder = new BooleanBuilder();

		if (StringUtils.hasText(usernameFilter)) {
			builder.and(qUser.username.contains(usernameFilter));
		}

		JPAQuery<User> query = jpaQueryFactory.from(qUser).where(builder.getValue()).select(qUser);
		Long total = query.clone().select(qUser.username.countDistinct()).fetchOne();

		if (StringUtils.hasText(sortBy)) {
			Path<? extends Comparable<?>> path = null;
			switch (sortBy) {
				case "username":
					path = qUser.username;
					break;
				case "name":
					path = qUser.name;
					break;
			}
			if (path != null) {
				query.orderBy(new OrderSpecifier<>(descending ? Order.DESC : Order.ASC, path));
			}
		}

		List<User> result = query.limit(limit).offset(offset).fetch();
		return new QueryResults<User>(result, limit, offset, total == null ? 0L : total);
	}

	@Override
	@Transactional
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = getByUsername(username);

		if (user == null) {
			throw new UsernameNotFoundException(username);
		}

		List<GrantedAuthority> authorities = new ArrayList<>();
		if (user.getRoles() != null) {
			for (String role : user.getRoles()) {
				authorities.add(new SimpleGrantedAuthority(role));
			}
		}

		String passwordHash = user.getPasswordHash();

		if (passwordHash == null) {
			passwordHash = "";
		}

		LocalUserDetails userDetails = new LocalUserDetails(username, passwordHash, authorities);
		return userDetails;
	}

	@Override
	public void afterSingletonsInstantiated() {
		if (!userRepository.exists(qUser.roles.contains("ROLE_ADMIN"))) {
			if (!StringUtils.hasText(adminPassword)) {
				adminPassword = RandomStringUtils.random(24, true, true);
				logger.error("password for 'admin': " + adminPassword);
			}
			User admin = new User();
			admin.setUsername("admin");
			admin.setRoles(List.of("ROLE_ADMIN", "ROLE_DEBUG"));
			admin.setPasswordHash(passwordEncoder.encode(adminPassword));
			admin.setLocale("de");
			userRepository.save(admin);
		}
	}

	@Transactional
	public User getByUsername(String username) {
		return userRepository.findOne(qUser.username.equalsIgnoreCase(username)).orElse(null);
	}

	public User getByExternalId(String externalId) {
		return userRepository.findOne(qUser.externalId.eq(externalId)).orElse(null);
	}

	public User getByAuth(Authentication authentication) {
		if (authentication != null) {
			if (authentication instanceof UsernamePasswordAuthenticationToken) {
				UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
				return getByUsername(token.getName());
			} else if (authentication instanceof OAuth2AuthenticationToken) {
				OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
				String externalId = token.getAuthorizedClientRegistrationId() + "-" +
						token.getName();
				User user = getByExternalId(externalId);
				if (user == null) {
					user = new User();
					user.setExternalId(externalId);
					String tmpUsername = token.getPrincipal().getAttribute("preferred_username");
					if (!StringUtils.hasText(tmpUsername)) {
						tmpUsername = token.getPrincipal().getAttribute("username");
					}
					if (!StringUtils.hasText(tmpUsername)) {
						tmpUsername = token.getPrincipal().getAttribute("name");
					} else {
						user.setName(token.getPrincipal().getAttribute("name"));
					}
					if (!StringUtils.hasText(tmpUsername)) {
						tmpUsername = token.getName();
					}
					if (!StringUtils.hasText(tmpUsername)) {
						tmpUsername = "user";
					}
					int count = 1;
					String username = tmpUsername;
					while (userRepository.exists(qUser.username.equalsIgnoreCase(username))) {
						username = tmpUsername + "-" + count;
						count++;
					}

					user.setUsername(username);
					user.setEmail(token.getPrincipal().getAttribute("email"));
					user = userRepository.save(user);
				}
				return user;
			}
		}
		return null;
	}

	public User save(User user) {
		if (exists(user.getUsername())) {
			user.setPasswordHash(this.getPasswordHash(user.getUsername()));
		}

		return userRepository.save(user);
	}

	public boolean exists(String username) {
		return userRepository.exists(qUser.username.equalsIgnoreCase(username));
	}

	public void delete(User user) {
		turnoverManager.deleteByUsername(user.getUsername());
		userRepository.delete(user);
	}

	@Transactional
	public String getPasswordHash(String username) {
		Assert.isTrue(userRepository.existsById(username), "User with username '" + username + "' not exists!");
		return userRepository.findById(username).get().getPasswordHash();
	}

	public User setPassword(String username, String password) {
		Assert.isTrue(userRepository.existsById(username), "User with username '" + username + "' not exists!");
		User user = userRepository.findById(username).get();
		user.setPasswordHash(passwordEncoder.encode(password));
		return userRepository.save(user);
	}
}
