/**
 *  ---------
 * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
 * |#       #|
 * |#       #|  Copyright (c) 2017 CardContact Systems GmbH
 * |'##> <##'|  Schuelerweg 38, 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 SubjectController
 */

var CommonUI			= require('scsh/srv-cc1/CommonUI').CommonUI;
var PKIXCommon			= require("scsh/x509/PKIXCommon").PKIXCommon;
var Holder				= require('scsh/pki-db/Holder').Holder;
var TrustCenter			= require('pki-as-a-service/subjects/TrustCenter').TrustCenter;
var PKAWizard			= require('pki-as-a-service/ui/PKAWizard').PKAWizard;
var PKAWizardForPKIAPI	= require('scsh/srv-cc1/PKAWizardForPKIAPI').PKAWizardForPKIAPI;
var PKIAPIConnector = require('scsh/srv-cc1/PKIAPIConnector').PKIAPIConnector;
var SubjectOverviewController	= require('pki-as-a-service/ui/SubjectOverviewController').SubjectOverviewController;
var MenuModel = require('pki-as-a-service/ui/MenuModel').MenuModel;
var HolderView = require('pki-as-a-service/ui/HolderView').HolderView;
var CVCHolderView = require('pki-as-a-service/ui/CVCHolderView').CVCHolderView;
var X509HolderView = require('pki-as-a-service/ui/X509HolderView').X509HolderView;



function SubjectController(ui) {
	this.ui = ui;
	this.service = ui.service;

	this.overviewController = new SubjectOverviewController(ui, false);
}

exports.SubjectController = SubjectController;



SubjectController.SUBJECT_TYPE_ANY = "Any";



SubjectController.prototype.getSubject = function(req, res) {
	try {
		var id = parseInt(req.url[2]);

		if (typeof(id) != "number" || isNaN(id)) {
			throw new GPError(module.id, GPError.INVALID_DATA, 0, "Argument id not valid in URL");
		}

		var subject = this.service.getSubject(id);
	} catch (e) {
		if (e.error == GPError.OBJECT_NOT_FOUND) {
			GPSystem.log(GPSystem.WARN, module.id, e);
			res.setStatus(HttpResponse.SC_NOT_FOUND);
		} else {
			GPSystem.log(GPSystem.ERROR, module.id, e);
			res.setStatus(HttpResponse.SC_BAD_REQUEST);
		}
		return;
	}

	return subject;
}



SubjectController.prototype.getSubjectView = function(req, res, subject) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var content =	<div>
				<h1>{ subject.bo.type }</h1>

				<br/>
				<div id="form" />
				<br/>
			</div>;

	var factory = this.service.pluginRegistry.getFactoryForSubject(subject.bo.type);
	if (!factory) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Can not find factory for subject " + subject.bo.type);
	}

	var ctrl = factory.getControllerForSubject(subject.bo.type);

	if (ctrl) {
		var form = ctrl.getView(req, res, this, subject);
	} else {
		var form = <p>No viewer found for { subject.bo.type} </p>
	}

	var div = content..div.(@id == "form");
	div.appendChild(form);

	var menuModel = null;
	if (ctrl.getMenuModel) {
		var menuModel = ctrl.getMenuModel(req, this, subject);
	}

	var page = this.ui.generateTemplate(req.url, menuModel);
 	var c = page..div.(@id == "content");
 	c.div = content;

	return page;
}



SubjectController.prototype.getMenuModelFromRegistry = function(name, subject, homeURL) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var model = new MenuModel(<span>{ name }</span>);

	var processes = this.service.pluginRegistry.enumerateRunnableProcessesForSubject(this.ui.session.user, subject);

	var withoutSection = [];
	var sections = {}
	for (var i = 0; i < processes.length; i++) {
		var p = processes[i];

		var url = homeURL + "/sr/new?process=" + p.process + "&for=" + subject.getId();
		var a = <a href={url}>{ p.label }</a>

		if (typeof(p.section) != "undefined") {
			if (typeof(sections[p.section]) == "undefined") {
				var heading = new MenuModel(p.section);
				heading.status = MenuModel.STATE_HEADING;
				sections[p.section] = heading
			}
			sections[p.section].addItem(a);
		} else {
			withoutSection.push(a);
		}
	}

	for (var i = 0; i < withoutSection.length; i++) {
		model.addItem(withoutSection[i]);
	}


	for (var i in sections) {
		model.addItem(sections[i]);
	}

	return model;
}



SubjectController.prototype.renderAssignedRoles = function(subject, homeURL) {
	var roleDAO = this.service.daof.getRoleDAO();
	var roles = roleDAO.getAssignedRoles(subject.getId());

	if (roles.length == 0) {
		return;
	}

	var div = <div>
			<h2>Assigned Roles</h2>
			<table class="pure-table pure-table-horizontal">
				<thead>
					<tr><th>Role</th><th>Service Request</th></tr>
				</thead>
				<tbody/>
			</table>
		</div>;

	var tbody = div..tbody;
	for (var i = 0; i < roles.length; i++) {
		var roleURL = homeURL + "/role/" + roles[i].id;
		var srURL = homeURL + "/sr/" + roles[i].serviceRequestId
		tbody.tr +=
			<tr>
				<td><a href={roleURL}>{roles[i].name}</a></td>
				<td><a href={srURL}>{roles[i].serviceRequestId}</a></td>
			</tr>
	}

	return div;
}



SubjectController.prototype.renderHolderViews = function(subject, homeURL) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var holderDAO = this.service.daof.getHolderDAO();
	var list = holderDAO.getHolderListBySubject(subject.getId());

	var div = <div />;
	for (var i = 0; i < list.length; i++) {
		var holder = list[i];
		if (holder.type != null) {
			var view = this.renderHolderView(holder, subject, homeURL);
			div.appendChild(view);
		}
	}

	return div;
}



SubjectController.prototype.renderHolderView = function(holder, subject, homeURL) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var factory = this.service.pluginRegistry.getFactoryForHolderType(holder.type);
	if (factory) {
		var view = factory.getHolderView(holder);
	} else {
		if (holder.certificateType == 1) {
			var view = new CVCHolderView(this.service, holder);
		} else if (holder.certificateType == 2) {
			var view = new X509HolderView(this.service, holder);
		} else {
			var view = new HolderView(this.service, holder);
		}
	}

	return view.getView(homeURL, this, subject);
}



SubjectController.prototype.renderHolderPath = function(path, homeURL) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var h = <h3 id={path[path.length - 1].id}/>;
	for (var i = 0; i < path.length; i++) {
		var holder = path[i];
		if (i > 0) {
			h.appendChild('/');
		}
		if (i < path.length - 1) {
			if (holder.subjectId) {
				h.appendChild(<a href={homeURL + "/subject/" + holder.subjectId + "#" + holder.id}>{holder.name}</a>);
			} else {
				h.appendChild(<span>{holder.name}</span>);
			}
		} else {
			h.appendChild(holder.name);
		}
	}

	return h;
}



SubjectController.prototype.renderCertificate = function(subject, homeURL) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var holderdao = this.service.daof.getHolderDAO();
	var holderList = holderdao.getHolderListBySubject(subject.getId());

	if (holderList.length == 0) {
		return;
	}

	var empty = true;
	var div = <div/>;
	for (var i = 0; i < holderList.length; i++) {
		var holder = holderList[i];
		if (holder.certificateType != Holder.X509) {
			GPSystem.log(GPSystem.DEBUG, module.id, "Skip rendering of CVC certificate: " + holder);
			continue;
		}
		if (holder.type != null) {
			GPSystem.log(GPSystem.DEBUG, module.id, "Skip holder with type");
			continue;
		}
		empty = false;
		var path = holderdao.determineHolderPathList(holder);

		var hp = this.renderHolderPath(path, homeURL);
		var ct = this.renderCertificateTable(holder, homeURL);
		if (ct)  {
			div.appendChild(hp);
			div.appendChild(ct);
		}
	}

	if (empty) {
		return;
	}

	return div;
}



SubjectController.prototype.renderCertificateTable = function(holder, homeURL) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	if (holder.certificateType != Holder.X509) {
		GPSystem.log(GPSystem.WARN, module.id, "Rendering of CVC certificates not supported: " + holder);
		return;
	}

	var certdao = this.service.daof.getCertificateDAO();

	var t = <table class="pure-table pure-table-horizontal"/>;
	t.thead += <thead>
			<tr>
				<th>Subject</th>
				<th>Issuer</th>
				<th>Serial</th>
				<th>Key Domain</th>
			</tr>
		</thead>;
	t.tbody += <tbody/>;

	var boList = certdao.enumerateCertificates(holder);

	if (boList.length == 0) {
		return
	}

	var signerDAO = this.service.daof.getSignerDAO();

	for (var i = 0; i < boList.length; i++) {
		var bo = boList[i];
		var cert = new X509(bo.bytes);
		var dn = cert.getSubjectDNString();
		var rdn = PKIXCommon.parseDN(dn);
		var scn = PKIXCommon.findRDN(rdn, "CN");

		if (!scn) {
			scn = dn;
		}

		var dn = cert.getIssuerDNString();
		var rdn = PKIXCommon.parseDN(dn);
		var icn = PKIXCommon.findRDN(rdn, "CN");

		if (!icn) {
			icn = dn;
		}

		var url = homeURL + "/cert/" + bo.id;

		var currentTag = "";
		if (boList.length > 1 && bo.id == holder.certId) {
			currentTag = String.fromCharCode(0xBB);
		}

		var signer = signerDAO.getSignerByKeyId(holder, bo.keyId);
		if (signer && signer.keyDomain) {
			var kdInfo = signer.keyDomain.toString(HEX);
			if (kdInfo.length > 16) {
				kdInfo = kdInfo.substring(0, 16) + "...";
			}
		} else {
			var kdInfo = "";
		}

		var tr =
		<tr>
			<td>
				{currentTag}
				<a href={ url }>{ scn }</a>
			</td>
			<td>{ icn }</td>
			<td>{ cert.getSerialNumberString() }</td>
			<td>{ kdInfo }</td>
		</tr>

		t.tbody.appendChild(tr);
	}

	return t;
}



SubjectController.prototype.renderToken= function(subject, homeURL) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var t =
		<table class="pure-table pure-table-horizontal">
			<thead>
			<tr>
				<th>ID</th>
				<th>Service</th>
				<th>Key Domains</th>
				<th>Status</th>
			</tr>
			</thead>
			<tbody/>
		</table>;

	if (subject instanceof TrustCenter) {
		var readOnly = !subject.isCertificationOfficer(this.ui.session.user);
	}

	var tokenList = subject.getToken();
	for each (var token in tokenList) {
		var tr = <tr/>;

		if (token.serviceRequestId) {
			var path = <a href={ homeURL + "/sr/" + token.serviceRequestId }>{token.path}</a>;
		} else {
			var path = token.path;
		}
		tr.td += <td>{ path }</td>;

		var lastSeenAt = token.lastSeenAt;
		if (!lastSeenAt) {
			lastSeenAt = "";
		}
		tr.td += <td>{ lastSeenAt }</td>;

		var hsm = this.service.hsmService.getHSMState(token.path);

		if (!hsm || hsm.isOffline()) {
			var rhsm = this.service.backendRegistry.queryHSMStatus(lastSeenAt, subject.getId(), this.ui.session.user.id, token.id);
//			var rhsm = this.service.backendRegistry.locateHSM(subject.getId(), this.ui.session.user.id, token.id);

			if (rhsm && (rhsm.pkaStatus || rhsm.pinStatus)) {
				rhsm.lastSeenAt = lastSeenAt;
				tr.td += this.renderKeyDomainsFromAPI(rhsm.keyDomains);
				tr.td += this.renderTokenAPIStatus(rhsm, readOnly);
			} else {
				if (hsm && hsm.error) {
					var str = "Connection Error (" + hsm.error + ")";
				} else {
					var str = "Seen on " + token.lastSeen.toLocaleString();
				}
				tr.td += <td>{ str }</td>;
				tr.td += <td></td>;
			}
		} else {
			tr.td += this.renderKeyDomains(hsm.keyDomain);
			tr.td += this.renderTokenStatus(hsm, readOnly);
		}

		t.tbody.tr += tr;
	}

	return t;
}



SubjectController.prototype.renderTokenStatus= function(hsm, readOnly) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	if (!hsm) {
		return <td></td>;
	}

	if (hsm.pka) {
		var authStatus = hsm.pka.describeStatus();
	} else {
		var authStatus = SmartCardHSM.describePINStatus( hsm.pinsw, "User");
	}

	if (readOnly) {
		return <td>{ authStatus }</td>;
	}

	var status;
	if (hsm.isUserAuthenticated()) {
		status =
			<td>
				<form class="pure-form" action="" method="get">
					{ authStatus }
					<input name="op" type="hidden" value="logout"/>
					<input name="token" type="hidden" value={hsm.path}/>
					<button class="pure-button" type="submit">Logout</button>
				</form>
			</td>;
	} else {
		if (hsm.pka) {
			status =
				<td>
					<form class="pure-form" action="" method="get">
						{ authStatus }
						<input name="op" type="hidden" value="authenticate"/>
						<input name="token" type="hidden" value={hsm.path}/>
						<button class="pure-button" type="submit">Authenticate</button>
					</form>
				</td>;
		} else if (hsm.hasProtectedPINEntry()) {
			status =
				<td>
					<form class="pure-form" action="" method="get">
						{ authStatus }
						<input name="op" type="hidden" value="verifypin"/>
						<input name="token" type="hidden" value={hsm.path}/>
						<button class="pure-button" type="submit">Verify PIN</button>
					</form>
				</td>;
		} else {
			status =
				<td>
					<form class="pure-form" action="" method="get">
						<p>{ authStatus }</p>
						<input name="op" type="hidden" value="verifypin"/>
						<input name="token" type="hidden" value={hsm.path}/>
						<input name="pin" type="password" size="20" class="pure-input"/>
						<button class="pure-button" type="submit">Verify PIN</button>
					</form>
				</td>;
		}
	}

	GPSystem.log(GPSystem.DEBUG, module.id, "status = " + status);
	return status;
}



SubjectController.prototype.renderTokenAPIStatus= function(token, readOnly) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	if (!token.pkaStatus) {
		return <td>{token.pinStatus}</td>;
	}

	if (token.pkaStatus.numberOfOutstandingPublicKeys > 0) {
		var status = token.pkaStatus.numberOfOutstandingPublicKeys + " missing key(s) in "
		+ token.pkaStatus.numberOfRequiredPublicKeysForAuthentication
		+ " of " + token.pkaStatus.totalNumberOfPublicKeys + " public key authentication";
	} else {
		var status = token.pkaStatus.numberOfAuthenticatedPublicKeys + " authenticated public key(s) in "
		+ token.pkaStatus.numberOfRequiredPublicKeysForAuthentication
		+ " of " + token.pkaStatus.totalNumberOfPublicKeys + " scheme";
	}

	if (readOnly) {
		return <td>{status}</td>;
	}

	if (token.authenticated) {
		var opValue = "logout";
		var buttonValue = "Logout";
	} else {
		var opValue = "authenticate";
		var buttonValue = "Authenticate";
	}
	var form =
		<td>
			<p>{ status }</p>
			<form class="pure-form" action="" method="get">
				<input name="op" type="hidden" value={opValue}/>
				<input name="tokenId" type="hidden" value={token.id}/>
				<input name="lastSeenAt" type="hidden" value={token.lastSeenAt}/>
				<button class="pure-button" type="submit">{buttonValue}</button>
			</form>
		</td>;

	return form;
}



SubjectController.prototype.renderKeyDomainsFromAPI = function(keyDomains) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var td = <td>
				<ul style="list-style: none; margin:0; padding:0;" />
			</td>;

	for (var i = 0; i < keyDomains.length; i++) {
		var kdid = keyDomains[i].uid;

		if (kdid.length > 16) {
				kdid = kdid.substring(0, 16) + "...";
			}

		if (keyDomains[i].label) {
			kdid = keyDomains[i].label + " (" + kdid + ")";
		}

		td.ul.li += <li>{kdid}</li>;
	}
	return td;
}



SubjectController.prototype.renderKeyDomains= function(keyDomains) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var td = <td>
				<ul style="list-style: none; margin:0; padding:0;" />
			</td>;

	td.ul.li += <li>{keyDomains[0].bytes(0, 8).toString(HEX) + "..."} (Default)</li>;
	for (var i = 1; i < keyDomains.length; i++) {
		var kdid = keyDomains[i].toString(HEX);
		if (kdid.length > 16) {
			kdid = kdid.substring(0, 16) + "...";
		}
		td.ul.li += <li>{kdid}</li>;
	}
	return td;
}



SubjectController.prototype.getHSM = function(subject, tokenId) {
	var hsm = this.service.hsmService.getHSMState(tokenId);

	if (!hsm) {
		return;
	}

	if (subject instanceof TrustCenter) {
		if (!subject.isCertificationOfficer(this.ui.session.user)) {
			return;
		}
	}

	if (hsm.isOffline()) {
		return;
	}

	return hsm;
}



SubjectController.prototype.handleRequest = function(req, res) {
	default xml namespace = "http://www.w3.org/1999/xhtml";
	GPSystem.log(GPSystem.DEBUG, module.id, "handleRequest()");

	if (req.url.length < 3) {
		this.overviewController.handleRequest(req, res);
		return;
	}

	var subject = this.getSubject(req, res);
	if (!subject) {
		return;
	}

	if (!subject.canRead(this.ui.session.user)) {
		this.ui.serveNotAuthorizedPage(req, res, req.url);
		return;
	}

	if (req.queryString) {
		this.handleHSMOperation(req, res, subject);
	}

	var page = this.getSubjectView(req, res, subject);

	this.ui.sendPage(req, res, req.url, page);
}



/**
 * Create wizard to handle public key authentication
 */
SubjectController.prototype.handlePKAAuthenticate = function(req, res, hsm, redirectURL) {
	this.ui.wizard = new PKAWizard(this.ui, hsm, redirectURL);
	this.ui.redirectTo(req, res, req.url, "/wizard");
}



SubjectController.prototype.handleHSMOperation = function(req, res, subject) {
	GPSystem.log(GPSystem.DEBUG, module.id, "handleHSMOperation(" + req.queryString + ")");

	var operation = CommonUI.parseQueryString(req.queryString);
	var hsm = this.getHSM(subject, operation.token);
	var url = this.ui.homeURL(req.url) + "/subject/" + subject.getId();

	if (hsm) {
		switch(operation.op) {
		case "verifypin":
			this.service.hsmService.verifyUserPIN(hsm, operation.pin);
			break;
		case "authenticate":
			this.handlePKAAuthenticate(req, res, hsm, url);
			return;
			break;
		case "logout":
			hsm.logout();
			break;
		}
	} else {
		var hsm = this.service.backendRegistry.queryHSMStatus(operation.lastSeenAt, subject.getId(), this.ui.session.user.id, operation.tokenId);
		if (!hsm) {
			this.ui.setErrorMessage("token_removed", "Token removed");
		} else {
			var api = hsm.api;

			switch(operation.op) {
			case "authenticate":
				this.ui.wizard = new PKAWizardForPKIAPI(this.ui, subject.getId(), operation.tokenId, api, url);
				this.ui.redirectTo(req, res, req.url, "/wizard");
				return
			case "logout":
				api.postLogout(subject.getId(), operation.tokenId, this.ui.session.user.id);
				break
			}
		}
	}

	res.setStatus(HttpResponse.SC_SEE_OTHER);
	res.addHeader("Location", url);
}



SubjectController.prototype.handleRestRequest = function(req, res) {
	GPSystem.log(GPSystem.DEBUG, module.id, "handleRestRequest(): req.method:" + req.method + ", req.queryString:" + req.queryString);

	if (req.url.length < 3) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Invalid URL");
	}

	var subject = this.getSubject(req, res);
	if (!subject) {
		return;
	}

	if (!subject.canRead(this.ui.session.user)) {
		res.setStatus(HttpResponse.SC_FORBIDDEN);
		return;
	}

	if (req.method == "POST") {
		// Not implemented yet: this.restfulPostProcessAction(req, res, subject);
		res.setContentType("application/json");
		res.setStatus(HttpResponse.SC_NOT_IMPLEMENTED);
	} else {
		this.restfulGET(req, res, subject);
	}
}



SubjectController.prototype.restfulGET = function(req, res, subject) {
	var factory = this.service.pluginRegistry.getFactoryForSubject(subject.getType());
	if (!factory) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Can not find factory for subject " + subject.getType());
	}

	var ctrl = factory.getControllerForSubject(subject.getType());

	if (ctrl.restfulGET != undefined) {
		ctrl.restfulGET(req, res, this, subject);
	} else {
		res.setContentType("application/json");
		res.setStatus(HttpResponse.SC_NOT_IMPLEMENTED);
	}
}
