package com.aslan.sfdc.connect.credentials;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Element;

import com.aslan.util.CapstormBestPracticeFileLocator;

/**
 * Database credentials instance used when:
 * - Connecting directly to a database using a username and password.
 * - Storing password information in a Keepass database.
 */
public class KeepassJDBCUserPasswordCredentials extends AbstractJDBCUserPasswordCredentials {

	private static Log log = LogFactory.getLog(KeepassJDBCUserPasswordCredentials.class);

	/*
	 * Look for a named credential in the KEEPASS_FILE_NAME file, using
	 * the command line tool KEEPASS_SCRIPT.
	 * 
	 * The Keepass script needs to be a kpcli perl script, version 3.2.
	 * 
	 * For more information including setup instructions, see:
	 * http://kpcli.sourceforge.net/
	 */
	private static final String KEEPASS_FILE_NAME="passwords.kdbx";
	private static final String KEEPASS_SCRIPT="kpcli.pl";

	/*
	 * Use the standard Keepass Username & Password fields when fetching
	 * username/password.
	 * 
	 * Script Output follows the format:
	 * 
	 * Path: /path/in/passwords.kdbx
	 * Title: password-entry-title
	 * Uname: database-username
	 * Pass: database-password
	 * URL: 
	 * Notes: 
	 * 
	 * Followed by a section for arbitrary string values, formatted
	 * in one of two ways:
	 * 
	 * String Values: 
	 *         1) Some-Key = some-value
	 *         2) Instance Purpose = Testing environment for Project Maraca
	 *
	 * ------ OR -------
	 *
	 * Strgs: Some-Key = some-value
	 */
	private static Pattern USERNAME_PATTERN = Pattern.compile("Uname: (.*)");
	private static Pattern PASSWORD_PATTERN = Pattern.compile("Pass: (.*)");

	/*
	 * Keys used to save / restore state in the Capstorm save-file.
	 */
	private static final String ATTR_PASSWORD="keepass_password";
	private static final String ATTR_ENTRY="keepass_entry";

	// Keepass file & command-line tool locations
	private File keepassFile = null;
	private File keepassScript = null;

	// Keepass password and entry path
	private String password = "";
	private String entry = "";

	private JPasswordField passwordEditor;
	private JTextField entryEditor;

	/*
	 * Database username, password.
	 */
	private String dbUser, dbPassword;

	/*
	 * Used to determine whether or not cached credentials can be reused.
	 */
	private String fingerprint;

	public KeepassJDBCUserPasswordCredentials() {

		/*
		 * Look for the Keepass file and command-line script in Capstorm's
		 * standard locations (i.e. the same locations searched for the customer key)
		 */
		keepassFile = CapstormBestPracticeFileLocator.findFile(KEEPASS_FILE_NAME);
		keepassScript = CapstormBestPracticeFileLocator.findFile(KEEPASS_SCRIPT);
	}

	/**
	 * Update the Keepass file password.
	 * 
	 * This method is used when loading a save file.
	 * 
	 * @param password new Keepass file password.
	 */
	public void setKeepassPassword(String password) {
		this.password = password;
		if(null != passwordEditor) {
			passwordEditor.setText(password);
		}
	}

	/**
	 * Update the Keepass password entry name.
	 * 
	 * This method is used when loading a save file.
	 * 
	 * @param entry new Keepass password entry name.
	 */
	public void setEntry(String entry) {
		this.entry = entry;
		if(null != entryEditor) {
			entryEditor.setText(entry);
		}
	}

	/**
	 * @return the password used to access the Keepass file.
	 */
	public String getKeepassPassword() {
		return password;
	}

	/**
	 * @return the Keepass password entry name.
	 */
	public String getEntry() {
		return entry;
	}

	/**
	 * Run the Keypass script, fetching the username / password
	 * from the named credential.
	 * @throws Exception 
	 */
	private void readFromKeepass() throws Exception {

		/*
		 * Run the Keepass script, fetching the named entry.
		 */
		Process p;
		BufferedReader procOut;
		int retCode;
		File tmpPass = null;
		OutputStream fOut = null;
		try {

			tmpPass = File.createTempFile("capstorm", "tmp");
			fOut = new FileOutputStream(tmpPass);
			fOut.write(password.getBytes());
			fOut.close();
			ProcessBuilder pb = new ProcessBuilder(
					"perl", keepassScript.getAbsolutePath(), 
					"-kdb", keepassFile.getAbsolutePath(), 
					"--pwfile", tmpPass.getAbsolutePath(), 
					"--command", "show -f "+entry
					);
			pb.redirectErrorStream(true);

			p = pb.start();

			procOut = new BufferedReader(new InputStreamReader(p.getInputStream()));
			retCode = p.waitFor();
		} finally {
			if(null != tmpPass) {
				if(null != fOut) {
					fOut.close();
				}
				tmpPass.delete();
			}
		}
		if(0 != retCode) {
			log.info("Error reading from keepass file, return code: "+retCode);
			throw new Exception("Error reading from keepass file, return code: "+retCode);
		}
		String line = null;

		/*
		 * Parse the script's output.
		 */
		dbUser = "";
		dbPassword = "";
		while((line = procOut.readLine()) != null) {
			Matcher uNameMatcher = USERNAME_PATTERN.matcher(line);
			if(uNameMatcher.find()) {
				dbUser = uNameMatcher.group(1);
			}
			Matcher passMatcher = PASSWORD_PATTERN.matcher(line);
			if(passMatcher.find()) {
				dbPassword = passMatcher.group(1);
			}
		}
		if(StringUtils.isBlank(dbUser) && StringUtils.isBlank(dbPassword)) {
			log.info("No keepass entry found for "+entry);
			throw new Exception("No keepass entry found for "+entry);
		}
	}

	/**
	 * Returns the database username, as found in the Keepass file.
	 * @throws Exception 
	 */
	@Override
	public String getUsername() throws Exception {
		if(fingerprint != getFingerprint()) {
			readFromKeepass();
			fingerprint = getFingerprint();
		}
		return dbUser;
	}

	/**
	 * Returns the database password, as found in the Keepass file.
	 * @throws Exception 
	 */
	@Override
	public String getPassword() throws Exception {
		if(fingerprint != getFingerprint()) {
			readFromKeepass();
			fingerprint = getFingerprint();
		}
		return dbPassword;
	}

	/**
	 * Saves the UI's state information to the Capstorm savefile, optionally
	 * including the Keepass file password.
	 */
	@Override
	public void saveState(Element ele, boolean savePasswords) {
		saveXMLAttribute(ele, ATTR_ENTRY, getEntry());
		if(savePasswords) {
			saveXMLAttribute(ele, ATTR_PASSWORD, encrypt(getKeepassPassword()));
		}
	}

	/**
	 * Loads the UI's state from a Capstorm savefile.
	 */
	@Override
	public void restoreState(Element ele) {
		String loadEntry = loadXMLAttribute(ele, ATTR_ENTRY);
		setEntry(loadEntry==null?"":loadEntry);

		String loadPassword = loadXMLAttribute(ele, ATTR_PASSWORD);
		setKeepassPassword(loadPassword==null?"":decrypt(loadPassword));

	}

	/**
	 * Adds the Keepass file password & password entry name fields to
	 * the Capstorm UI.
	 * 
	 * When inputs are updated to make a connection attempt possible, 
	 * the UI informs the application that the  "Test Connection" 
	 * button should be enabled via "fireCredentialsComplete()"
	 */
	@Override
	public int injectIntoUI(JPanel panel, int startRow) {

		passwordEditor = new JPasswordField(password);;
		entryEditor = new JTextField(entry);

		passwordEditor.getDocument().addDocumentListener(new DocumentListener() {
			@Override
			public void removeUpdate(DocumentEvent e) {
				changedUpdate(e);
			}
			@Override
			public void insertUpdate(DocumentEvent e) {
				changedUpdate(e);
			}

			@Override
			public void changedUpdate(DocumentEvent e) {
				password = new String(passwordEditor.getPassword());
				if(isComplete()) {
					fireCredentialsComplete(true);
				} else {
					fireCredentialsComplete(false);
				}
			}
		});

		entryEditor.getDocument().addDocumentListener(new DocumentListener() {
			@Override
			public void removeUpdate(DocumentEvent e) {
				changedUpdate(e);
			}
			@Override
			public void insertUpdate(DocumentEvent e) {
				changedUpdate(e);
			}

			@Override
			public void changedUpdate(DocumentEvent e) {
				entry = entryEditor.getText();
				if(isComplete()) {
					fireCredentialsComplete(true);
				} else {
					fireCredentialsComplete(false);
				}
			}
		});

		addRowToUI("Password", 
				"Keepass file password."
				, passwordEditor, panel, startRow++);
		addRowToUI("Entry", 
				"Path to the password entry in Keepass<br>"
						+"For example, a password entry named 'database' in the folder 'Network',<br>"
						+"with the Network folder located in the root folder named 'passwords',<br>"
						+"would have an entry of '/passwords/Network/database'",
						entryEditor, panel, startRow++);

		return super.injectIntoUI(panel, startRow);
	}

	/**
	 * Return the Fingerprint used to determine whether or not cached credentials
	 * and sessions can be reused.
	 */
	@Override
	public String getFingerprint() {
		return StringUtils.join(new String[] {getEntry(), getKeepassPassword()}, ":");
	}

	/**
	 * Return true if the credential information provided is enough to attempt to make a connection.
	 * Return false otherwise.
	 */
	@Override
	public boolean isComplete() {
		if(StringUtils.isNotBlank(getEntry()) && StringUtils.isNotBlank(getKeepassPassword())) {
			return true;
		}
		return false;
	}

	/**
	 * Wipe out all user data for this credential.
	 */
	@Override
	public void clear() {
		setKeepassPassword("");
		setEntry("");
	}

	/**
	 * Only show the Keepass credential option if:
	 * - The passwords.kdbx file is available.
	 * - The Keepass command-line tool is available.
	 */
	@Override
	public boolean isAvailable() {
		if(null == keepassFile || null == keepassScript) {
			return false;
		}
		return true;
	}

	/**
	 * Invoked when the application detects that passwords are or are not required.
	 * 
	 * The default is to require credentials -- the only situation where they may not
	 * be required is when using SQL/Server's Integrated Security for authentication.
	 */
	@Override
	public void setRequired(boolean required) {
		; // Not Implemented for Keepass password storage.
	}
}
