package com.ffii.core.common.mobile.web;

import java.util.Collection;
import java.util.UUID;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import com.ffii.core.common.mobile.entity.AccessToken;
import com.ffii.core.common.mobile.service.AccessTokenService;
import com.ffii.core.department.entity.Department;
import com.ffii.core.department.service.DepartmentService;
import com.ffii.core.support.AbstractController;
import com.ffii.core.user.entity.User;
import com.ffii.core.user.service.UserAttemptService;
import com.ffii.core.user.service.UserService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/** @author Fung */
@RestController
@RequestMapping("/public/mobile")
public class PublicMobileLoginController extends AbstractController {

  protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

  private final int AUTO_LOCK_COUNT;
  private PasswordEncoder passwordEncoder;
  private AccessTokenService accessTokenService;
  private UserService userService;
  private UserAttemptService userAttemptService;
  private DepartmentService departmentService;

  public PublicMobileLoginController(
      @Value("${app.login.attempt-lock:5}") int autoLockCount,
      PasswordEncoder passwordEncoder,
      AccessTokenService accessTokenService,
      UserService userService,
      UserAttemptService userAttemptService,
      DepartmentService departmentService) {
    this.AUTO_LOCK_COUNT = autoLockCount;
    this.passwordEncoder = passwordEncoder;
    this.accessTokenService = accessTokenService;
    this.userService = userService;
    this.userAttemptService = userAttemptService;
    this.departmentService = departmentService;
  }

  @Operation(summary = "mobile login", responses = {
      @ApiResponse(responseCode = "200"),
      @ApiResponse(responseCode = "400", description = "bad request", content = @Content),
      @ApiResponse(responseCode = "401", description = "login failure", content = @Content),
      @ApiResponse(responseCode = "403", description = "account unavailable", content = @Content)
  })
  @PostMapping("/login")
  public LoginResponse login(@RequestBody @Valid LoginRequest loginRequest) {
    try {
      String accessToken = this.authenticate(loginRequest.getDeviceId(), loginRequest.getUsername(),
          loginRequest.getPassword());
      this.userAttemptService.loginSuccess(loginRequest.getUsername());
      User user = userService.loadUserByUsername(loginRequest.getUsername());
      // Department department = departmentService.find(user.getDeptId()).orElseThrow();
      return new LoginResponse(accessToken, user, null);
    } catch (AuthenticationException exception) {
      userAttemptService.loginFailure(loginRequest.getUsername());

      if (userAttemptService.findAttempt(loginRequest.getUsername()) >= AUTO_LOCK_COUNT) {
        userService.findByUsername(loginRequest.getUsername()).ifPresent(user -> {
          if (!user.isLocked()) {
            user.setLocked(Boolean.TRUE);
            userService.save(user);
          }
        });
      }

      if (exception instanceof LockedException ||
          exception instanceof DisabledException ||
          exception instanceof AccountExpiredException ||
          exception instanceof CredentialsExpiredException) {
        throw new ResponseStatusException(HttpStatus.FORBIDDEN);
      } else {
        throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
      }
    }
  }

  /** @return access token */
  private String authenticate(String deviceId, String username, String password) throws AuthenticationException {
    this.check(
        userService.findByUsername(username)
            .orElseThrow(() -> new BadCredentialsException(
                messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"))),
        password);

    return accessTokenService.save(new AccessToken(username, deviceId, UUID.randomUUID().toString()))
        .getAccessToken();
  }

  private void check(UserDetails user, String password) throws AuthenticationException {
    if (!user.isAccountNonLocked()) {
      logger.debug("User account is locked");

      throw new LockedException(messages.getMessage(
          "AbstractUserDetailsAuthenticationProvider.locked",
          "User account is locked"));
    }

    if (!user.isEnabled()) {
      logger.debug("User account is disabled");

      throw new DisabledException(messages.getMessage(
          "AbstractUserDetailsAuthenticationProvider.disabled",
          "User is disabled"));
    }

    if (!user.isAccountNonExpired()) {
      logger.debug("User account is expired");

      throw new AccountExpiredException(messages.getMessage(
          "AbstractUserDetailsAuthenticationProvider.expired",
          "User account has expired"));
    }

    if (!passwordEncoder.matches(password, user.getPassword())) {
      logger.debug("Authentication failed: password does not match stored value");

      throw new BadCredentialsException(messages.getMessage(
          "AbstractUserDetailsAuthenticationProvider.badCredentials",
          "Bad credentials"));
    }
  }

  public static class LoginRequest {

    @NotBlank
    private String deviceId;
    @NotBlank
    private String username;
    @NotBlank
    private String password;

    public String getDeviceId() {
      return this.deviceId;
    }

    public void setDeviceId(String deviceId) {
      this.deviceId = deviceId;
    }

    public String getUsername() {
      return this.username;
    }

    public void setUsername(String username) {
      this.username = username;
    }

    public String getPassword() {
      return this.password;
    }

    public void setPassword(String password) {
      this.password = password;
    }
  }

  public static class LoginResponse {

    String accessToken;
    LoginResponse.User user;

    public LoginResponse(String accessToken, com.ffii.core.user.entity.User user, Department department) {
      this.accessToken = accessToken;
      this.user = new LoginResponse.User(user.getUsername(), user.getName(), user.getId(), user.getDeptId(),
          department == null ? null : department.getCaseOrderBy(), user.getAuthorities());
    }

    public String getAccessToken() {
      return accessToken;
    }

    public void setAccessToken(String accessToken) {
      this.accessToken = accessToken;
    }

    public LoginResponse.User getUser() {
      return user;
    }

    public void setUser(LoginResponse.User user) {
      this.user = user;
    }

    public static class User {

      String username;
      String fullname;
      int id;
      Integer departmentId;
      Integer caseOrderBy;
      Collection<? extends GrantedAuthority> authorities;

      public User(String username, String fullname, int id, Integer departmentId, Integer caseOrderBy,
          Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.fullname = fullname;
        this.id = id;
        this.departmentId = departmentId;
        this.caseOrderBy = caseOrderBy;
        this.authorities = authorities;
      }

      public String getUsername() {
        return username;
      }

      public void setUsername(String username) {
        this.username = username;
      }

      public int getId() {
        return id;
      }

      public void setId(int id) {
        this.id = id;
      }

      public Integer getDepartmentId() {
        return departmentId;
      }

      public void setDepartmentId(Integer departmentId) {
        this.departmentId = departmentId;
      }

      public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
      }

      public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
      }

      public Integer getCaseOrderBy() {
        return caseOrderBy;
      }

      public void setCaseOrderBy(Integer caseOrderBy) {
        this.caseOrderBy = caseOrderBy;
      }

      public String getFullname() {
        return fullname;
      }

    }
  }

}
