/**
 *  ---------
 * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
 * |#       #|
 * |#       #|  Copyright (c) 1999-2016 CardContact Software & System Consulting
 * |'##> <##'|  Andreas Schwier, 32429 Minden, Germany (www.cardcontact.de)
 *  ---------
 *
 *  This file is part of OpenSCDP.
 *
 *  OpenSCDP is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  OpenSCDP is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with OpenSCDP; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @fileoverview A service request model for registering a new token
 */

var ServiceRequestModel		= require('pki-as-a-service/service/ServiceRequestModel').ServiceRequestModel;
var Subject				= require('scsh/pki-db/Subject').Subject;
var RoleDAO				= require('scsh/pki-db/db/RoleDAO').RoleDAO;



/**
 * Data model for registering  a token
 *
 *
 * @constructor
 */
function RegisterMyTokenRequestModel(service, bo) {
	ServiceRequestModel.call(this, service, bo);
	this.validationResult = [];
}

RegisterMyTokenRequestModel.prototype = Object.create(ServiceRequestModel.prototype);
RegisterMyTokenRequestModel.constructor = RegisterMyTokenRequestModel;

exports.RegisterMyTokenRequestModel = RegisterMyTokenRequestModel;



RegisterMyTokenRequestModel.STATE_WRONG_ACTIVATION_CODE = "Wrong Activation Code";
RegisterMyTokenRequestModel.STATE_COMPLETED = "Token Registered";

RegisterMyTokenRequestModel.INITIAL_ACTIVATION_TRIES = 3;
RegisterMyTokenRequestModel.NO_OF_INITIAL_ACTIVATION_REQUESTS = 3;


RegisterMyTokenRequestModel.prototype.getForm = function(user) {
	if (this.form == undefined) {
		var changeableEmail = (this.isManagedBy(user) || (user.id == this.getOriginatorId())) && (this.bo.lifecycle != ServiceRequestModel.LIFECYCLE_TERMINATED);

		var displayemail = (this.model.email ? this.model.email: "");
		var label = "msg.registration.email";
		if (changeableEmail && this.model.newemail) {
			// New e-mail prepared, waiting for actual commit
			displayemail = this.model.newemail;
			label = "msg.registration.newemail";
		}

		this.form = [{
			id: "regdata",
			legend: "msg.registration.data",
			fields: [
				{ id: "token", label: "msg.registration.token", type: "text", size: 50, required: true, editable: false, value: (this.model.tokenId ? this.model.tokenId: "") },
				{ id: "email", label: label, type: "email", size: 50, required: true, editable: changeableEmail, value: displayemail  },
			]},
		];

		// Feature #227 Allow user to configure notification settings
		if ((this.bo.originatorId == user.id) && this.model.email && (this.bo.lifecycle != ServiceRequestModel.LIFECYCLE_TERMINATED)) {
			var subject = this.getOriginator();
			this.form.push(
				{
					id: "notifications",
					legend: "msg.registration.notifications",
					fields: [
						{ id: "allowSRNotification", type: "checkbox", value: "use this e-mail address to notify about life-cycle changes in service requests that I started or that I'm involved in", checked: subject.isSRNotificationAllowed(), editable: true},
						{ id: "allowUPDNotification", type: "checkbox",value: "use this e-mail address to inform about software updates for the SmartCard-HSM or the PKI-as-a-Service portal", checked: subject.isUPDNotificationAllowed(), editable: true}
					]
				}
			);
		}

	}

	return this.form;
}



RegisterMyTokenRequestModel.prototype.isManagedBy = function(user) {
	return (user.roles.indexOf(RoleDAO.ID_RegistrationManager) >= 0) || (this.getOriginatorId() && this.getOriginator().isManager(user));
}



RegisterMyTokenRequestModel.prototype.assignToRegistrationOfficer = function() {
	if (this.model.raRoleID) {
		this.assignToRole(this.model.raRoleID);
	} else {
		if (this.hasPredefinedRARole()) {
			this.assignToRole(RoleDAO.ID_RegistrationManager);
		}
	}
}



RegisterMyTokenRequestModel.prototype.hasPredefinedRARole = function() {
	var roleDAO = this.service.daof.getRoleDAO();
	return roleDAO.getRoleById(RoleDAO.ID_RegistrationManager) != null;
}



RegisterMyTokenRequestModel.prototype.getActionList = function(user) {
	if (this.actionList != undefined) {
		return this.actionList;
	}

	this.actionList = [];

	if (this.bo.lifecycle == ServiceRequestModel.LIFECYCLE_TERMINATED) {
		return this.actionList;
	}

	if (user.id == this.bo.originatorId) {
		// Feature #227 Allo user to configure notification settings
		this.actionList.push("action.save");
		if (this.model.newemail) {
			this.actionList.push("action.registration.change_email");
		}
		this.actionList.push("action.registration.terminate");
	} else if (this.isManagedBy(user)) {
		this.actionList.push("action.save");
		if (this.bo.lifecycle == ServiceRequestModel.LIFECYCLE_ERROR) {
			if (this.model.activationRequests <= 0) {
				this.actionList.push("action.registration.unblock_activation_requests");
			} else if (this.model.activationTries <= 0) {
				this.actionList.push("action.registration.unblock_activation_code");
			}
		}

		if (this.bo.lifecycle == ServiceRequestModel.LIFECYCLE_APPROVE) {
				this.actionList.push("action.approve");
		}

		if ((this.bo.lifecycle == ServiceRequestModel.LIFECYCLE_ERROR) || (this.bo.lifecycle == ServiceRequestModel.LIFECYCLE_APPROVE)) {
				this.actionList.push("action.registration.reject");
		}

		if (this.model.newemail) {
			this.actionList.push("action.registration.change_email");
		}

		this.actionList.push("action.registration.terminate");
	}

	return this.actionList;
}



/**
 * Helper function to check if the given email is valid
 *
 * @param {String} email the e-mail address
 */
RegisterMyTokenRequestModel.prototype.isValidEmail = function(email) {
	GPSystem.log(GPSystem.DEBUG, module.id, "isValidEmail( " + email + " )");

	if (email.length <= 0 ||
		(email.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+/) == null) ||
		(email.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+/)[0] != email)) {
			this.addMessage("Failed - Invalid e-mail address " + email);
			GPSystem.log(GPSystem.DEBUG, module.id, "Invalid e-mail address " + email);
			return false;
	}

	return true;
}



RegisterMyTokenRequestModel.prototype.checkEmail = function(fld, email) {
	GPSystem.log(GPSystem.DEBUG, module.id, "checkEmail( " + email + " )");

	// e-mail field is not changed
	if ((this.model.newemail == undefined) && (this.model.email == email)) {
		return true;
	}

	if (email.length <= 0 ||
		(email.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+/) == null) ||
		(email.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+/)[0] != email)) {
			this.addMessage("Failed - Invalid e-mail address " + email);
			fld.message = "Invalid e-mail address " + email;
			GPSystem.log(GPSystem.DEBUG, module.id, "Invalid e-mail address " + email);
			return false;
	}

	var subjectDAO = this.service.daof.getSubjectDAO();
	var subject = subjectDAO.getSubjectByEmail(email);

	if (subject) {
		var origid = this.getOriginatorId();
		if (origid) {
			// This is not a self-registration
			if (subject.id != origid) {
				fld.message = "The email " + email + " was already assigned. Please choose a different email.";
				return false;
			}

			// e-mail changed back to original e-mail stored for subject
			if ((this.bo.lifecycle != ServiceRequestModel.LIFECYCLE_VALIDATE) &&
				(this.bo.lifecycle != ServiceRequestModel.LIFECYCLE_RETURNED)) {
				delete this.model.newemail;
				return true;
			}
		}
	}

	this.model.newemail = email;

	return true;
}



RegisterMyTokenRequestModel.prototype.updateModel = function(fld, val) {
	switch(fld.id) {
		case "email":
			if (!this.checkEmail(fld, val)) {
				fld.value = val;
				return false;
			}
			break;
		default:
			this.model[fld.id] = val;
	}
	return true;
}



RegisterMyTokenRequestModel.prototype.perform = function(user, action) {
	GPSystem.log(GPSystem.DEBUG, module.id, "perform(" + user.id + "," + action + ")");

	var actionList = this.getActionList(user);

	if (actionList.indexOf(action) == -1) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Action " + action + " unknown");
	}

	switch(action) {
		case "action.save":
			if (user.id == this.bo.originatorId) {
				this.updateNotificationSettings();
			}
			break;
		case "action.approve":
			this.sendNewActivationCode();
			this.setLifeCycle(ServiceRequestModel.LIFECYCLE_VALIDATE);
			this.bo.assignedToRole = null;		// No notification as there is an activation code e-mail.
			this.setStatusInfo("Request approved");
			break;
		case "action.registration.change_email":
			this.model.activationRequests = RegisterMyTokenRequestModel.NO_OF_INITIAL_ACTIVATION_REQUESTS;
			this.model.activationTries = RegisterMyTokenRequestModel.INITIAL_ACTIVATION_TRIES;
			this.reregisterToken();
			this.model.email = this.model.newemail;
			delete this.model.newemail;
			this.sendNewActivationCode();
			this.bo.assignedToRole = null;
			this.setStatusInfo("Changed e-mail address, reset activation code and request counter");
			break;
		case "action.registration.unblock_activation_code":
			this.model.activationTries = RegisterMyTokenRequestModel.INITIAL_ACTIVATION_TRIES;
			this.sendNewActivationCode();
			this.bo.assignedToRole = null;
			this.setStatusInfo("Activation Code unblocked");
			break;
		case "action.registration.unblock_activation_requests":
			this.model.activationRequests = RegisterMyTokenRequestModel.NO_OF_INITIAL_ACTIVATION_REQUESTS;
			this.model.activationTries = RegisterMyTokenRequestModel.INITIAL_ACTIVATION_TRIES;
			this.setLifeCycle(ServiceRequestModel.LIFECYCLE_VALIDATE);
			this.assignToRole(null);		// Return to user
			this.setStatusInfo("Requesting new Activation Codes unblocked");
			break;
		case "action.registration.reject":
			this.setLifeCycle(ServiceRequestModel.LIFECYCLE_REJECTED);
			this.bo.assignedToRole = null;
			this.setStatusInfo("Service Request rejected");
			break;
		case "action.registration.terminate":
			this.deregisterToken();
			this.service.removeRegistrationTokenAction(this.model.tokenId);
			this.setLifeCycle(ServiceRequestModel.LIFECYCLE_TERMINATED);
			this.bo.assignedToRole = null;
			this.setStatusInfo("Token deregistered");
			break;
	}

	this.commit(user.id);
	return true;
}



RegisterMyTokenRequestModel.prototype.getToken = function() {
	var tokenDAO = this.service.daof.getTokenDAO();
	return tokenDAO.getToken(this.model.tokenId);
}



RegisterMyTokenRequestModel.prototype.deregisterToken = function() {
	var tokenDAO = this.service.daof.getTokenDAO();
	tokenDAO.deregisterToken(this.model.tokenId);
}



RegisterMyTokenRequestModel.prototype.reregisterToken = function() {
	// ToDo: Refactor after migrating old originatorId entries of users that enroll persons (Changed 2024-11-12, ASC)
	this.model.subjectId = this.getOriginatorId();
	var tokenDAO = this.service.daof.getTokenDAO();
	var token = tokenDAO.getToken(this.model.tokenId);

	tokenDAO.deregisterToken(this.model.tokenId);

	var actionbo = this.service.getRegistrationBOFromTokenActions(token);
	if (actionbo == null) {
		tokenDAO.addTokenAction(token.id, this.bo.id);
	}
}



RegisterMyTokenRequestModel.prototype.setSubject = function(email) {
	var subjectDAO = this.service.daof.getSubjectDAO();
	if (this.model.subjectId) {
		var subject = subjectDAO.getSubject(this.model.subjectId);
		subjectDAO.updateEmail(subject, email);
	} else {
		var subject = subjectDAO.getSubjectByEmail(email);

		if (!subject) {
			var template = {
				type: "Person",
				email: email
			};
			subject = subjectDAO.newSubject(template);

			if (subject.id == 1) {
				// Set special role "systemadmin" for first user in order to bootstrap the system
				var roleDAO = this.service.daof.getRoleDAO();
				roleDAO.createPredefinedRoles();
				roleDAO.newAssignedRole(subject.id, RoleDAO.ID_SystemAdmin, this.bo.id);
				roleDAO.newAssignedRole(subject.id, RoleDAO.ID_HSMAdmin, this.bo.id);
				roleDAO.newAssignedRole(subject.id, RoleDAO.ID_Auditor, this.bo.id);
				roleDAO.newAssignedRole(subject.id, RoleDAO.ID_SubscriberManager, this.bo.id);
				roleDAO.newAssignedRole(subject.id, RoleDAO.ID_Subscriber, this.bo.id);
				roleDAO.newAssignedRole(subject.id, RoleDAO.ID_RegistrationManager, this.bo.id);
				roleDAO.newAssignedRole(subject.id, RoleDAO.ID_TokenManager, this.bo.id);
			} else if (this.service.grantSubscriberRole) {
				var roleDAO = this.service.daof.getRoleDAO();
				roleDAO.newAssignedRole(subject.id, RoleDAO.ID_Subscriber, this.bo.id);
			}
		}
	}

	var srDAO = this.service.daof.getServiceRequestDAO();
	srDAO.updateOriginatorId(this.bo, subject);
}



RegisterMyTokenRequestModel.prototype.updateNotificationSettings = function() {
	this.setNotificationSettings(this.model.allowSRNotification, this.model.allowUPDNotification);
}



RegisterMyTokenRequestModel.prototype.setNotificationSettings = function(allowSRNotification, allowUPDNotification) {
	var subject = this.getOriginator();
	var c = subject.bo.getContent();
	var hasChanged = false;

	if (typeof(allowSRNotification) != "undefined" &&
		c.allowSRNotification != allowSRNotification) {
		c.allowSRNotification = allowSRNotification;
		hasChanged = true;
	}

	if (typeof(allowUPDNotification) != "undefined" &&
		c.allowUPDNotification != allowUPDNotification) {
		c.allowUPDNotification = allowUPDNotification;
		hasChanged = true;
	}

	if (hasChanged) {
		var subjectDAO = this.service.daof.getSubjectDAO();
		subject.bo.setContent(c);
		subjectDAO.updateContent(subject.bo);
	}
	this.setStatusInfo("Updated notification settings");
}



/**
 * Generate random data and decimalize into a sequence of digits
 *
 * @param {digits} number of digits
 * @type String
 * @return the random activation code
 */
RegisterMyTokenRequestModel.prototype.generateActivationCode = function(digits) {
	var crypto = new Crypto();
	var raw = crypto.generateRandom(digits);

	var c;
	do	{
		var str = raw.toString(HEX);
		c = digits;
		var result = "";
		var i = 0;

		while ((i < str.length) && (c > 0)) {
			var d = str.charCodeAt(i);
			if ((d >= 0x30) && (d <= 0x39)) {
				result = result.concat(String.fromCharCode(d));
				c--;
			}
			i++;
		}
		if (c > 0) {
			raw = raw.not();
		}

	} while (c > 0);

	return result;
}



/**
 * Generate a new Activation Code and send it to
 * the given e-mail address.
 */
RegisterMyTokenRequestModel.prototype.sendNewActivationCode = function() {
	if (this.service.testMode) {
		/* use this activation code generator only for testing*/
		var crypto = new Crypto()
		var hash = crypto.digest(Crypto.SHA_256, new ByteString(this.model.email, ASCII));
		this.model.activationCode = hash.bytes(0,3).toString(HEX);
	} else {
		/* use this activation code generator for productive system */
		this.model.activationCode = this.generateActivationCode(6);
	}

	GPSystem.log(GPSystem.DEBUG, module.id, "ACTIVATION CODE " + this.model.activationCode);

	this.setStatusInfo("Activation code send");
	this.setLifeCycle(ServiceRequestModel.LIFECYCLE_VALIDATE);
	if (!this.service.testMode) {
		this.service.sendActivationCode(this);
	}
}



RegisterMyTokenRequestModel.prototype.getEmail = function() {
	return this.model.email;
}



RegisterMyTokenRequestModel.prototype.getActivationCode = function() {
	return this.model.activationCode
}



RegisterMyTokenRequestModel.prototype.getActivationTries = function() {
	return this.model.activationTries;
}



RegisterMyTokenRequestModel.prototype.isCompleted = function() {
	return this.getStatusInfo() == RegisterMyTokenRequestModel.STATE_COMPLETED;
}



/**
 * Assign a valid e-mail address to a subject and send an activation code.
 * Sets the service request life-cycle to LIFECYCLE_VALIDATE.
 *
 * @param {String} email the e-mail address
 */
RegisterMyTokenRequestModel.prototype.setEmail = function(email, user) {
	assert(email, "Parameter email must not be empty");
	assert(typeof(email) == "string", "Parameter email must be a string");

	if (this.bo.lifecycle != ServiceRequestModel.LIFECYCLE_NEW &&
		this.bo.lifecycle != ServiceRequestModel.LIFECYCLE_EDIT &&
		this.bo.lifecycle != ServiceRequestModel.LIFECYCLE_VALIDATE) {
		GPSystem.log(GPSystem.ERROR, module.id, "setEmail not allowed in life-cycle " + this.bo.lifecycle);
		return;
	}

	if (this.model.activationRequests <= 0) {
		this.setStatusInfo("Activation Code Generation Limit Reached");
		this.setLifeCycle(ServiceRequestModel.LIFECYCLE_ERROR);
		this.assignToRegistrationOfficer();
		this.commit();
		return;
	}

	if (!this.model.email || !this.model.raRoleID) {
		this.model.email = email
		if (!this.isValidEmail(email)) {
			this.commit();
			return;
		}
	}

	if ((this.service.selfRegistrationMode == 1)
		&& this.hasPredefinedRARole() // there is no RA officer for the first registration
		&& (this.bo.lifecycle == ServiceRequestModel.LIFECYCLE_NEW) && (user == undefined)) {
		this.setStatusInfo("Request submitted for approval");
		this.setLifeCycle(ServiceRequestModel.LIFECYCLE_APPROVE);
		this.assignToRegistrationOfficer();
		this.commit();
		return;
	}

	this.sendNewActivationCode();
	this.model.activationRequests--;
	this.commit(user ? user.id : undefined);
}



/**
 * Verify the activation code send via mail to the user.
 * Each verification decreases the left activation tries.
 * A failed verification sets the service request status to
 * STATE_WRONG_ACTIVATION_CODE.
 * If there are no more activation tries the service request
 * will be assigned to the corresponding registration officer.
 * After a successful verifcation the status is set to STATE_COMPLETED.
 *
 * @param {String} code the activation code
 */
RegisterMyTokenRequestModel.prototype.verifyActivationCode = function(code) {
	if (this.bo.lifecycle != ServiceRequestModel.LIFECYCLE_VALIDATE) {
		GPSystem.log(GPSystem.ERROR, module.id, "verifyActivationCode not allowed in life-cycle " + this.bo.lifecycle);
		return;
	}

	// Try activation code
	this.model.activationTries--;

	if (this.model.activationCode != code) {
		if (this.model.activationTries <= 0) {
			this.setLifeCycle(ServiceRequestModel.LIFECYCLE_ERROR);
			this.assignToRegistrationOfficer();
		}
		this.setStatusInfo(RegisterMyTokenRequestModel.STATE_WRONG_ACTIVATION_CODE);
		this.commit();
		return;
	}

	// e-mail address verified
	this.setSubject(this.model.email);
	var tokenDAO = this.service.daof.getTokenDAO();
	tokenDAO.updateSubjectId(this.getToken(), this.bo.originatorId);
	tokenDAO.updateServiceRequestId(this.getToken(), this.bo.id);

	this.setStatusInfo(RegisterMyTokenRequestModel.STATE_COMPLETED);
	this.setLifeCycle(ServiceRequestModel.LIFECYCLE_COMPLETE);
	this.service.removeRegistrationTokenAction(this.model.tokenId);
	this.commit();
}



/**
 * Cancel the RegisterMyTokenServiceRequest during enrollment.
 * Called by the user via RegistrationWizard.
 */
RegisterMyTokenRequestModel.prototype.cancelServiceRequest = function() {
	if (this.model.raRoleID) {
		this.setLifeCycle(ServiceRequestModel.LIFECYCLE_RETURNED);
		this.assignToRole(this.model.raRoleID);
		this.commit();
	}
}



/**
 * Handle Card Updater Calls
 */
RegisterMyTokenRequestModel.prototype.handleSmartCardHSM = function(sc, chain, user) {
	// Return true to keep the token action
	return true;
}
