/**
 *  ---------
 * |.##> <##.|  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 Card related actions performed in the UI
 */



var CardAction			= require('scsh/srv-cc1/CardAction').CardAction;

var HSMKeyStore			= require('scsh/sc-hsm/HSMKeyStore').HSMKeyStore;
var PKIXCommon			= require('scsh/x509/PKIXCommon').PKIXCommon;
var KeyDomain			= require('pki-as-a-service/service/KeyDomain').KeyDomain;
var Holder				= require('scsh/pki-db/Holder').Holder;
var Certificate			= require('scsh/pki-db/Certificate').Certificate;
var LongDate			= require('scsh/pki-db/LongDate').LongDate;


function ImportSignerWizard(ui, sr, redirectUrl) {
	CardAction.call(this, ui);
	this.sr = sr;
	this.redirectUrl = redirectUrl;
	this.requestPIN = 1;
	GPSystem.log(GPSystem.DEBUG, module.id, "new ImportSignerWizard(" + sr + "," + redirectUrl + ")");

	this.allSigner = [];
}


ImportSignerWizard.prototype = Object.create(CardAction.prototype);
ImportSignerWizard.constructor = ImportSignerWizard;

exports.ImportSignerWizard = ImportSignerWizard;



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

	if (req.method == "POST") {
		if (req.parameters.enumerate) {
			this.ui.wizard = new ImportSignerWizard(this.ui, this.sr, this.redirectUrl);
			this.ui.redirectTo(req, res, req.url, "/wizard");
			return true;
		} else if (req.parameters.signer) {
			this.selectedSigner = req.parameters.signer;
		} else if (req.parameters.cert) {
			this.handleCertUpload(req.parameters.cert);
		} else if (req.parameters.holderName || req.parameters.keyId) {
			this.updateHolderName(req.parameters.holderName, req.parameters.keyId);
		} else if (req.parameters.import) {
			if (this.handleImport()) {
				GPSystem.log(GPSystem.DEBUG, module.id, "redirect to " + this.redirectUrl);
				this.ui.redirectTo(req, res, req.url, this.redirectUrl);
				return;
			}
		}
	}

	var page = this.getPage(req, res, req.url);
	if (page) {
		var t = this.ui.generateTemplate(req.url);
		var c = t..div.(@id == "content");
		c.div = page;
		this.ui.sendPage(req, res, req.url, t);
	} else {
		GPSystem.log(GPSystem.DEBUG, module.id, "redirect to " + this.redirectUrl);
		this.ui.redirectTo(req, res, req.url, this.redirectUrl);
	}
}



ImportSignerWizard.prototype.getPage = function(req, res, url) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	GPSystem.log(GPSystem.DEBUG, module.id, "getPage() : req.method: " + req.method);

	// Perform local redirect

	if (this.state != CardAction.SUCCESS) {
		var page = CardAction.prototype.getPage.call(this, req, res, url);
	}

	if (page) {
		return page;
	}

	// Return ImportSignerWizard page

	var page = <div>
		<div id="error"/>

		<h1>Import Signer Wizard</h1>

		<p>Current Token: {this.path}</p>

		<form class="pure-form" action="" method="post" enctype="multipart/form-data">
			<input name="enumerate" type="hidden" value="true" />
			<button class="pure-button" type="submit">Change Token</button>
		</form>
	</div>

	if (this.allSigner && this.allSigner.length > 0) {
		var div =
			<div>
				<form class="pure-form" action="" method="post" enctype="multipart/form-data">
					<label>Current Signer</label>
					<select name="signer"/>
					<button class="pure-button" type="submit">Select</button>
				</form>
			</div>

		var selectSigner = div.form.select;

		for (var i = 0; i < this.allSigner.length; i++) {
			var signer = this.allSigner[i];

			var option = <option value={signer.keyId}>{signer.label}</option>;

			if (signer.keyId == this.selectedSigner) {
				option.@selected = "selected";
			}

			selectSigner.appendChild(option);
		}

		page.appendChild(div);

		var signer = this.getSelectedSigner();

		page.appendChild(this.renderSigner(signer));
	}



	return page;
}



ImportSignerWizard.prototype.renderSigner = function(signer) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	GPSystem.log(GPSystem.DEBUG, module.id, "renderSigner( " + signer.label + ")");

	var div = <div/>

	var t =
		<table class="pure-table pure-table-horizontal">
			<thead>
				<tr>
					<th>Subject</th>
					<th>Issuer</th>
					<th>Key ID</th>
					<th>Holder Name</th>
				</tr>
			</thead>
			<tbody/>
		</table>;

	for (var i = 0; i < signer.chain.length; i++) {
		var obj = signer.chain[i];
		var cert = obj.cert
		if (cert instanceof Certificate) {
			var cert = new X509(cert.bytes);
		}

		if (cert.getAuthorityKeyIdentifier()) {
			var authKeyId = cert.getAuthorityKeyIdentifier().toString(HEX);
			var authKeyId = authKeyId.substr(0, 16) + "...";
		} else {
			var authKeyId = "N/A";
		}

		if (cert.getSubjectKeyIdentifier()) {
			var subjectKeyId = cert.getSubjectKeyIdentifier().toString(HEX);
			var subjectKeyId = subjectKeyId.substr(0, 16) + "...";
		} else {
			var subjectKeyId = "N/A";
		}

		var tr = <tr/>;
		tr.td += <td>{ cert.getSubjectDNString() }</td>;
		tr.td += <td>{ cert.getIssuerDNString() }</td>;
		tr.td +=
			<td>
				<p>Subject Key ID:   { subjectKeyId }</p>
				<p>Authority Key ID: { authKeyId }</p>
			</td>;


		if (obj.holder) {
			tr.td +=
				<td>
					<input type="text" value={ obj.holder.name } disabled="disabled" />
				</td>
		} else {
			tr.td +=
				<td>
					<form action="" method="post" enctype="multipart/form-data">
						<input name="keyId" type="hidden" value={ cert.getSubjectKeyIdentifier().toString(HEX) } />
						<input id="holderName" name="holderName" type="text" value={ obj.holderName } />
					</form>
				</td>

			if (obj.errorHolderName) {
				tr.td.form.span += <span class="pure-form-message" style="color: red">Name does already exist</span>
			}

		}

		t.tbody.tr += tr;
	}

	div.appendChild(t);

	if (signer.isPathComplete) {
		div.appendChild(
			<form action="" method="post" enctype="multipart/form-data">
				<input name="import" type="hidden" value="true" />
				<input type="submit" value="Import"/>
			</form>
		);
	} else {
		if (signer.errorCert) {
			div.appendChild(
				<p>{ signer.errorCert }</p>
			);
		}
		div.appendChild(
			<form action="" method="post" enctype="multipart/form-data">
				<input type="file" name="cert" size="0"/>
				<input type="submit" value="Upload"/>
			</form>
		);
	}

	return div;
}



ImportSignerWizard.prototype.enumerateSigner = function(sc) {
	GPSystem.log(GPSystem.DEBUG, module.id, "enumerateSigner()");

	this.allSigner = [];

	var ks = new HSMKeyStore(sc);
	var labels = ks.enumerateKeys();

	for (var i = 0; i < labels.length; i++) {
		var label = labels[i];
		GPSystem.log(GPSystem.DEBUG, module.id, "process key " + label);

		var key = ks.getKey(label);
		var keyId = key.getPKCS15Id();

		var signerDAO = this.sr.service.daof.getSignerDAO();
		var list = signerDAO.listSignerByKeyId(keyId);

		if (list.length > 0) {
			GPSystem.log(GPSystem.DEBUG, module.id, "Skip already known signer " + label);
			continue;
		}

		var signer = {
			label: label,
			keyId: keyId,
			isPathComplete: false,
			chain: []
		}

		if (key.keyDomain) { // TODO Test
			signer.keyDomain = this.keyDomainList[key.keyDomain + 1];
		} else {
			signer.keyDomain = this.keyDomainList[0];
		}

		if (!this.createSignature(signer, sc, key)) {
			continue;
		}
		// 		var keyId = cert.getSubjectKeyIdentifier();
		var certificateDAO = this.sr.service.daof.getCertificateDAO();
		var cert = certificateDAO.getCertificateByKeyId(keyId);
		if (cert) {
			var xcert = new X509(cert.bytes);
			if (this.verifySignature(signer, xcert)) {

				// cert path is complete
				this.addCertificateChain(signer, cert);

				GPSystem.log(GPSystem.DEBUG, module.id, "Signer Certificate already known");
				GPSystem.log(GPSystem.DEBUG, module.id, "Add key " + label);
			}
		} else if (ks.hasCertificate(label)) {
			var certbin = ks.getCertificate(label);
			try {
				var signerCert = new X509(certbin);
				if (!this.addCertificateToChain(signer, signerCert)) {
					continue;
				}
			} catch (e) {
				GPSystem.log(GPSystem.DEBUG, module.id, "Skip " + label +  " because not a X509 certificate");
				GPSystem.log(GPSystem.DEBUG, module.id, e.message + "\n" + e.stack);
				continue;
			}
		}

		this.allSigner.push(signer)

	}

	this.state = CardAction.SUCCESS;
}



ImportSignerWizard.prototype.enumerateCACertificates = function(sc) {
	GPSystem.log(GPSystem.DEBUG, module.id, "enumerateCACertificates()");

	this.caCertificates = [];

	var labels = sc.enumerateCACertificates();
	for (var i = 0; i < labels.length; i++) {
		var label = labels[i];
		var enc = sc.getCACertificate(label);
		try {
			var cert = new X509(enc);
			cert.getPublicKey(); // Check if this is an X509 Certificate
			this.caCertificates.push(cert);
		} catch (e) {
			GPSystem.log(GPSystem.DEBUG, module.id, "Error decoding ca certificate: " + e.message + "\n" + e.stack);
		}
	}
}



ImportSignerWizard.prototype.getSelectedSigner = function() {
	GPSystem.log(GPSystem.DEBUG, module.id, "getSelectedSigner()");
	if (this.selectedSigner) {
		for (var i = 0; i < this.allSigner.length; i++) {
			var keyId = new ByteString(this.selectedSigner, HEX);
			if (this.allSigner[i].keyId.equals(keyId)) {
				var signer = this.allSigner[i];
				return signer;
			}

		}
	}

	if (this.allSigner.length > 0) {
		return this.allSigner[0];
	}
}



ImportSignerWizard.prototype.handleImport = function() {
	GPSystem.log(GPSystem.DEBUG, module.id, "handleImport()");

	var signer = this.getSelectedSigner();

	if (!signer.isPathComplete) {
		GPSystem.log(GPSystem.ERROR, module.id, "Cannot import if path is not complete");
		return;
	}

	var holderDAO = this.sr.service.daof.getHolderDAO();
	var certificateDAO = this.sr.service.daof.getCertificateDAO();

	var name = "";

	for (var i = signer.chain.length - 1; i >= 0; i--) {
		var obj = signer.chain[i];

		if (!obj.holder) {
			if (i == signer.chain.length - 1) {
				var parentId = 0;
				var hasHolder = holderDAO.getHolder("/" + obj.holderName, Holder.X509);
			} else {
				var parentId = signer.chain[ i + 1 ].holder.id;
				var hasHolder = holderDAO.getHolderByTypeAndName(parentId, obj.holderName, Holder.X509);
			}

			if (hasHolder) {
				obj.errorHolderName = true;
				return false;
			}

			var template = {
				name: obj.holderName,
				type: "X509CA"
			}

			obj.holder = holderDAO.newHolderForParent(parentId, Holder.X509, template);

			var issuer = obj.cert.getIssuerDNString();
			var subject = obj.cert.getSubjectDNString();
			var autid = obj.cert.getAuthorityKeyIdentifier();
			var subid = obj.cert.getSubjectKeyIdentifier();
			var serial = obj.cert.getSerialNumber().toString(HEX);

			if ((autid && autid.equals(subid)) || issuer.equals(subject)) {
				var dir = Certificate.SHORT;
			} else {
				var dir = Certificate.UP;
			}

			var template = {
				keyId: signer.keyId,
				expiry: new LongDate(obj.cert.getNotAfter()),
				serviceRequestId: this.sr.getId()
			};

			var certVO = certificateDAO.newCertificate(obj.holder, serial, dir, obj.cert.getBytes(), template);

			holderDAO.updateCurrentCertificate(obj.holder, certVO);
		}

		name += "/" + obj.holder.name;
	}

	var holder = signer.chain[0].holder;
	name = name.substr(1) + " [" + Date() + "]";
	var cert = signer.chain[0].cert;
	if (cert instanceof Certificate) {
		cert = new X509(cert.bytes);
	}

	var signerTemplate = {
		holder: holder,
		name: name,
		keyId: signer.keyId,
		keyDomain: signer.keyDomain,
		cert: cert
	}

	this.sr.importSigner(this.ui.session.user, signerTemplate);
	return true;
}



ImportSignerWizard.prototype.addCertificateChain = function(signer, cert) {
	GPSystem.log(GPSystem.DEBUG, module.id, "addCertificateChain()");

	var holderDAO = this.sr.service.daof.getHolderDAO();
	var holder = holderDAO.getHolderById(cert.holderId);
	signer.chain.push({
		cert: cert,
		holder: holder
	});

	while (holder.parentId) {
		var holder = holderDAO.getHolderById(holder.parentId);
		var certificateDAO = this.sr.service.daof.getCertificateDAO();
		var caCert = certificateDAO.getCurrentCertificate(holder);
		signer.chain.push({
			cert: caCert,
			holder: holder
		});
	}

	signer.isPathComplete = true;
}



ImportSignerWizard.prototype.addCertificateToChain = function(signer, cert) {
	GPSystem.log(GPSystem.DEBUG, module.id, "addCertificateToChain()");

	var dn = cert.getSubjectDNString();
	var rdn = PKIXCommon.parseDN(dn);
	var name = PKIXCommon.findRDN(rdn, "CN");

	if (!name) {
		name = dn;
	}

	// Check if the new certificate match to either the private key or the last
	// certificate of the chain
	if (!this.verifyCertificateChain(signer, cert)) {
		return false;
	}

	signer.chain.push({
		cert: cert,
		holderName: name
	});

	// Check self signed certificates

	if (cert.getSubjectKeyIdentifier().equals(cert.getAuthorityKeyIdentifier()) ||
		cert.getSubjectDNString().equals(cert.getIssuerDNString())) {
		try {
			cert.verifyWith(cert);
		} catch (e) {
			GPSystem.log(GPSystem.DEBUG, module.id, "Verification of root certificate failed");

			signer.chain.pop(); // remove imported cert from chain
			return false;
		}

		GPSystem.log(GPSystem.DEBUG, module.id, "Add root certificate");
		signer.isPathComplete = true;
	} else {
		// Check with ca cert from db
		var certificateDAO = this.sr.service.daof.getCertificateDAO();
		var keyId = cert.getAuthorityKeyIdentifier();
		var parentCert = certificateDAO.getCertificateByKeyId(keyId);

		if (parentCert) {
			var parentXCert = new X509(parentCert.bytes);
			try {
				cert.verifyWith(parentXCert);
			} catch (e) {
				GPSystem.log(GPSystem.DEBUG, module.id, "Certificate verification failed");

				signer.chain.pop(); // remove imported cert from chain
				return false;
			}

			this.addCertificateChain(signer, parentCert);
		} else {
			for (var i = 0; i < this.caCertificates.length; i++) {
				var caCert = this.caCertificates[i];
				if (caCert.getSubjectKeyIdentifier().equals(keyId)) {
					GPSystem.log(GPSystem.DEBUG, module.id, "Verify with CA certificate");
					this.addCertificateToChain(signer, caCert);
				}
			}
		}
	}

	return true;
}



ImportSignerWizard.prototype.createSignature = function(signer, sc, key) {
	GPSystem.log(GPSystem.DEBUG, module.id, "createSignature()");
	if (key.desc.tag == 0xA0) { // EC
		signer.mech = Crypto.ECDSA;
	} else if (key.desc.tag == 0x30) { // RSA
		signer.mech = Crypto.RSA_SHA256;
	} else {
		GPSystem.log(GPSystem.ERROR, module.id, "Unexpected Key Description: " + key.desc);
		return false;
	}

	var crypto = sc.getCrypto();
	signer.message = new ByteString("Hello World", ASCII);
	signer.signature = crypto.sign(key, signer.mech, signer.message);

	return true;
}



ImportSignerWizard.prototype.verifySignature = function(signer, cert) {
	GPSystem.log(GPSystem.DEBUG, module.id, "verifySignature()");
	var crypto = new Crypto();
	var key = cert.getPublicKey();

	return crypto.verify(key, signer.mech, signer.message, signer.signature);
}



ImportSignerWizard.prototype.verifyCertificateChain = function(signer, cert) {
	GPSystem.log(GPSystem.DEBUG, module.id, "verifyCertificateChain()");
	if (signer.chain.length == 0) {
		var keyId = cert.getSubjectKeyIdentifier();
		if (!keyId.equals(signer.keyId)) {
			signer.errorCert = "Uploaded key id does not match";
			GPSystem.log(GPSystem.DEBUG, module.id, signer.errorCert);
			return false;
		}

		if (!this.verifySignature(signer, cert)) {
			signer.errorCert = "Certificate does not match private key";
			GPSystem.log(GPSystem.DEBUG, module.id, "Signature verification failed");
			return false;
		}
	} else {
		//
		var lastCert = signer.chain[signer.chain.length - 1].cert;
		try {
			lastCert.verifyWith(cert);
		} catch (e) {
			signer.errorCert = "Certificate verification failed";
			GPSystem.log(GPSystem.DEBUG, module.id, signer.errorCert);
			return false;
		}
	}

	return true;
}



ImportSignerWizard.prototype.handleCertUpload = function(encodedCert) {
	GPSystem.log(GPSystem.DEBUG, module.id, "handleCertUpload()");

	var signer = this.getSelectedSigner();
	delete signer.errorCert;

	try {
		var enc = new ByteString(encodedCert, HEX);
		var xcert = new X509(enc);
		GPSystem.log(GPSystem.DEBUG, module.id, "process uploaded cert " + xcert);
	} catch (e) {
			signer.errorCert = "Error parsing certificate";
			return false;
	}

	this.addCertificateToChain(signer, xcert);

	return true;
}



ImportSignerWizard.prototype.updateHolderName = function(holderName, keyId) {
	GPSystem.log(GPSystem.DEBUG, module.id, "updateHolderName( " + holderName + ", " + keyId + " )");

	var signer = this.getSelectedSigner();
	var skid = new ByteString(keyId, HEX);

	for (var i = 0; i < signer.chain.length; i++) {
		var obj = signer.chain[i];
		var cert = obj.cert;
		if (cert.getSubjectKeyIdentifier().equals(skid)) {
			obj.holderName = holderName;
			delete obj.errorHolderName;
			break;
		}
	}
}



ImportSignerWizard.prototype.handleCardAction = function(card, session, pathInfo) {
	var sc = new SmartCardHSM(card);
	var devAutCert = sc.readBinary(SmartCardHSM.C_DevAut);
	var crypto = new Crypto();
	var chain = SmartCardHSM.validateCertificateChain(crypto, devAutCert);
	if (chain == null) {
		this.setStatusInfo("Could not authenticate SmartCard-HSM");
		return;
	}

	this.path = chain.path
	this.keyDomain = KeyDomain.getDefaultKeyDomainFromCert(chain.devicecert);
	this.keyDomainList = KeyDomain.getKeyDomains(sc, chain.devicecert);

	sc.openSecureChannel(crypto, chain.publicKey);

	this.enumerateCACertificates(sc);
	this.enumerateSigner(sc);
}
