package com.ffii.fhsmsc.config.security.jwt.web;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import com.ffii.core.response.AuthRes;
import com.ffii.core.utils.AES;
import com.ffii.core.utils.JwtTokenUtil;
import com.ffii.fhsmsc.config.security.jwt.service.JwtUserDetailsService;
import com.ffii.fhsmsc.config.security.service.LoginLogService;
import com.ffii.fhsmsc.model.ExceptionResponse;
import com.ffii.fhsmsc.model.JwtRequest;
import com.ffii.fhsmsc.model.JwtResponse;
import com.ffii.fhsmsc.model.RefreshToken;
import com.ffii.fhsmsc.model.TokenRefreshRequest;
import com.ffii.fhsmsc.model.TokenRefreshResponse;
import com.ffii.fhsmsc.modules.common.SettingNames;
import com.ffii.fhsmsc.modules.settings.service.SettingsService;
import com.ffii.fhsmsc.modules.user.entity.User;
import com.ffii.fhsmsc.modules.user.entity.UserRepository;
import com.ffii.fhsmsc.modules.user.service.UserAuthorityService;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;

@RestController
public class JwtAuthenticationController {
  @Autowired
  private AuthenticationManager authenticationManager;

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @Autowired
  private JwtUserDetailsService userDetailsService;

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private UserAuthorityService userAuthorityService;

  @Autowired
  private LoginLogService loginLogService;

  @Autowired
  private SettingsService settingsService;

  private static final long EXPIRY_IN_MINTUE = 60000;
  private static final long TOKEN_DURATION = 30;

  @PostMapping("/login")
  public ResponseEntity<?> login(@RequestBody JwtRequest authenticationRequest, HttpServletRequest request)
      throws Exception {
    String username = authenticationRequest.getUsername();
    HttpStatus httpStatus = HttpStatus.UNAUTHORIZED;
    String message = "Invalid username and password.";
    Integer maxAttempt = settingsService.getInt(SettingNames.SYS_LOGIN_ATTEMPT_LIMIT);
    Integer penalityTime = settingsService.getInt(SettingNames.SYS_LOGIN_ATTEMPT_PENALITY_TIME);
    String checkAddrr = request.getHeader("X-Forwarded-For");
    if (checkAddrr == null || "".equals(checkAddrr)) {
      checkAddrr = request.getRemoteAddr();
    }
    List<Map<String, Object>> failAttempts = loginLogService.failChecking(username, checkAddrr, maxAttempt);
    if (failAttempts.size() >= maxAttempt) {
      // check if last failAttempts not contain sucess login then trigger penality
      // time
      if (!failAttempts.stream().map(x -> {
        return x.get("success");
      }).collect(Collectors.toList()).contains(true)) {
        LocalDateTime lastAttemptTime = (LocalDateTime) failAttempts.get(0).get("loginTime");
        if (LocalDateTime.now().isBefore(lastAttemptTime.plusMinutes(penalityTime))) {
          return ResponseEntity.status(httpStatus)
              .body(new ExceptionResponse(
                  "Login failure reach the limit.\n Please try again after "
                    // add 1 to round up
                      + (ChronoUnit.MINUTES.between(LocalDateTime.now(), lastAttemptTime.plusMinutes(penalityTime)) + 1)
                      + " minutes.",
                  null));
        }
      }
    }

    AuthRes authRes =  authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());

    UserDetails userDetails = null;
    if (authRes.isSuccess()) {
      try {
        userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
      } catch (UsernameNotFoundException e) {
        message = username + " not yet register in the system.";
        authRes.setSuccess(false);
        authRes.setException(ExceptionUtils.getStackTrace(e));
      } catch (Exception e) {
        httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
        message = "Internal Server Error";
        authRes.setSuccess(false);
        authRes.setException(ExceptionUtils.getStackTrace(e));
      }
    }

    String remoteAddrr = request.getHeader("X-Forwarded-For");
    if (remoteAddrr == null || "".equals(remoteAddrr)) {
      remoteAddrr = request.getRemoteAddr();
    }

    loginLogService.createLoginLog(username, remoteAddrr, authRes.isSuccess());

    if (userDetails == null) {
      return ResponseEntity.status(httpStatus)
          .body(new ExceptionResponse(message, authRes.getException()));
    } else {
      User user = userRepository.findByName(userDetails.getUsername()).get(0);
      if (user.isLocked()) {
        return ResponseEntity.status(httpStatus)
            .body(new ExceptionResponse(username + " has been locked.", null));
      }
    }

    return createAuthTokenResponse(userDetails);
  }

  private AuthRes authenticate(String username, String password) throws Exception {
    boolean success = false;
    String exception = "";
    try {
      authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
      success = true;
    } catch (Exception e) {
      exception = ExceptionUtils.getStackTrace(e);
    }
    return new AuthRes(success, exception);
  }

  private ResponseEntity<?> createAuthTokenResponse(UserDetails userDetails) {
    long accessTokenExpiry = /* settingsService.getInt(SettingNames.SYS_IDLE_LOGOUT_TIME) */ TOKEN_DURATION
        * EXPIRY_IN_MINTUE;
    final String accessToken = jwtTokenUtil.generateToken(userDetails, accessTokenExpiry);
    final String refreshToken = jwtTokenUtil.createRefreshToken(userDetails.getUsername()).getToken();

    User user = userRepository.findByUsernameAndDeletedFalse(userDetails.getUsername()).get();

    List<String> abilities = new ArrayList<String>();
    userAuthorityService.getUserAuthority(user).forEach(auth -> abilities.add(auth.get("authority").toString()));

    return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken, null, user, abilities));
  }

  @PostMapping("/refresh-token")
  public ResponseEntity<TokenRefreshResponse> refreshtoken(@Valid @RequestBody TokenRefreshRequest request)
      throws Exception {
    long accessTokenExpiry = /* settingsService.getInt(SettingNames.SYS_IDLE_LOGOUT_TIME) */ TOKEN_DURATION
        * EXPIRY_IN_MINTUE;
    String requestRefreshToken = request.getRefreshToken();

    requestRefreshToken = requestRefreshToken.replaceAll("\"", "");
    String[] decryptStringList = AES.decrypt(requestRefreshToken, JwtTokenUtil.AES_SECRET)
        .split(JwtTokenUtil.TOKEN_SEPARATOR);
    RefreshToken instance = new RefreshToken();
    String username = decryptStringList[0];
    instance.setExpiryDate(Instant.ofEpochMilli(Long.valueOf(decryptStringList[1])));
    instance.setToken(requestRefreshToken);
    instance.setUserName(decryptStringList[0]);

    if (!jwtTokenUtil.verifyExpiration(instance)) {
      throw new ResponseStatusException(HttpStatus.EXPECTATION_FAILED,
          "Refresh token was expired. Please make a new signin request");
    }

    final UserDetails userDetails = userDetailsService.loadUserByUsername(username);

    String accessToken = jwtTokenUtil.generateToken(userDetails, accessTokenExpiry);
    String refreshToken = jwtTokenUtil.createRefreshToken(username).getToken();
    return ResponseEntity.ok(new TokenRefreshResponse(accessToken, refreshToken));
  }

}
