package freenet.clients.http.wizardsteps;

import java.io.IOException;

import freenet.clients.http.ExternalLinkToadlet;
import freenet.clients.http.FirstTimeWizardToadlet;
import freenet.clients.http.SecurityLevelsToadlet;
import freenet.l10n.NodeL10n;
import freenet.node.MasterKeysFileSizeException;
import freenet.node.MasterKeysWrongPasswordException;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.SecurityLevels;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.api.HTTPRequest;
import freenet.support.io.FileUtil;
import freenet.support.io.FileUtil.OperatingSystem;

/**
 * Allows the user to set the physical security level.
 */
public class SECURITY_PHYSICAL implements Step {

	private final NodeClientCore core;

	private enum PASSWORD_PROMPT {
		SET_BLANK, //Requested new password was blank
		DECRYPT_WRONG, //Decryption password was wrong
		DECRYPT_BLANK,  //Decryption password was blank
		SET_NO_MATCH //The new password pair that was requested does not match.
	}

	/**
	 * Constructs a new SECURITY_PHYSICAL GET handler.
	 * @param core used to check or set the current security level and password.
	 */
	public SECURITY_PHYSICAL(NodeClientCore core) {
		this.core = core;
	}

	@Override
	public void getStep(HTTPRequest request, PageHelper helper) {

		if (request.isParameterSet("error")) {
			if (errorHandler(request, helper)) {
				//Error page generated successfully.
				return;
			}
			//Problem generating error page; generate default.
		}

		HTMLNode contentNode = helper.getPageContent(WizardL10n.l10n("physicalSecurityPageTitle"));
		HTMLNode infoboxContent = helper.getInfobox("infobox-normal",
		        WizardL10n.l10nSec("physicalThreatLevelShort"), contentNode, null, false);
		infoboxContent.addChild("p", WizardL10n.l10nSec("physicalThreatLevel"));

		HTMLNode form = helper.addFormChild(infoboxContent, ".", "physicalSecurityForm");
		HTMLNode div = form.addChild("div", "class", "opennetDiv");
		String controlName = "security-levels.physicalThreatLevel";
		HTMLNode swapWarning = div.addChild("p").addChild("i");
		NodeL10n.getBase().addL10nSubstitution(swapWarning, "SecurityLevels.physicalThreatLevelTruecrypt",
		        new String[]{"bold", "truecrypt"},
		        new HTMLNode[]{HTMLNode.STRONG,
		                HTMLNode.linkInNewWindow(ExternalLinkToadlet.escape("http://www.truecrypt.org/"))});
		OperatingSystem os = FileUtil.detectedOS;
		div.addChild("p", NodeL10n.getBase().getString("SecurityLevels.physicalThreatLevelSwapfile",
		        "operatingSystem",
		        NodeL10n.getBase().getString("OperatingSystemName."+os.name())));
		if(os == FileUtil.OperatingSystem.Windows) {
			swapWarning.addChild("#", " " + WizardL10n.l10nSec("physicalThreatLevelSwapfileWindows"));
		}
		for(SecurityLevels.PHYSICAL_THREAT_LEVEL level : SecurityLevels.PHYSICAL_THREAT_LEVEL.values()) {
			HTMLNode input;
			input = div.addChild("p").addChild("input",
			        new String[] { "type", "name", "value" },
			        new String[] { "radio", controlName, level.name() });
			input.addChild("b", WizardL10n.l10nSec("physicalThreatLevel.name." + level));
			input.addChild("#", ": ");
			NodeL10n.getBase().addL10nSubstitution(input, "SecurityLevels.physicalThreatLevel.choice."+level, new String[] { "bold" }, new HTMLNode[] { HTMLNode.STRONG });
			if(level == SecurityLevels.PHYSICAL_THREAT_LEVEL.HIGH &&
			        core.node.securityLevels.getPhysicalThreatLevel() != level) {
				// Add password form on high security if not already at high security.
				HTMLNode p = div.addChild("p");
				p.addChild("label", "for", "passwordBox", WizardL10n.l10nSec("setPasswordLabel")+":");
				p.addChild("input", new String[] { "id", "type", "name" }, new String[] { "passwordBox", "password", "masterPassword" });
				// Confirm password box
				p.addChild("label", "for", "confirmPasswordBox", WizardL10n.l10nSec("confirmPasswordLabel")+":");
				p.addChild("input", new String[] { "id", "type", "name" }, new String[] { "confirmPasswordBox", "password", "confirmMasterPassword" });
			}
		}
		div.addChild("#", WizardL10n.l10nSec("physicalThreatLevelEnd"));
		form.addChild("input",
		        new String[] { "type", "name", "value" },
		        new String[] { "submit", "back", NodeL10n.getBase().getString("Toadlet.back")});
		form.addChild("input",
		        new String[] { "type", "name", "value" },
		        new String[] { "submit", "next", NodeL10n.getBase().getString("Toadlet.next")});
	}

	/**
	 * Internal error handler wrapper with the hope of making the code more readable.
	 * @param request defines which error and information about it
	 * @param helper creates page, infoboxes, forms.
	 * @return whether an error page was successfully generated.
	 */
	private boolean errorHandler(HTTPRequest request, PageHelper helper) {
		String physicalThreatLevel = request.getParam("newThreatLevel");
		SecurityLevels.PHYSICAL_THREAT_LEVEL newThreatLevel = SecurityLevels.parsePhysicalThreatLevel(physicalThreatLevel);
		String error = request.getParam("error");

		if (error.equals("pass")) {
			//Password prompt requested
			PASSWORD_PROMPT type;
			try {
				type = PASSWORD_PROMPT.valueOf(request.getParam("type"));
			}  catch (IllegalArgumentException e) {
				//Render the default page if unable to parse password prompt type.
				return false;
			}

			final String pageTitleKey;
			final String infoboxTitleKey;
			final boolean forDowngrade;
			final boolean forUpgrade;
			final boolean wasWrong = type == PASSWORD_PROMPT.DECRYPT_WRONG;

			switch (type) {
				case SET_BLANK:
					pageTitleKey = "passwordPageTitle";
					infoboxTitleKey = "enterPasswordTitle";
					forDowngrade = false;
					forUpgrade = true;
					break;
				case DECRYPT_WRONG:
					pageTitleKey ="passwordForDecryptTitle";
					infoboxTitleKey = "passwordWrongTitle";
					forDowngrade = false;
					forUpgrade = false;
					break;
				case DECRYPT_BLANK:
					pageTitleKey = "passwordForDecryptTitle";
					infoboxTitleKey = "passwordForDecryptTitle";
					forDowngrade = true;
					forUpgrade = false;
					break;
				case SET_NO_MATCH:
					pageTitleKey = "passwordPageTitle";
					infoboxTitleKey = "enterPasswordTitle";
					forDowngrade = false;
					forUpgrade = true;
					break;
				default:
					//Unanticipated value for type!
					return false;
			}

			HTMLNode contentNode = helper.getPageContent(WizardL10n.l10nSec(pageTitleKey));

			HTMLNode content = helper.getInfobox("infobox-error", WizardL10n.l10nSec(infoboxTitleKey),
			        contentNode, null, true);

			if (type == PASSWORD_PROMPT.SET_BLANK || type == PASSWORD_PROMPT.DECRYPT_BLANK) {
				content.addChild("p", WizardL10n.l10nSec("passwordNotZeroLength"));
			} else if (type == PASSWORD_PROMPT.SET_NO_MATCH) {
				content.addChild("p", WizardL10n.l10nSec("passwordsDoNotMatch"));
			}

			HTMLNode form = helper.addFormChild(content, ".", "masterPasswordForm");

			SecurityLevelsToadlet.generatePasswordFormPage(wasWrong, form, content, forDowngrade, forUpgrade,
			        newThreatLevel.name(), null);

			addBackToPhysicalSeclevelsButton(form);
			return true;
		} else if (error.equals("corrupt")) {
			//Password file corrupt
			SecurityLevelsToadlet.sendPasswordFileCorruptedPageInner(helper, core.node.getMasterPasswordFile().getPath());
			return true;
		} else if (error.equals("delete")) {
			SecurityLevelsToadlet.sendCantDeleteMasterKeysFileInner(helper, core.node.getMasterPasswordFile().getPath(), newThreatLevel.name());
			return true;
		}

		//Error type was not recognized.
		return false;
	}

	public SecurityLevels.PHYSICAL_THREAT_LEVEL getCurrentLevel() {
		return core.node.securityLevels.getPhysicalThreatLevel();
	}

	@Override
	public String postStep(HTTPRequest request) throws IOException {
		final String errorCorrupt = FirstTimeWizardToadlet.WIZARD_STEP.SECURITY_PHYSICAL+"&error=corrupt";
		String pass = request.getPartAsStringFailsafe("masterPassword", SecurityLevelsToadlet.MAX_PASSWORD_LENGTH);
		String confirmPass = request.getPartAsStringFailsafe("confirmMasterPassword", SecurityLevelsToadlet.MAX_PASSWORD_LENGTH);
		final boolean passwordIsBlank = pass.isEmpty() && confirmPass.isEmpty();
		final boolean passwordsDoNotMatch = !pass.equals(confirmPass);

		String physicalThreatLevel = request.getPartAsStringFailsafe("security-levels.physicalThreatLevel", 128);
		SecurityLevels.PHYSICAL_THREAT_LEVEL oldThreatLevel = core.node.securityLevels.getPhysicalThreatLevel();
		SecurityLevels.PHYSICAL_THREAT_LEVEL newThreatLevel = SecurityLevels.parsePhysicalThreatLevel(physicalThreatLevel);
		if (FirstTimeWizardToadlet.shouldLogMinor()) {
			Logger.minor(this, "Old threat level: " + oldThreatLevel + " new threat level: " + newThreatLevel);
		}

		/*If the user didn't select a network security level before clicking continue, the selected
		* security level could not be determined, clicked back from a password error page, redirect to the main page.*/
		if (newThreatLevel == null || !request.isPartSet("security-levels.physicalThreatLevel") ||
		        request.isPartSet("backToMain")) {
			return FirstTimeWizardToadlet.WIZARD_STEP.SECURITY_PHYSICAL.name();
		}
		//Changing to high physical threat level: set password.
		if (newThreatLevel == SecurityLevels.PHYSICAL_THREAT_LEVEL.HIGH && oldThreatLevel != newThreatLevel) {
			if (passwordIsBlank) {
				// Must set the password to something non-blank.
				return promptPassword(newThreatLevel, PASSWORD_PROMPT.SET_BLANK);
			} else if (passwordsDoNotMatch) {
				//Must Confirm the password before setting it
				return promptPassword(newThreatLevel, PASSWORD_PROMPT.SET_NO_MATCH);
			} else {
				try {
					if(oldThreatLevel == SecurityLevels.PHYSICAL_THREAT_LEVEL.NORMAL ||
					        oldThreatLevel == SecurityLevels.PHYSICAL_THREAT_LEVEL.LOW) {
						core.node.changeMasterPassword("", pass, true);
					} else {
						core.node.setMasterPassword(pass, true);
					}
				} catch (Node.AlreadySetPasswordException e) {
					// Do nothing, already set a password.
				} catch (MasterKeysWrongPasswordException e) {
					throw new IOException("Incorrect password when changing from another level to high", e);
				} catch (MasterKeysFileSizeException e) {
					return errorCorrupt;
				}
			}
		}
		//Decreasing to low or normal from high: remove password.
		if ((newThreatLevel == SecurityLevels.PHYSICAL_THREAT_LEVEL.LOW || newThreatLevel == SecurityLevels.PHYSICAL_THREAT_LEVEL.NORMAL) &&
			        oldThreatLevel == SecurityLevels.PHYSICAL_THREAT_LEVEL.HIGH) {
			if (passwordIsBlank) {
				//Prompt for the old password, which is needed to decrypt
				return promptPassword(newThreatLevel, PASSWORD_PROMPT.DECRYPT_BLANK);
			} else if (core.node.getMasterPasswordFile().exists()) {
				//Old password for decryption specified.
				try {
					core.node.changeMasterPassword(pass, "", true);
				} catch (IOException e) {
					if(!core.node.getMasterPasswordFile().exists()) {
						// Ok.
						System.out.println("Master password file no longer exists, assuming this is deliberate");
					} else {
						System.err.println("Cannot change password as cannot write new passwords file: "+e);
						e.printStackTrace();
						throw new IOException("cantWriteNewMasterKeysFile", e);
					}
				} catch (MasterKeysWrongPasswordException e) {
					return promptPassword(newThreatLevel, PASSWORD_PROMPT.DECRYPT_WRONG);
				} catch (MasterKeysFileSizeException e) {
					return errorCorrupt;
				} catch (Node.AlreadySetPasswordException e) {
					System.err.println("Already set a password when changing it - maybe master.keys copied in at the wrong moment???");
				}
			}

		}
		//Maximum threat level: remove master keys file.
		if (newThreatLevel == SecurityLevels.PHYSICAL_THREAT_LEVEL.MAXIMUM) {
			try {
				core.node.killMasterKeysFile();
			} catch (IOException e) {
				return FirstTimeWizardToadlet.WIZARD_STEP.SECURITY_PHYSICAL+
				        "&error=delete&newThreatLevel="+newThreatLevel.name();
			}
		}
		setThreatLevel(newThreatLevel, oldThreatLevel);
		return FirstTimeWizardToadlet.WIZARD_STEP.NAME_SELECTION.name();
	}

	/**
	 * Internal utility function for displaying a password prompt.
	 * @param newThreatLevel the user-selected threat level, to be used in creating the form.
	 * @param type what type of prompt needed
	 * @return URL to display the requested page
	 */
	private String promptPassword(SecurityLevels.PHYSICAL_THREAT_LEVEL newThreatLevel, PASSWORD_PROMPT type) {
		if (type == PASSWORD_PROMPT.DECRYPT_WRONG) {
			System.err.println("Wrong password!");
		}
		StringBuilder destination = new StringBuilder(FirstTimeWizardToadlet.WIZARD_STEP.SECURITY_PHYSICAL+
		        "&error=pass&newThreatLevel=").append(newThreatLevel.name()).append("&type=").append(type.name());
		return destination.toString();
	}

	public void setThreatLevel(SecurityLevels.PHYSICAL_THREAT_LEVEL newThreatLevel, SecurityLevels.PHYSICAL_THREAT_LEVEL oldThreatLevel) throws IOException {
		core.node.securityLevels.setThreatLevel(newThreatLevel);
		core.storeConfig();
		try {
			core.node.lateSetupDatabase(null);
		} catch (MasterKeysWrongPasswordException e) {
			// Ignore, impossible???
			System.err.println("Failed starting up database while switching physical security level to "+newThreatLevel+" from "+oldThreatLevel+" : wrong password - this is impossible, it should have been handled by the other cases, suggest you remove master.keys");
		} catch (MasterKeysFileSizeException e) {
			System.err.println("Failed starting up database while switching physical security level to "+newThreatLevel+" from "+oldThreatLevel+" : "+core.node.getMasterPasswordFile()+" is too " + e.sizeToString());
		}
	}

	private void addBackToPhysicalSeclevelsButton(HTMLNode form) {
		form.addChild("p").addChild("input",
		        new String[] { "type", "name", "value" },
		        new String[] { "submit", "backToMain", WizardL10n.l10n("backToSecurityLevels")});
	}
}
