package cz.muni.fi.pa165.soccermanager.security;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import cz.muni.fi.pa165.soccermanager.dto.LoginResult;
import cz.muni.fi.pa165.soccermanager.dto.UserAuthenticateDto;
import cz.muni.fi.pa165.soccermanager.dto.request.CreateUserRequest;
import cz.muni.fi.pa165.soccermanager.entity.User;
import cz.muni.fi.pa165.soccermanager.exception.DeleteEntityException;
import cz.muni.fi.pa165.soccermanager.exception.NameAlreadyExistsException;
import cz.muni.fi.pa165.soccermanager.mapper.UserMapper;
import cz.muni.fi.pa165.soccermanager.repository.UserRepository;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.GrantedAuthority;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;

@Component
@RequiredArgsConstructor
public class AuthServiceImpl {

  private final UserDetailsService userDetailsService;
  private final PasswordEncoder passwordEncoder;
  private final UserRepository userRepository;

  private final RSAPrivateKey privateKey;
  private final RSAPublicKey publicKey;

  private final InMemoryUserDetailsManager manager;

  private final UserMapper userMapper;

  public LoginResult performLogin(UserAuthenticateDto authenticateDto) {
    try {
      final var userDetails = userDetailsService.loadUserByUsername(authenticateDto.getUserName());

      if (passwordEncoder.matches(authenticateDto.getPassword(), userDetails.getPassword())) {
        final var claims = new HashMap<String, String>();
        claims.put("username", authenticateDto.getUserName());

        final var authorities = userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.joining(" "));
        final var userId = userRepository.findByNickname(authenticateDto.getUserName()).map(User::getId);
        claims.put("roles", authorities);
        claims.put("userId", userId.toString());

        final var jwt = createJwtForClaims(authenticateDto.getUserName(), claims);

        return new LoginResult(jwt, userId.orElse(null),
            userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()).contains("ADMIN"));
      }

      throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated");

    } catch (UsernameNotFoundException e) {
      throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found");
    }

  }

  public UserAuthenticateDto registerUser(CreateUserRequest request) {
    if (userRepository.nickNameExists(request.getNickName()).equals(Boolean.TRUE)) {
      throw new NameAlreadyExistsException("Nickname must be unique");
    }
    userRepository.saveAndFlush(userMapper.map(request));
    final var registration = org.springframework.security.core.userdetails.User
        .withUsername(request.getNickName())
        .authorities("USER")
        .passwordEncoder(passwordEncoder::encode)
        .password(request.getPassword())
        .build();
    manager.createUser(registration);
    final var log = new UserAuthenticateDto();
    log.setUserName(request.getNickName());
    log.setPassword(request.getPassword());
    return log;
  }

  @PreAuthorize("hasAuthority('SCOPE_ADMIN') && !@sc.thisUser(#u)")
  public void removeUser(Long u) {
    final var user = userRepository.findById(u).orElseThrow(() -> {
      throw new EntityNotFoundException();
    });
    if (user.getTeam() != null) {
      throw new DeleteEntityException("User have team");
    }
    userRepository.delete(user);
    manager.deleteUser(user.getNickName());
  }

  private String createJwtForClaims(String subject, Map<String, String> claims) {
    final var calendar = Calendar.getInstance();
    calendar.setTimeInMillis(Instant.now().toEpochMilli());
    calendar.add(Calendar.DATE, 1);

    final var jwtBuilder = JWT.create().withSubject(subject);

    // Add claims
    claims.forEach(jwtBuilder::withClaim);

    // Add expiredAt and etc
    return jwtBuilder
        .withNotBefore(new Date())
        .withExpiresAt(calendar.getTime())
        .sign(Algorithm.RSA256(publicKey, privateKey));
  }
}
