package hsr.dsa.local;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.apache.commons.io.FileUtils;

import hsr.dsa.utils.AES;
import hsr.dsa.utils.RSA;

public class LocalData {
	private static String profilesFolder = "profiles/";
	private final static String decryptedFolder = "decrypted/";
	private final static String userKeysPrefix = "userKey";
	private final static String profileName = "profile.json";
	private final static String publicKeyName = "public.pub";
	private final static String privateKeyName = "private.key";

	private AES aes;

	private static Map<String, String> passwordStorage;

	public LocalData() throws IOException {
		aes = new AES();

		passwordStorage = new HashMap<>();
		File profiles = new File(profilesFolder);

		if (!profiles.isDirectory())
			profiles.mkdirs();
	}

	private String getProfilePath(String username) {
		return profilesFolder + username + "/" + profileName;
	}

	private String getUserKeyPath(String username) {
		return profilesFolder + username + "/" + userKeysPrefix + "_";
	}

	private String getProfileDecryptedPath(String username) {
		return profilesFolder + username + "/" + decryptedFolder + profileName;
	}

	private String getUserKeyDecryptedPath(String username) {
		return profilesFolder + username + "/" + decryptedFolder + userKeysPrefix + "_";
	}

	private byte[] readFile(String pathToFile) throws IOException {
		File file = new File(pathToFile);
		return Files.readAllBytes(file.toPath());
	}

	private void saveFile(byte[] content, String pathToFile) throws IOException {
		Path filePath = Paths.get(pathToFile);

		Files.write(filePath, content);
	}

	private void encryptProfileFiles(String username, String password) throws IOException,
			InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, BadPaddingException,
			IllegalBlockSizeException, InvalidAlgorithmParameterException {

		byte[] profile = readFile(getProfileDecryptedPath(username));
		saveFile(aes.encrypt(profile, password), getProfilePath(username));

		byte[] useKeyPublic = readFile(getUserKeyDecryptedPath(username) + publicKeyName);
		saveFile(aes.encrypt(useKeyPublic, password), getUserKeyPath(username) + publicKeyName);

		byte[] useKeyPrivate = readFile(getUserKeyDecryptedPath(username) + privateKeyName);
		saveFile(aes.encrypt(useKeyPrivate, password), getUserKeyPath(username) + privateKeyName);
	}

	private void decryptProfileFiles(String username, String password) throws Exception {

		File decryptedFolderProfile = new File(profilesFolder + username + "/" + decryptedFolder);
		if (!decryptedFolderProfile.isDirectory())
			decryptedFolderProfile.mkdirs();

		byte[] profile = readFile(getProfilePath(username));
		saveFile(aes.decrypt(profile, password), getProfileDecryptedPath(username));

		byte[] useKeyPublic = readFile(getUserKeyPath(username) + publicKeyName);
		saveFile(aes.decrypt(useKeyPublic, password), getUserKeyDecryptedPath(username) + publicKeyName);

		byte[] useKeyPrivate = readFile(getUserKeyPath(username) + privateKeyName);
		saveFile(aes.decrypt(useKeyPrivate, password), getUserKeyDecryptedPath(username) + privateKeyName);
	}

	/**
	 * Get list of profiles which are decrypted
	 * @return List of decrypted directories
	 * @throws InvalidDataException Profile directory does not exist
	 */
	private List<String> getProfileListDecrypted() throws InvalidDataException {

		File file = new File(profilesFolder);
		String[] profileDirectories = file.list(new FilenameFilter() {
			@Override
			public boolean accept(File current, String name) {
				return new File(current, name).isDirectory();
			}
		});

		List<String> decryptedProfileList = new ArrayList<>();
		if (profileDirectories == null) {
			throw new InvalidDataException("Profile directory does not exist");
		}
		for (String existingUser : profileDirectories) {
			if (new File(profilesFolder + "/" + existingUser + "/" + decryptedFolder).isDirectory()) {
				decryptedProfileList.add(existingUser);
			}
		}
		return decryptedProfileList;
	}

	/**
	 *
	 * @param username new username
	 * @param password password for the given username
	 * @param blockChainPrivateKey 
	 * @return Created Profile with given username and passowrd
	 * @throws Exception
	 * @throws InvalidDataException Username or password is not valid, either null or empty
	 */
	public Profile createProfileData(String username, String password, String blockChainPrivateKey) throws Exception, InvalidDataException {
		if (username == null || username.isEmpty())
			throw new InvalidDataException("Username is not defined");
		if (password == null || password.isEmpty())
			throw new InvalidDataException("Password is not defined");

		List<String> profileList = getProfileList();
		List<String> profileListDecrypted = getProfileListDecrypted();
		if (profileList != null && profileList.contains(username)
				|| profileListDecrypted != null && profileListDecrypted.contains(username))
			throw new InvalidDataException("Profile with this username already exists");

		if (profileList == null || profileList.isEmpty() || !profileList.contains(username)) {
			Profile profile = new Profile(username, blockChainPrivateKey);
			passwordStorage.put(username, password);
			File decryptedFolderProfile = new File(profilesFolder + username + "/" + decryptedFolder);
			if (!decryptedFolderProfile.isDirectory())
				decryptedFolderProfile.mkdirs();
			String userKeyPath = getUserKeyDecryptedPath(username);
			RSA rsa = new RSA(userKeyPath, publicKeyName, privateKeyName);
			profile.setRsa(rsa);
			saveProfileData(profile);
			return profile;

		} else {
			return loadProfileData(username, password);
		}
	}

	private boolean checkIfAllDecryptedFilesExist(String username) {

		return new File(getUserKeyDecryptedPath(username) + publicKeyName).exists()
				&& new File(getUserKeyDecryptedPath(username) + privateKeyName).exists()
				&& new File(getProfileDecryptedPath(username)).exists();
	}

	private boolean checkIfAllFilesExist(String username) {

		return new File(getUserKeyPath(username) + publicKeyName).exists()
				&& new File(getUserKeyPath(username) + privateKeyName).exists()
				&& new File(getProfilePath(username)).exists();
	}

	/**
	 * Load a single profile
	 * 
	 * @param username username of the profile
	 * @param password password of the profile
	 * @return The profile of the given user, when password is correct, otherwise it
	 *         is null
	 * @throws Exception
	 * @throws InvalidDataException when profile does not exists
	 */
	public Profile loadProfileData(String username, String password) throws Exception {
		passwordStorage.put(username, password);
		List<String> profileList = getProfileList();
		if (profileList == null || profileList.isEmpty() || !profileList.contains(username)) {
			throw new InvalidDataException("Profile folder does not exist");
		} else {
			Profile profile = null;
			if (checkIfAllDecryptedFilesExist(username)) {
				@SuppressWarnings("rawtypes")
				DeserializerJSON deserializedProfile = new DeserializerJSON(getProfileDecryptedPath(username),
						Profile.class);
				profile = (Profile) deserializedProfile.run();

			} else if (checkIfAllFilesExist(username)) {
				decryptProfileFiles(username, password);

				@SuppressWarnings("rawtypes")
				DeserializerJSON deserializedProfile = new DeserializerJSON(getProfileDecryptedPath(username),
						Profile.class);
				profile = (Profile) deserializedProfile.run();
			}
			if(profile == null) {
				throw new InvalidDataException("Profile does not exist on storage");
			}
			String userKeyPath = getUserKeyDecryptedPath(username);
			RSA rsa = new RSA(userKeyPath, publicKeyName, privateKeyName);
			profile.setRsa(rsa);
			return profile;
		}
	}

	/**
	 * Saves a single profile
	 * 
	 * @param profile A profile, which shall be saved
	 */
	public void saveProfileData(Profile profile) throws IOException {
		SerializerJSON profileToSerialize = new SerializerJSON(getProfileDecryptedPath(profile.getUsername()), profile);
		profileToSerialize.run();
	}

	/**
	 * Lists all available profiles
	 * 
	 * @return List of all available profiles
	 */
	public List<String> getProfileList() throws InvalidDataException {

		File file = new File(profilesFolder);
		String[] directories = file.list(new FilenameFilter() {
			@Override
			public boolean accept(File current, String name) {
				return new File(current, name).isDirectory();
			}
		});

		if (directories != null) {
			Arrays.sort(directories);
			return Arrays.asList(directories);
		}
		throw new InvalidDataException("Profile directory does not exist");
	}

	/**
	 * All Profiles have to be saved before, otherwise the profiles data will get
	 * lost
	 * @throws IOException 
	 * @throws InvalidAlgorithmParameterException 
	 * @throws IllegalBlockSizeException 
	 * @throws BadPaddingException 
	 * @throws NoSuchPaddingException 
	 * @throws NoSuchAlgorithmException 
	 * @throws InvalidKeyException 
	 * 
	 * @throws Exception
	 */
	public void closeProfiles()
			throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException,
			BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException,
			IOException, InvalidDataException {

		// encrypt all opened profiles
		for (String profileName : getProfileListDecrypted()) {
			if(passwordStorage.containsKey(profileName)) {
				encryptProfileFiles(profileName, passwordStorage.get(profileName));
	
				// delete all decrypted profiles
				FileUtils.deleteDirectory(new File(profilesFolder + profileName + "/" + decryptedFolder));
			}
		}
	}

	/**
	 * Deletes one profile
	 * 
	 * @param username
	 * @throws IOException
	 */
	public void deleteProfileData(String username) throws IOException {
		FileUtils.deleteDirectory(new File(profilesFolder + username));
	}

	/**
	 * Provides public and private key of a user for the User itself
	 * 
	 * @param username which is in a profile
	 * @return RSA instance to access public and private key
	 * @throws InvalidKeySpecException
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 * @throws IOException
	 */
	public RSA getPublicPrivateKeyInstanceUser(String username)
			throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IOException {
		String userKeyPath = getUserKeyDecryptedPath(username);
		return new RSA(userKeyPath, publicKeyName, privateKeyName);
	}
}
