package ch.rasc.eds.starter.service;

import static ch.ralscha.extdirectspring.annotation.ExtDirectMethodType.POLL;

import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestParam;

import ch.ralscha.extdirectspring.annotation.ExtDirectMethod;
import ch.ralscha.extdirectspring.annotation.ExtDirectMethodType;
import ch.ralscha.extdirectspring.bean.ExtDirectFormPostResult;
import ch.rasc.eds.starter.Application;
import ch.rasc.eds.starter.config.security.JpaUserDetails;
import ch.rasc.eds.starter.config.security.RequireAdminAuthority;
import ch.rasc.eds.starter.config.security.RequireAnyAuthority;
import ch.rasc.eds.starter.dto.UserDetailDto;
import ch.rasc.eds.starter.entity.QUser;
import ch.rasc.eds.starter.entity.User;
import ch.rasc.eds.starter.util.JPAQueryFactory;
import ch.rasc.eds.starter.util.TotpAuthUtil;
import ch.rasc.eds.starter.web.CsrfController;

@Service
public class SecurityService {
	public static final String AUTH_USER = "authUser";

	private final JPAQueryFactory jpaQueryFactory;

	private final PasswordEncoder passwordEncoder;

	private final MailService mailService;

	private final ApplicationEventPublisher applicationEventPublisher;

	public SecurityService(JPAQueryFactory jpaQueryFactory,
			PasswordEncoder passwordEncoder, MailService mailService,
			ApplicationEventPublisher applicationEventPublisher) {
		this.jpaQueryFactory = jpaQueryFactory;
		this.passwordEncoder = passwordEncoder;
		this.mailService = mailService;
		this.applicationEventPublisher = applicationEventPublisher;
	}

	@ExtDirectMethod
	@Transactional
	public UserDetailDto getAuthUser(
			@AuthenticationPrincipal JpaUserDetails jpaUserDetails) {

		if (jpaUserDetails != null) {
			User user = jpaUserDetails.getUser(this.jpaQueryFactory);
			UserDetailDto userDetailDto = new UserDetailDto(jpaUserDetails, user, null);

			if (!jpaUserDetails.isPreAuth()) {
				user.setLastAccess(ZonedDateTime.now(ZoneOffset.UTC));
			}

			return userDetailDto;
		}

		return null;
	}

	@ExtDirectMethod(ExtDirectMethodType.FORM_POST)
	@PreAuthorize("hasAuthority('PRE_AUTH')")
	@Transactional
	public ExtDirectFormPostResult signin2fa(HttpServletRequest request,
			@AuthenticationPrincipal JpaUserDetails jpaUserDetails,
			@RequestParam("code") int code) {

		User user = jpaUserDetails.getUser(this.jpaQueryFactory);
		if (user != null) {
			if (TotpAuthUtil.verifyCode(user.getSecret(), code, 3)) {
				user.setLastAccess(ZonedDateTime.now(ZoneOffset.UTC));
				jpaUserDetails.grantAuthorities();

				Authentication newAuth = new UsernamePasswordAuthenticationToken(
						jpaUserDetails, null, jpaUserDetails.getAuthorities());
				SecurityContextHolder.getContext().setAuthentication(newAuth);

				ExtDirectFormPostResult result = new ExtDirectFormPostResult();
				result.addResultProperty(AUTH_USER, new UserDetailDto(jpaUserDetails,
						user, CsrfController.getCsrfToken(request)));
				return result;
			}

			BadCredentialsException excp = new BadCredentialsException(
					"Bad verification code");
			AuthenticationFailureBadCredentialsEvent event = new AuthenticationFailureBadCredentialsEvent(
					SecurityContextHolder.getContext().getAuthentication(), excp);
			this.applicationEventPublisher.publishEvent(event);

			user = jpaUserDetails.getUser(this.jpaQueryFactory);
			if (user.getLockedOutUntil() != null) {
				HttpSession session = request.getSession(false);
				if (session != null) {
					Application.logger.debug("Invalidating session: " + session.getId());
					session.invalidate();
				}
				SecurityContext context = SecurityContextHolder.getContext();
				context.setAuthentication(null);
				SecurityContextHolder.clearContext();
			}
		}

		return new ExtDirectFormPostResult(false);
	}

	@ExtDirectMethod(ExtDirectMethodType.FORM_POST)
	@Transactional
	public ExtDirectFormPostResult resetRequest(@RequestParam("email") String email) {
		List<User> users = this.jpaQueryFactory
				.selectFrom(QUser.user).where(QUser.user.loginName.eq(email)
						.or(QUser.user.email.eq(email)).and(QUser.user.deleted.isFalse()))
				.fetch();

		if (users.size() > 1) {
			users = users.stream().filter(u -> u.getLoginName().equals(email))
					.collect(Collectors.toList());
		}

		if (users.size() == 1) {
			User user = users.iterator().next();

			String token = UUID.randomUUID().toString();
			this.mailService.sendPasswortResetEmail(user, token);

			user.setPasswordResetTokenValidUntil(
					ZonedDateTime.now(ZoneOffset.UTC).plusHours(4));
			user.setPasswordResetToken(token);
		}

		return new ExtDirectFormPostResult();
	}

	@ExtDirectMethod(ExtDirectMethodType.FORM_POST)
	@Transactional
	public ExtDirectFormPostResult reset(@RequestParam("newPassword") String newPassword,
			@RequestParam("newPasswordRetype") String newPasswordRetype,
			@RequestParam("token") String token) {

		if (StringUtils.hasText(token) && StringUtils.hasText(newPassword)
				&& StringUtils.hasText(newPasswordRetype)
				&& newPassword.equals(newPasswordRetype)) {
			String decodedToken = new String(Base64.getUrlDecoder().decode(token));
			User user = this.jpaQueryFactory.selectFrom(QUser.user)
					.where(QUser.user.passwordResetToken.eq(decodedToken),
							QUser.user.deleted.isFalse(), QUser.user.enabled.isTrue())
					.fetchFirst();
			if (user != null && user.getPasswordResetTokenValidUntil() != null) {

				ExtDirectFormPostResult result;

				if (user.getPasswordResetTokenValidUntil()
						.isAfter(ZonedDateTime.now(ZoneOffset.UTC))) {
					user.setPasswordHash(this.passwordEncoder.encode(newPassword));
					user.setSecret(null);

					JpaUserDetails principal = new JpaUserDetails(user);
					UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
							principal, null, principal.getAuthorities());
					SecurityContextHolder.getContext().setAuthentication(authToken);

					result = new ExtDirectFormPostResult();
					result.addResultProperty(AUTH_USER,
							new UserDetailDto(principal, user, null));
				}
				else {
					result = new ExtDirectFormPostResult(false);
				}
				user.setPasswordResetToken(null);
				user.setPasswordResetTokenValidUntil(null);
				this.jpaQueryFactory.getEntityManager().merge(user);

				return result;
			}
		}

		return new ExtDirectFormPostResult(false);
	}

	@ExtDirectMethod
	@RequireAdminAuthority
	@Transactional(readOnly = true)
	public UserDetailDto switchUser(Long userId) {
		User switchToUser = this.jpaQueryFactory.getEntityManager().find(User.class,
				userId);
		if (switchToUser != null) {

			JpaUserDetails principal = new JpaUserDetails(switchToUser);
			UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
					principal, null, principal.getAuthorities());

			SecurityContextHolder.getContext().setAuthentication(token);

			return new UserDetailDto(principal, switchToUser, null);
		}

		return null;
	}

	@ExtDirectMethod(value = POLL, event = "heartbeat")
	@RequireAnyAuthority
	public void heartbeat() {
		// nothing here
	}

}