package net.prominic.domino.vagrant;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.json.JSONObject;
import org.json.JSONTokener;

import lotus.domino.*;

/** 
 * Register and additional Domino server from the local server.
 * Expected to run on a hcl_domino_standalone_provisioner Vagrant instance.
 * @see https://help.hcltechsw.com/dom_designer/12.0.2/basic/H_EXAMPLE_REGISTERNEWSERVER_METHOD_JAVA.html
 */
public class CreateUser extends AgentBase {
 
	private static final String APP_NAME = "CreateUser";

	/** Reuse the generated properties file for CrossCertifyNotesID by default */
	protected static final String DEFAULT_PROPERTIES_FILE = "CrossCertifyNotesID.properties";
	protected static String dataDirectory = null;
	protected static String certID = null;
	protected static String settingsFile = null;
	// protected static String aclTemplate = null;
	protected static boolean debugMode = true;

 
	public static void main(String[] args) {
		Session session = null;
		DateTime dt = null;
		FileInputStream fis = null;
		Database namesDatabase = null;
		View userView = null;
		IDVault idVault = null;
		try {
			log("Application '" + APP_NAME + "' started.");
			
			
			NotesThread.sinitThread();
			
			loadSharedProperties();

			// The JSON file used for Domino server setup can also be used for for this configuration
			fis = new FileInputStream(settingsFile);
			JSONObject json = (JSONObject)new JSONTokener(fis).nextValue();
			JSONObject serverSetup = json.getJSONObject("serverSetup");
			JSONObject serverConfig = serverSetup.getJSONObject("server");

			
			// Arguments:
			if (args.length < 1) {
				throw new Exception("No properties file specified for the new user.");
			}
			String userPropertiesFileName = args[0];
			File userPropertiesFile = new File(userPropertiesFileName);
			if (!userPropertiesFile.exists()) {
				throw new Exception("Could not find file '" + userPropertiesFileName + "'.");
			}
			log("Loading additional server properties:  '" + userPropertiesFile.getAbsolutePath() + "'.");
			Properties userProperties = new Properties();
			try {
				fis = new FileInputStream(userPropertiesFile);
				userProperties.load(fis);
			}
			catch (Exception ex) {
				throw new Exception("Could not load properties file '" + userPropertiesFile.getAbsolutePath() + "'." );
			}
			finally {
				if (null != fis) {
					try {
						fis.close();
					}
					catch (Exception ex) {
						// ignore
					}
				}
			}
			
			// Read from server configuration:
			String domainName = serverConfig.getString("domainName");
			String server = serverConfig.getString("name");
			String certIDPassword = serverSetup.getJSONObject("org").getString("certifierPassword");  // not in original steps below
			

			// load properties from user configuration
			String firstName = readRequiredProperty(userProperties, "firstName");
			String lastName = readRequiredProperty(userProperties, "lastName");
			String shortName = readRequiredProperty(userProperties, "shortName");
			String userPassword = readRequiredProperty(userProperties, "password");
			String idFile = readRequiredProperty(userProperties, "idFile");
			
			
			// optional properties - normally shouldn't be set
            boolean deployToIDVault = readBooleanProperty(userProperties, "deployToIDVault", true);
            String middleInitial = userProperties.getProperty("middleInitial", "");
            String location = userProperties.getProperty("location", "");
            String comment = userProperties.getProperty("comment", "Generated by CreateUser.jar");
            String mailFilePath = userProperties.getProperty("mailFilePath", ""); //"mail/" + shortName + ".db");
            String mailInternetAddress = userProperties.getProperty("mailInternetAddress", ""); //shortName + "@" + domain);
            String forward = userProperties.getProperty("forward", "");
            String expirationDaysStr = userProperties.getProperty("expirationDays", "30");
            String minPasswordLengthStr = userProperties.getProperty("minPasswordLength", "1");  // password validated separately
            boolean northAmerican = readBooleanProperty(userProperties, "northAmerican", true);
            String orgUnitName = userProperties.getProperty("orgUnitName", "");
            String registrationLog = userProperties.getProperty("registrationLog", "log.nsf");
            boolean updateAddressBook = readBooleanProperty(userProperties, "updateAddressBook", true);
            boolean storeIDInAddressBook = readBooleanProperty(userProperties, "storeIDInAddressBook", true);
            boolean syncInternetPassword = readBooleanProperty(userProperties, "syncInternetPassword", true);
            
			// Check that ID file does not exist
			File idFileTest = new File(idFile);
			if (idFileTest.exists()) {
				throw new Exception("Target ID file already exists: '" + idFile + "'.");
			}
            
            boolean createMailDB = false;
            if (null != mailFilePath && !mailFilePath.trim().isEmpty()) {
            		createMailDB = true;
            }
            // validation
            if (null != mailFilePath && !mailFilePath.trim().isEmpty()) {
            		File mailFilePathTest = new File(mailFilePath);
            		if (mailFilePathTest.exists()) {
            			throw new Exception("Mail file already exists at '" + mailFilePath + "'.");
        			}

					throw new Exception("Mail files are not supported currently.");
					// TODO:  mail file needs to be relative to data directory
					// TODO:  java_user does not have permissions for %datadir%/mail
    			}
            int expirationDays = 30;
            try {
            		expirationDays = Integer.parseInt(expirationDaysStr);
            }
            catch (NumberFormatException ex) {
            		throw new Exception("Invalid integer value for property 'expirationDays':  '" + expirationDaysStr + "'.");
            }
            int minPasswordLength = 1;
            try {
            		minPasswordLength = Integer.parseInt(minPasswordLengthStr);
            }
            catch (NumberFormatException ex) {
            		throw new Exception("Invalid integer value for property 'minPasswordLength':  '" + minPasswordLengthStr + "'.");
            }
			
			
			
			
			// Create the session
			// currently we are using the admin user for actions like this
			String adminPassword = serverSetup.getJSONObject("admin").getString("password");
			String sessionServer = null; // local server
			String sessionUser = null;  // default user
			debug("NotesFactory.createSession");
			session = NotesFactory.createSession(sessionServer, args, sessionUser, adminPassword);
			log("Running on Notes Version: '" + session.getNotesVersion() + "'.");
			
			// Uniqueness verification:
			namesDatabase = session.getDatabase(server, "names.nsf", false);
			if (null == namesDatabase || !namesDatabase.isOpen()) {
				throw new Exception("Could not open names database:  " + server + "!names.nsf");
			}
			// Compute the full username and check for uniqueness
			ArrayList<String> namesToCheck = new ArrayList<String>();
			userView = namesDatabase.getView("($Users)");
			if (null == userView) { 
				throw new Exception("Could not open user view");
			}
			String computedSuffix = domainName;
			if (null != orgUnitName && !orgUnitName.trim().isEmpty()) {
				computedSuffix = orgUnitName + "/" + domainName;
			}
			String computedName = firstName + " " + lastName;
			namesToCheck.add(computedName);
			if (null != middleInitial && !middleInitial.trim().isEmpty()) {
				computedName = firstName + " " + middleInitial + " " + lastName;
				namesToCheck.add(computedName);
			}
			String fullName = computedName + "/" + computedSuffix;
			namesToCheck.add(fullName);
			assertUnique("shortName", userView, shortName);
			for (String nameToCheck : namesToCheck) {
				assertUnique("Full Name", userView, nameToCheck);
			}

			// check IDVault as well if it is used
			if (deployToIDVault) {
				idVault = session.getIDVault();
				if (idVault == null) {
					throw new Exception("Unable to access ID Vault.");
				}

				// Check if ID is already in vault
				if (idVault.isIDInVault(fullName, server)) {
					throw new Exception("User '" + fullName + " already exists in the ID Vault.");
				}
			}

			
            // TODO:  mailInternetAddress - find a view for this.
			

			Registration reg = session.createRegistration();
			
			reg.setCreateMailDb(createMailDB);
			reg.setRegistrationServer(server);
			reg.setCertifierIDFile(certID);
			
			dt = session.createDateTime(new Date());
			dt.adjustDay(expirationDays);
			System.out.print("expirationDate:"+dt.toString());
			reg.setExpiration(dt);
			
			reg.setIDType(Registration.ID_HIERARCHICAL);
			reg.setMinPasswordLength(minPasswordLength); // password strength
			reg.setNorthAmerican(northAmerican);
			if(orgUnitName.length() > 0){
				reg.setOrgUnit(orgUnitName);
			}
			reg.setRegistrationLog(registrationLog);
			reg.setUpdateAddressBook(updateAddressBook);
			reg.setStoreIDInAddressBook(storeIDInAddressBook);
			reg.setShortName(shortName);
			reg.setSynchInternetPassword(syncInternetPassword);
			boolean successful = reg.registerNewUser(lastName, // last name
					idFile, // file to be created
					server, // mail server
					firstName, // first name
				middleInitial, // middle initial
				certIDPassword, // certifier password
				location, // location field
				comment, // comment field
				mailFilePath, // mail file
				forward, // forwarding domain
				userPassword); //user password
			if (successful) { 
				log("User created successfully.  Find the new ID file at '" + idFile + "'.");
			}
			else { 
				throw new Exception("Failed to create new user.");
			}

			// refresh the view
			debug("userView('($Users)').refresh()");
			userView.refresh();

			// force refresh of ($ServerAccess) to update access rights for new user
			View refreshView = null;
			String viewName = "($ServerAccess)";
			try {
				debug("namesDatabase.getView('" + viewName + "'");
				refreshView = namesDatabase.getView(viewName);
				if (null != refreshView) {
					debug("refreshView('($ServerAccess)').refresh()");
					refreshView.refresh();
				}
				else {
					log("Could not open view '" + viewName + "'.");
				}
			}
			catch (Exception ex) {
				log("Could not refresh view '" + viewName + "'.");
			}
			finally {
				if (null != refreshView) { refreshView.recycle(); }
			}

			if (deployToIDVault) {
                log("Uploading ID for user '" + fullName + "' to vault...");
                idVault.putUserIDFile(idFile, fullName, userPassword, server);

				// Verify the ID is now in the vault
				if (idVault.isIDInVault(fullName, server)) {
					String vaultServer = idVault.getServerName();
					log("SUCCESSFUL!");
					log("ID for user '" + fullName + "' is confirmed to be in the vault on server '" + vaultServer + "'.");
				} else {
					throw new Exception("Failed to verify ID in vault after upload/sync operation.");
				}
				log("Successfully uploaded ID to vault.");
			}

		}
		catch (Throwable throwable) {
			throwable.printStackTrace();
		}
		finally {
			try {
				if (null != dt) {  dt.recycle(); }
				if (null != idVault) {  idVault.recycle(); }
				if (null != userView) {  userView.recycle(); }
				if (null != namesDatabase) {  namesDatabase.recycle(); }
				if (null != session) {
					session.recycle();
				}
				if (null != fis) { fis.close(); }
			}
			catch (Exception ex) {
				ex.printStackTrace();
			}
			NotesThread.stermThread();
			log("Application '" + APP_NAME + "' completed.");
		}
	}

	/**
	 * Load the application properties, from the first available source here:<ul>
	 *   <li>The file configured by the <code>app.properties.file</code> property (set with <code>-Dapp.properties.file=%file%</code>)</li>
	 *   <li>The default file: <code>./CrossCertifyNotesID.properties</code>
	 *   <li>Default values defined in this class.
	 * </ul>
	 */
	public static void loadSharedProperties() {

		String propertiesFileName = System.getProperty("app.properties.file");
		debug ("propertiesFileName='" + propertiesFileName + "'.");
		if (null == propertiesFileName || propertiesFileName.isEmpty()) {
			propertiesFileName = DEFAULT_PROPERTIES_FILE;
		}

		Properties properties = new Properties();
		File propertiesFile = new File(propertiesFileName);
		if (propertiesFile.exists()) {
			log("Loading properties file '" + propertiesFile.getAbsolutePath() + "'.");
			FileInputStream fis = null;
			try {
				fis = new FileInputStream(propertiesFile);
				properties.load(fis);
			}
			catch (Exception ex) {
				log("Could not load properties file '" + propertiesFile.getAbsolutePath() + "'.  Using defaults..." );
			}
			finally {
				if (null != fis) {
					try {
						fis.close();
					}
					catch (Exception ex) {
						// ignore
					}
				}
			}
		}
		else {
			log("Properties file '" + propertiesFile.getAbsolutePath() + "' does not exist.  Using defaults...");
		}

		// read the properties
		dataDirectory = properties.getProperty("data.directory", "/local/notesdata");
		settingsFile = properties.getProperty("domino.setup.file", dataDirectory + "/setup.json");
		certID = properties.getProperty("cert.id.file", dataDirectory + "/cert.id");
		// aclTemplate = properties.getProperty("acl.template.file", "default_cross_certify_acl.json");
		// successFileName = properties.getProperty("output.file", DEFAULT_SUCCESS_FILE);
		String debugStr = properties.getProperty("debug", "false");
		if (null != debugStr && debugStr.equalsIgnoreCase("true")) {
			debugMode = true;
		}
		else {
			debugMode = false;
		}

	}
	
	/**
	 * Get the requested property from the Properties object.
	 * If the value is null or empty, throw an exception
	 * This is intended to be used for properties where defaulting the value would not make sense
	 * @param properties   the properties object
	 * @param key  the property key
	 * @throws Exception if the property is missing
	 */
	protected static String readRequiredProperty(Properties properties, String key) throws Exception {
		String value = properties.getProperty(key);
		if (null == value || value.trim().isEmpty()) {  // will I want to allow an empty value in some cases?
			throw new Exception("Missing value for property '" + key + "'.");
		}
		return value;
	}
	
	/**
	 * Get the requested property from the Properties object and parse it as a boolean value.
	 * If it is empty, use the provided defaultValue.
	 * If the value is not parseable as a boolean, throw an Exception
	 * @param properties   the properties object
	 * @param key  the property key
	 * @param defaultValue  This will be returned if the property is missing or invalid
	 * @throws Exception if the property is invalid
	 */
	protected static boolean readBooleanProperty(Properties properties, String key, boolean defaultValue) throws Exception {
		String value = properties.getProperty(key);
		if (null == value || value.trim().isEmpty()) {  // will I want to allow an empty value in some cases?
			return defaultValue;
		}
		
		// Boolean.parseBoolean treats anything besides "true" as false
		if (value.equalsIgnoreCase("true")) {
			return true;
		}
		else if (value.equalsIgnoreCase("false")) {
			return false;
		}
		else {
			throw new Exception("Invalid boolean value for property '" + key + "':  '" + value + "'.");
		}
	}
	
	/**
	 * Determine the server administrator by reading notes.ini
	 * This requires that {@link #dataDirectory} is set.
	 * @return  the raw name of the server admin for the local Domino installation
	 * @throws Exception if the server administrator could not be looked up
	 */
	protected static String getServerAdmin() throws Exception {
		String serverAdmin = null;
		BufferedReader reader = null;
		String iniPath = dataDirectory + File.separator + "notes.ini";
		
		// Admin=CN=Demo Admin/O=TEST1201
		Pattern adminPattern = Pattern.compile("admin\\s*=\\s*(\\S.*)$", Pattern.CASE_INSENSITIVE);

		try {
			reader = new BufferedReader(new FileReader(iniPath));
			String line = reader.readLine();

			while (line != null) {
				Matcher matcher = adminPattern.matcher(line);
				if (matcher.matches()) {
					serverAdmin = matcher.group(1);
					serverAdmin = serverAdmin.trim();
					break;
				}
				// else:  continue searching
				
				
				// read next line
				line = reader.readLine();
			}

			reader.close();
		} 
		finally {
			if (null != reader) { reader.close(); }
		}
		
		if (null == serverAdmin || serverAdmin.trim().isEmpty()) {
			throw new Exception("Failed to lookup server admin.");
		}
		return serverAdmin;
	}
	
	/*
	 * Check if the provided value is unique.
	 * This value will be checked against the first sorted key in the provided view.
	 * The check is case-insensitive
	 * @param key  the key that the value is associated with.  Used only for logging and error messages
	 * @param keyView  a view sorted by other values of the provided key
	 * @param value  the value to check
	 * @throws Exception if the value is not unique, or it checking failed.
	 */
	protected static void assertUnique(String key, View keyView, String value) throws Exception {
		debug("Checking " + key + " for uniqueness:  " + value);
		Document matchingDoc = null;
		try {
			// exact match, except for case
			matchingDoc = keyView.getDocumentByKey(value, true);
			if (null != matchingDoc) {
				throw new Exception("Duplicate value:  " + key + " = '" + value  + "'.");
			}
		}
		catch (NotesException ex) {
			throw new Exception("Unable to check uniqueness for " + key + ":  ", ex);
		}
		finally {
			if (null != matchingDoc) { matchingDoc.recycle(); }
		}
	}



	protected static void log(String message) {
		System.out.println(message);
	}
	protected static void debug(String message) {
		final String debugPrefix = "	(debug)";
		if (debugMode) {
			log(debugPrefix + message);
		}
	}
	protected static void log(Throwable t) {
		t.printStackTrace(System.out);
	}
}
