package com.ffii.lioner.modules.user.web;

import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.NoSuchMessageException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.ffii.core.exception.NotFoundException;
import com.ffii.core.exception.UnprocessableEntityException;
import com.ffii.core.response.IdRes;
import com.ffii.core.response.RecordsRes;
import com.ffii.core.utils.CriteriaArgsBuilder;
import com.ffii.core.utils.Params;
import com.ffii.core.utils.PasswordUtils;
import com.ffii.lioner.modules.common.PasswordRule;
import com.ffii.lioner.modules.common.SecurityUtils;
import com.ffii.lioner.modules.common.SettingNames;
import com.ffii.lioner.modules.settings.service.SettingsService;
import com.ffii.lioner.modules.user.entity.User;
import com.ffii.lioner.modules.user.entity.UserPasswordHistory;
import com.ffii.lioner.modules.user.req.NewLionerUserReq;
import com.ffii.lioner.modules.user.req.NewPublicUserReq;
import com.ffii.lioner.modules.user.req.NewUserReq;
import com.ffii.lioner.modules.user.req.SearchUserReq;
import com.ffii.lioner.modules.user.req.UpdateUserReq;
import com.ffii.lioner.modules.user.service.UserPasswordHistoryService;
import com.ffii.lioner.modules.user.service.UserService;
import com.ffii.lioner.modules.user.service.res.LoadUserRes;

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

@RestController
@RequestMapping("/user")
public class UserController {

	private final Log logger = LogFactory.getLog(getClass());
	private UserService userService;
	private PasswordEncoder passwordEncoder;
	private SettingsService settingsService;
	private UserPasswordHistoryService userPasswordHistoryService;

	private Integer passwordHist;
	
	@Autowired
    private LdapTemplate ldapTemplate;

	private Boolean ldapWebServiceRequired;

	public UserController(
			UserService userService,
			PasswordEncoder passwordEncoder,
			SettingsService settingsService,
			UserPasswordHistoryService userPasswordHistoryService) {
		this.userService = userService;
		this.passwordEncoder = passwordEncoder;
		this.settingsService = settingsService;
		this.userPasswordHistoryService = userPasswordHistoryService;
		this.passwordHist = settingsService.getInt(SettingNames.SYS_PASSWORD_RULE_HISTORY);

	}

	// @Operation(summary = "list user", responses = { @ApiResponse(responseCode =
	// "200"),
	// @ApiResponse(responseCode = "404", content = @Content) })
	@GetMapping
	@PreAuthorize("hasAuthority('MAINTAIN_USER')")
	public ResponseEntity<?> list(@ModelAttribute @Valid SearchUserReq req) {
		return ResponseEntity.ok(userService.search(req));
	}

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

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

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

	// @Operation(summary = "new user by public user")
	@PostMapping("/registry")
	@ResponseStatus(HttpStatus.CREATED)
	// @PreAuthorize("hasAuthority('MAINTAIN_USER')")
	public ResponseEntity<?> createPublicUserRecord(@RequestBody NewPublicUserReq req)
			throws UnsupportedEncodingException {
		logger.info("Create user request:");
		return ResponseEntity.ok(new IdRes(userService.newPublicUserRecord(req).getId()));
	}

	@PostMapping("/admin/registry")
	@ResponseStatus(HttpStatus.CREATED)
	// @PreAuthorize("hasAuthority('MAINTAIN_USER')")
	public ResponseEntity<?> createUserRecord(@RequestBody NewLionerUserReq req)
			throws UnsupportedEncodingException, NoSuchMessageException, ParseException, IllegalAccessException {
		return ResponseEntity.ok(new IdRes(userService.newUserRecord(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('MAINTAIN_USER')")
	public void updateRecord(@PathVariable int id, @RequestBody @Valid UpdateUserReq req) {
		userService.updateRecord(id, req);
	}

	// @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)
	// @PreAuthorize("hasAuthority('MAINTAIN_USER')")
	public void changePassword(@RequestBody @Valid ChangePwdReq req) {
		long id = SecurityUtils.getUser().get().getId();
		User instance = userService.find(id).orElseThrow(NotFoundException::new);

		logger.info("TEST req: " + req.getPassword());
		logger.info("TEST instance: " + instance.getPassword());

		if (!passwordEncoder.matches(req.getPassword(), instance.getPassword())) {
			throw new UnprocessableEntityException("Incorrect existing password");
		}

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

		List<String> passHists = userPasswordHistoryService.getPasswordHistory(id);
		passHists.stream().forEach(oldPass -> {
			if (passwordEncoder.matches(req.getNewPassword(), oldPass)) {
				throw new UnprocessableEntityException("The password already exists in the history of the past " + passwordHist + " times.");
			}
		});

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

		// if password updated
		UserPasswordHistory record = new UserPasswordHistory();
		record.setUserId(id);
		record.setPassword(instance.getPassword());
		record.setDate(LocalDateTime.now());
		record = userPasswordHistoryService.save(record);
	}

	// @Operation(summary = "reset password", responses = {
	// @ApiResponse(responseCode = "204"),
	// @ApiResponse(responseCode = "404", content = @Content),
	// })
	@PostMapping("/{id}/reset-password")
	@PreAuthorize("hasAuthority('MAINTAIN_USER')")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	public ResponseEntity<?> resetPassword(@PathVariable long id) throws UnsupportedEncodingException {
		String password = userService.resetPassword(id);
		return ResponseEntity.ok(password);
	}

	// @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;
		}

	}

	@GetMapping("/auth/combo")
	public RecordsRes<Map<String, Object>> authComboJson(HttpServletRequest request)
			throws ServletRequestBindingException {
		System.out.println(request);
		return new RecordsRes<>(userService.listUserAuth(
				CriteriaArgsBuilder.withRequest(request)
						.addInteger("userId")
						.build()));
	}

	@GetMapping("/combo")
	public RecordsRes<Map<String, Object>> comboJson(HttpServletRequest request) throws ServletRequestBindingException {
		return new RecordsRes<>(userService.searchForCombo(
				CriteriaArgsBuilder.withRequest(request)
						.addInteger(Params.ID)
						.addStringLike(Params.QUERY)
						.build()));
	}

	@GetMapping("/audit/combo")
	public RecordsRes<Map<String, Object>> comboJsonWithDeletedUser(HttpServletRequest request) throws ServletRequestBindingException {
		return new RecordsRes<>(userService.searchForAuditCombo(
				CriteriaArgsBuilder.withRequest(request)
						.addInteger(Params.ID)
						.addStringLike(Params.QUERY)
						.build()));
	}

	@GetMapping("/loginLog")
	@PreAuthorize("hasAuthority('MAINTAIN_USER')")
	public RecordsRes<Map<String, Object>> list(HttpServletRequest request) throws ServletRequestBindingException {
		System.out.println(request);
		List<Map<String, Object>> temp = userService.listLoginLog(
				CriteriaArgsBuilder.withRequest(request)
						.addStringLike("username")
						.addStringLike("ipAddr")
						.addDate("fromDate")
						.addDateTo("toDate")
						.build());
		return new RecordsRes<>(temp);
	}

	@GetMapping("/checkDuplicate")
	public Map<String, Boolean> checkDuplicate(@RequestParam String name, Long id) {
		boolean isNameTaken = userService.isNameTaken(name, id);
		return Map.of(
				"isTaken", isNameTaken);
	}

	@GetMapping("/isPasswordExpired")
	public Map<String, Boolean> getIsPasswordExpired(@RequestParam Long id) {
		Integer maximumDay = settingsService.getInt(SettingNames.SYS_PASSWORD_RULE_DURATION);
		Integer dayDiff = userService.getLastChangePasswordDate(id);
		return Map.of("expired", dayDiff >= maximumDay);
	}
}
