package com.ffii.core.user.web;

import java.io.UnsupportedEncodingException;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;

import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.ffii.core.common.ErrorCodes;
import com.ffii.core.common.PasswordRule;
import com.ffii.core.common.SecurityUtils;
import com.ffii.core.exception.BadRequestException;
import com.ffii.core.exception.NotFoundException;
import com.ffii.core.exception.UnprocessableEntityException;
import com.ffii.core.response.FailureRes;
import com.ffii.core.response.IdRes;
import com.ffii.core.response.RecordsRes;
import com.ffii.core.settings.service.SettingsService;
import com.ffii.core.support.AbstractController;
import com.ffii.core.user.entity.User;
import com.ffii.core.user.req.NewUserReq;
import com.ffii.core.user.req.SearchUserReq;
import com.ffii.core.user.req.UpdateUserReq;
import com.ffii.core.user.res.LoadUserRes;
import com.ffii.core.user.service.UserService;
import com.ffii.core.user.service.pojo.UserRecord;
import com.ffii.core.utils.CriteriaArgsBuilder;
import com.ffii.core.utils.Params;
import com.ffii.core.utils.PasswordUtils;

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

/** @author Fung */
@RestController
@RequestMapping("/protected/user")
public class UserController extends AbstractController {

  private UserService userService;
  private PasswordEncoder passwordEncoder;
  private SettingsService settingsService;

  public UserController(
      UserService userService,
      PasswordEncoder passwordEncoder,
      SettingsService settingsService) {
    this.userService = userService;
    this.passwordEncoder = passwordEncoder;
    this.settingsService = settingsService;
  }

  @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"),
      @ApiResponse(responseCode = "404", content = @Content) })
  @GetMapping
  @PreAuthorize("hasAuthority('VIEW_USER')")
  public RecordsRes<UserRecord> list(@ModelAttribute @Valid SearchUserReq req) {
    return new RecordsRes<>(userService.search(req), userService.searchTotalCount(req));
  }

  @Operation(summary = "load user data", responses = { @ApiResponse(responseCode = "200"),
      @ApiResponse(responseCode = "404", content = @Content) })
  @GetMapping("/{id}")
  @PreAuthorize("hasAuthority('VIEW_USER')")
  public LoadUserRes load(@PathVariable int id) {
    return new LoadUserRes(
        userService.find(id).orElseThrow(NotFoundException::new),
        userService.listUserAuthId(id),
        userService.listUserGroupId(id));
  }

  @Operation(summary = "delete user", responses = { @ApiResponse(responseCode = "204"),
      @ApiResponse(responseCode = "404", content = @Content) })
  @DeleteMapping("/{id}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  @PreAuthorize("hasAuthority('EDIT_USER')")
  public void delete(@PathVariable int id) {
    userService.markDelete(userService.find(id).orElseThrow(NotFoundException::new));
  }

  @Operation(summary = "lock user", responses = {
      @ApiResponse(responseCode = "204"),
      @ApiResponse(responseCode = "404", content = @Content) })
  @PatchMapping("/{id}/lock")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  @PreAuthorize("hasAuthority('EDIT_USER')")
  public void lock(@PathVariable int id) {
    if (!userService.lock(id, true)) {
      throw new NotFoundException();
    }
  }

  @Operation(summary = "unlock user", responses = {
      @ApiResponse(responseCode = "204"),
      @ApiResponse(responseCode = "404", content = @Content) })
  @PatchMapping("/{id}/unlock")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  @PreAuthorize("hasAuthority('EDIT_USER')")
  public void unlock(@PathVariable int id) {
    if (!userService.lock(id, false)) {
      throw new NotFoundException();
    }
  }

  @Operation(summary = "new user")
  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  @PreAuthorize("hasAuthority('EDIT_USER')")
  public IdRes newRecord(@RequestBody @Valid NewUserReq req) throws UnsupportedEncodingException {
    return new IdRes(userService.newRecord(req).getId());
  }

  @Operation(summary = "update user", responses = {
      @ApiResponse(responseCode = "204"),
      @ApiResponse(responseCode = "400", content = @Content),
      @ApiResponse(responseCode = "404", content = @Content),
  })
  @PutMapping("/{id}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  @PreAuthorize("hasAuthority('EDIT_USER')")
  public void updateRecord(@PathVariable int id, @RequestBody @Valid UpdateUserReq req) {
    userService.updateRecord(id, req);
  }

  @Operation(summary = "login as some user")
  @PutMapping("/login-as/{username}")
  @PreAuthorize("hasAuthority('SUPERUSER')")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void loginAs(@PathVariable String username) {
    SecurityUtils.loginUser(userService, username);
  }

  @Operation(summary = "current user change password", description = "error: USER_WRONG_NEW_PWD = new password not available", responses = {
      @ApiResponse(responseCode = "204"),
      @ApiResponse(responseCode = "400", content = @Content),
      @ApiResponse(responseCode = "404", content = @Content),
      @ApiResponse(responseCode = "422", content = @Content(schema = @Schema(implementation = FailureRes.class))),
  })
  @PatchMapping("/change-password")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void changePassword(@RequestBody @Valid ChangePwdReq req) {
    int id = SecurityUtils.getUser().get().getId();
    User instance = userService.find(id).orElseThrow(NotFoundException::new);

    if (!passwordEncoder.matches(req.getPassword(), instance.getPassword())) {
      throw new BadRequestException();
    }

    PasswordRule rule = new PasswordRule(settingsService);
    if (!PasswordUtils.checkPwd(req.getNewPassword(), rule)) {
      throw new UnprocessableEntityException(ErrorCodes.USER_WRONG_NEW_PWD);
    }

    instance.setPassword(passwordEncoder.encode(req.getNewPassword()));
    userService.save(instance);
  }

  @Operation(summary = "reset password", responses = {
      @ApiResponse(responseCode = "204"),
      @ApiResponse(responseCode = "404", content = @Content),
  })
  @PostMapping("/{id}/reset-password")
  @PreAuthorize("hasAuthority('EDIT_USER')")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void resetPassword(@PathVariable int id) throws UnsupportedEncodingException {
    userService.resetPassword(id);
  }

  @Operation(summary = "list for combo")
  @GetMapping("/combo")
  // @PreAuthorize("hasAuthority('VIEW_USER')")
  public RecordsRes<Map<String, Object>> comboJson(HttpServletRequest request) throws ServletRequestBindingException {
    Map<String, Object> args = CriteriaArgsBuilder.withRequest(request)
        .addInteger(Params.ID)
        .addStringLike(Params.QUERY)
        .addBoolean("send")
        .build();

    User u = SecurityUtils.getUser().get();
    if (args.containsKey("send") && (Boolean) args.containsKey("send")) {
      args.remove("send");
      args.put("senderId", u.getId());
    }

    // if current user is not from host compnay, then restrict access to users in
    // (self company + host company)
    // if (u.getCompanyId() > 1) {
    // args.put("companyIds", Arrays.asList(u.getCompanyId()));
    // }

    return new RecordsRes<>(userService.searchForCombo(args));
  }

  @Operation(summary = "get password rules")
  @GetMapping("/password-rule")
  public PasswordRule passwordRule() {
    return new PasswordRule(settingsService);
  }

  public static class ChangePwdReq {
    @NotBlank
    private String password;
    @NotBlank
    private String newPassword;

    public String getPassword() {
      return password;
    }

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

    public String getNewPassword() {
      return newPassword;
    }

    public void setNewPassword(String newPassword) {
      this.newPassword = newPassword;
    }

  }
}
