package com.proit.debitors.controller;

import com.proit.debitors.model.Debitor;
import com.proit.debitors.model.User;
import com.proit.debitors.model.UserProfile;
import com.proit.debitors.model.UserProfileType;
import com.proit.debitors.service.DebitorService;
import com.proit.debitors.service.UserProfileService;
import com.proit.debitors.service.UserService;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;



@Controller
@RequestMapping("/")
@SessionAttributes("roles")
public class AppController {

	@Autowired
	UserService userService;

	@Autowired
	DebitorService debitorService;
	
	@Autowired
	UserProfileService userProfileService;
	
	@Autowired
	MessageSource messageSource;

	@Autowired
	PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices;
	
	@Autowired
	AuthenticationTrustResolver authenticationTrustResolver;

	@Autowired
	ObjectMapper objectMapper;
	
	/**
	 * This method will list all existing users.
	 */
	@RequestMapping(value = { "/list" }, method = RequestMethod.GET)
	public String listUsers(ModelMap model) {
		List<User> users = userService.findAllUsers();
		model.addAttribute("users", users);
		model.addAttribute("loggedinuser", getPrincipal());
		return "userslist";
	}

	/**
	 * This method will provide the medium to add a new user.
	 */
	@RequestMapping(value = { "/newuser" }, method = RequestMethod.GET)
	public String newUser(ModelMap model) {
		User user = new User();
		model.addAttribute("user", user);
		model.addAttribute("edit", false);
		model.addAttribute("loggedinuser", getPrincipal());
		return "addnewuser";
	}

	/**
	 * This method will be called on form submission, handling POST request for
	 * saving user in database. It also validates the user input
	 */
	@RequestMapping(value = { "/newuser" }, method = RequestMethod.POST)
	public String saveUser(@Valid User user, BindingResult result,
			ModelMap model) {

		if (result.hasErrors()) {
			return "addnewuser";
		}

		/*
		 * Preferred way to achieve uniqueness of field [sso] should be implementing custom @Unique annotation 
		 * and applying it on field [sso] of Model class [User].
		 * 
		 * Below mentioned peace of code [if block] is to demonstrate that you can fill custom errors outside the validation
		 * framework as well while still using internationalized messages.
		 * 
		 */
		if(!userService.isUserSSOUnique(user.getId(), user.getSsoId())){
			FieldError ssoError =new FieldError("user","ssoId",messageSource.getMessage("non.unique.ssoId", new String[]{user.getSsoId()}, Locale.getDefault()));
		    result.addError(ssoError);
			return "addnewuser";
		}
		
		userService.saveUser(user);

		model.addAttribute("success", "User " + user.getFirstName() + " "+ user.getLastName() + " registered successfully");
		model.addAttribute("loggedinuser", getPrincipal());
		//return "success";
		return "registrationsuccess";
	}

	/**
	 * This method will provide the medium to add a new user.
	 */
	@RequestMapping(value = { "/register" }, method = RequestMethod.GET)
	public String registerUser(ModelMap model) {
		User user = new User();
		//UserProfile userProfile =
		//user.setUserProfiles();
		model.addAttribute("user", user);
		model.addAttribute("edit", false);
		//model.addAttribute("loggedinuser", getPrincipal());
		return "registration";
	}

	/**
	 * This method will be called on form submission, handling POST request for
	 * saving user in database. It also validates the user input
	 */
	@RequestMapping(value = { "/register" }, method = RequestMethod.POST)
	public String registerUser(@Valid User user, BindingResult result,
						   ModelMap model) {

		if (result.hasErrors()) {
			return "registration";
		}

		/*
		 * Preferred way to achieve uniqueness of field [sso] should be implementing custom @Unique annotation
		 * and applying it on field [sso] of Model class [User].
		 *
		 * Below mentioned peace of code [if block] is to demonstrate that you can fill custom errors outside the validation
		 * framework as well while still using internationalized messages.
		 *
		 */
		if(!userService.isUserSSOUnique(user.getId(), user.getSsoId())){
			FieldError ssoError =new FieldError("user","ssoId",messageSource.getMessage("non.unique.ssoId", new String[]{user.getSsoId()}, Locale.getDefault()));
			result.addError(ssoError);
			return "registration";
		}

		userService.saveUser(user);

		model.addAttribute("success", "User " + user.getFirstName() + " "+ user.getLastName() + " registered successfully");
		model.addAttribute("loggedinuser", getPrincipal());
		//return "success";
		return "registrationsuccess";
	}


	/**
	 * This method will provide the medium to update an existing user.
	 */
	@RequestMapping(value = { "/edit-user-{ssoId}" }, method = RequestMethod.GET)
	public String editUser(@PathVariable String ssoId, ModelMap model) {
		User user = userService.findBySSO(ssoId);
		model.addAttribute("user", user);
		model.addAttribute("edit", true);
		model.addAttribute("loggedinuser", getPrincipal());
		return "addnewuser";
	}
	
	/**
	 * This method will be called on form submission, handling POST request for
	 * updating user in database. It also validates the user input
	 */
	@RequestMapping(value = { "/edit-user-{ssoId}" }, method = RequestMethod.POST)
	public String updateUser(@Valid User user, BindingResult result,
			ModelMap model, @PathVariable String ssoId) {

		if (result.hasErrors()) {
			return "addnewuser";
		}

		/*//Uncomment below 'if block' if you WANT TO ALLOW UPDATING SSO_ID in UI which is a unique key to a User.
		if(!userService.isUserSSOUnique(user.getId(), user.getSsoId())){
			FieldError ssoError =new FieldError("user","ssoId",messageSource.getMessage("non.unique.ssoId", new String[]{user.getSsoId()}, Locale.getDefault()));
		    result.addError(ssoError);
			return "registration";
		}*/


		userService.updateUser(user);

		model.addAttribute("success", "User " + user.getFirstName() + " "+ user.getLastName() + " updated successfully");
		model.addAttribute("loggedinuser", getPrincipal());
		return "registrationsuccess";
	}

	
	/**
	 * This method will delete an user by it's SSOID value.
	 */
	@RequestMapping(value = { "/delete-user-{ssoId}" }, method = RequestMethod.GET)
	public String deleteUser(@PathVariable String ssoId) {
		userService.deleteUserBySSO(ssoId);
		return "redirect:/list";
	}
	

	/**
	 * This method will provide UserProfile list to views
	 */
	@ModelAttribute("roles")
	public List<UserProfile> initializeProfiles() {
		return userProfileService.findAll();
	}
	
	/**
	 * This method handles Access-Denied redirect.
	 */
	@RequestMapping(value = "/Access_Denied", method = RequestMethod.GET)
	public String accessDeniedPage(ModelMap model) {
		model.addAttribute("loggedinuser", getPrincipal());
		return "accessDenied";
	}

	/**
	 * This method handles login GET requests.
	 * If users is already logged-in and tries to goto login page again, will be redirected to list page.
	 */
	@RequestMapping(value = "/login", method = RequestMethod.GET)
	public String loginPage() {
		if (isCurrentAuthenticationAnonymous()) {
			return "login";
	    } else {
			User user = userService.findBySSO(getPrincipal());
			if (user !=null && (user.hasRole(UserProfileType.DBA) || user.hasRole(UserProfileType.ADMIN) )) {
				return "redirect:/list";
			} else {
				return "redirect:/debitors";
			}
	    }
	}

	/**
	 * This method handles logout requests.
	 * Toggle the handlers if you are RememberMe functionality is useless in your app.
	 */
	@RequestMapping(value="/logout", method = RequestMethod.GET)
	public String logoutPage (HttpServletRequest request, HttpServletResponse response){
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		if (auth != null){    
			//new SecurityContextLogoutHandler().logout(request, response, auth);
			persistentTokenBasedRememberMeServices.logout(request, response, auth);
			SecurityContextHolder.getContext().setAuthentication(null);
		}
		return "redirect:/login?logout";
	}

	/**
	 * This method returns the principal[user-name] of logged-in user.
	 */
	private String getPrincipal(){
		String userName = null;
		Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

		if (principal instanceof UserDetails) {
			userName = ((UserDetails)principal).getUsername();
		} else {
			userName = principal.toString();
		}
		return userName;
	}
	
	/**
	 * This method returns true if users is already authenticated [logged-in], else false.
	 */
	private boolean isCurrentAuthenticationAnonymous() {
	    final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
	    return authenticationTrustResolver.isAnonymous(authentication);
	}


	private boolean isCurrentAuthenticationUser() {
		final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

		return authenticationTrustResolver.isAnonymous(authentication);
	}

	/**
	 * This method will list all existing users.
	 */
	@RequestMapping(value = {"/", "/debitors" }, method = RequestMethod.GET)
	public String listDebitors(ModelMap model) {

		User user = userService.findBySSO(getPrincipal());
		List<Debitor> debitors = new ArrayList<>(user.getUserDebitors());
		model.addAttribute("debitors", debitors);
		model.addAttribute("agent", user.getId());
		model.addAttribute("loggedinuser", getPrincipal());
		return "debitorslist";
	}

	/**
	 * This method will provide the medium to add a new user.
	 */
	@RequestMapping(value = { "/newdebitor" }, method = RequestMethod.GET)
	public String newDebitor(ModelMap model) {
		Debitor debitor = new Debitor();

		model.addAttribute("debitor", debitor);
		model.addAttribute("edit", false);
		model.addAttribute("loggedinuser", getPrincipal());
		return "addnewdebitor";
	}

	/**
	 * This method will be called on form submission, handling POST request for
	 * saving user in database. It also validates the user input
	 */
	@RequestMapping(value = { "/newdebitor" }, method = RequestMethod.POST)
	public String saveDebitor(@Valid Debitor debitor, BindingResult result,
							  ModelMap model) throws IOException {

		if (result.hasErrors()) {
			return "addnewdebitor";
		}

		/*
		 * Preferred way to achieve uniqueness of field [sso] should be implementing custom @Unique annotation
		 * and applying it on field [sso] of Model class [User].
		 *
		 * Below mentioned peace of code [if block] is to demonstrate that you can fill custom errors outside the validation
		 * framework as well while still using internationalized messages.
		 *
		 */
		User user = userService.findBySSO(getPrincipal());
		user.getUserDebitors().add(debitor);
		debitor.setUser(user);

		MultipartFile file =debitor.getFile();
		if ((file != null) && (file.getSize() > 0)) {
			debitor.setFileName(file.getOriginalFilename());
			debitor.setFileType(file.getContentType());
			debitor.setFileData(file.getBytes());
		}

		debitorService.saveDebitor(debitor);

		model.addAttribute("debitors", user.getUserDebitors());
		model.addAttribute("agent", user.getId());
		model.addAttribute("loggedinuser", getPrincipal());
		//return "success";
		return "debitorslist";
	}

	@RequestMapping(value = { "/delete-debitor-{debitorId}" }, method = RequestMethod.GET)
	public String deleteDebitor(@PathVariable Integer debitorId) {
		Debitor debitor = debitorService.findById(debitorId);
		User user = userService.findBySSO(getPrincipal());
		if (!debitor.getUser().getId().equals(debitor.getUser().getId())) {
			return "redirect:/Access_Denied";
		}
		debitorService.deleteDebitor(debitorId);
		return "redirect:/debitors";
	}

	/**
	 * This method will provide the medium to update an existing user.
	 */
	@RequestMapping(value = { "/edit-debitor-{debitorId}" }, method = RequestMethod.GET)
	public String editDebitor(@PathVariable Integer debitorId, ModelMap model) {
		Debitor debitor = debitorService.findById(debitorId);
		User user = userService.findBySSO(getPrincipal());
		if (!debitor.getUser().getId().equals(debitor.getUser().getId())) {
			return "redirect:/Access_Denied";
		}



		model.addAttribute("debitor", debitor);

		model.addAttribute("edit", true);
		model.addAttribute("loggedinuser", getPrincipal());
		return "addnewdebitor";
	}

	/**
	 * This method will be called on form submission, handling POST request for
	 * updating user in database. It also validates the user input
	 */
	@RequestMapping(value = { "/edit-debitor-{debitorId}" }, method = RequestMethod.POST)
	public String updateUser(@Valid Debitor debitor, BindingResult result,
							 ModelMap model, @PathVariable Integer debitorId) throws IOException {

		if (result.hasErrors()) {
			return "addnewdebitor";
		}

		MultipartFile file =debitor.getFile();
		if ((file != null) && (file.getSize() > 0)) {
			debitor.setFileName(file.getOriginalFilename());
			debitor.setFileType(file.getContentType());
			debitor.setFileData(file.getBytes());
		}

		debitorService.updateDebitor(debitor);
		User user = userService.findBySSO(getPrincipal());
		model.addAttribute("debitors", user.getUserDebitors());
		model.addAttribute("agent", user.getId());
		model.addAttribute("loggedinuser", getPrincipal());
		return "debitorslist";
	}
	@RequestMapping(value = { "/downloaddoc-{debitorId}" }, method = RequestMethod.GET)
	public void downloadDoc(@PathVariable Integer debitorId, HttpServletResponse response) throws Exception {
		Debitor debitor = debitorService.findById(debitorId);
		User user = userService.findBySSO(getPrincipal());
		if (!debitor.getUser().getId().equals(debitor.getUser().getId())) {
			throw new Exception("Access denied");
		}
		try {
			response.setContentType(debitor.getFileType());
			response.setHeader("Content-Disposition", "attachment; filename=\""+debitor.getFileName()+"\"");
			// get your file as InputStream
			InputStream is = new ByteArrayInputStream(debitor.getFileData());
			// copy it to response's OutputStream
			org.apache.commons.io.IOUtils.copy(is, response.getOutputStream());
			response.flushBuffer();
		} catch (IOException ex) {
			//log.info("Error writing file to output stream. Filename was '{}'", fileName, ex);
			throw new RuntimeException("IOError writing file to output stream");
		}

	}

	@RequestMapping(value = {"/debitors-{userId}" }, method = RequestMethod.GET)
	public String listUserDebitors(@PathVariable Integer userId, ModelMap model) {
		User user = userService.findBySSO(getPrincipal());
		User agent = userService.findById(userId);
		if (!user.hasRole(UserProfileType.ADMIN) && !user.hasRole(UserProfileType.DBA) && user.getId().intValue() != agent.getId().intValue()) {
			return "redirect:/Access_Denied";
		}

		List<Debitor> debitors = new ArrayList<>(agent.getUserDebitors());
		model.addAttribute("debitors", debitors);
		model.addAttribute("agent", agent.getId());
		model.addAttribute("loggedinuser", getPrincipal());
		return "debitorslist";
	}

	@RequestMapping(value = { "/downloaddebitors-{userId}" }, method = RequestMethod.GET)
	public void downloadDebitors(@PathVariable Integer userId, HttpServletResponse response) throws Exception {
		User user = userService.findBySSO(getPrincipal());
		User agent = userService.findById(userId);
		if (!user.hasRole(UserProfileType.ADMIN)
				&& !user.hasRole(UserProfileType.DBA) && user.getId().intValue() != agent.getId().intValue()) {
			throw new Exception("Access denied");
		}


		try {
			response.setContentType("application/json");
			response.setHeader("Content-Disposition", "attachment; filename=\""+agent.getIdno()+"_debitors.json\"");
			response.setCharacterEncoding("UTF-8");
			// get your file as InputStream
			InputStream is = new ByteArrayInputStream(objectMapper.writeValueAsBytes(agent.getUserDebitors()));
			// copy it to response's OutputStream
			org.apache.commons.io.IOUtils.copy(is, response.getOutputStream());
			response.flushBuffer();
		} catch (IOException ex) {
			//log.info("Error writing file to output stream. Filename was '{}'", fileName, ex);
			throw new RuntimeException("IOError writing file to output stream");
		}
	}

}