/**
 *  ---------
 * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
 * |#       #|
 * |#       #|  Copyright (c) 1999-2023 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 Connector for the PKI-API
 */


function PKIAPIConnector(cfg) {
	this.cfg = cfg;
}
PKIAPIConnector.constructor = PKIAPIConnector;

exports.PKIAPIConnector = PKIAPIConnector;

PKIAPIConnector.FAILED = "msg.failed";
PKIAPIConnector.COMPLETED = "msg.completed";
PKIAPIConnector.PENDING = "msg.pending";
PKIAPIConnector.RUNNING = "msg.running";


PKIAPIConnector.prototype.getConnection = function(url) {
	var c = new URLConnection(url);

	if (typeof(this.cfg.keyStore) != "undefined") {
		GPSystem.log(GPSystem.DEBUG, module.id, "Set trust store, key store and pw for PKI API connection");
		c.setTLSKeyStores(this.cfg.trustStore, this.cfg.keyStore, this.cfg.keyPassword);
	} else if (typeof(this.cfg.trustStore) != "undefined") {
		GPSystem.log(GPSystem.DEBUG, module.id, "Set only trust store for PKI API connection");
		c.setTLSKeyStores(this.cfg.trustStore);
	} else {
		GPSystem.log(GPSystem.DEBUG, module.id, "Didn't set tls key stores");
	}
	return c;
}



PKIAPIConnector.prototype.handleCookies = function(cookies) {
	var combinedCookie = "";
	for (var i = 0; i < cookies.length; i++) {
		var eol = cookies[i].indexOf(";");
		if (eol < 0) {
			eol = cookies[i].length;
		}
		var elem = cookies[i].substr(0, eol);
		if (i > 0) {
			combinedCookie += "; ";
		}
		combinedCookie += elem;
	}

	this.session = combinedCookie;
}



/**
 * Post an action to the PKI-API
 *
 * @param {Number} srID the Service Request ID
 * @param {Object} action the json action
 * @param {Number} userId the user id
 */
PKIAPIConnector.prototype.postAction = function(srID, action, userId) {
	GPSystem.log(GPSystem.DEBUG, module.id, "postAction()");
	return this.postJSON("/sr/" + srID + "/action", action, userId);
}



/**
 * Post a json object to the PKI-API
 *
 * @param {String} path the URL path of the resource
 * @param {Object} jsonObject the json object
 * @param {Number} userId the user id
 */
PKIAPIConnector.prototype.postJSON = function(path, jsonObject, userId) {
	GPSystem.log(GPSystem.DEBUG, module.id, "postJSON()");

	var url = this.cfg.apiURL + "/api" + path;

	var c = this.getConnection(url);

	c.addHeaderField("Content-Type", "application/json");
	c.addHeaderField("Accept", "application/json");

	if (typeof(userId) != "undefined") {
		c.addHeaderField("UserId", "" + userId);
	}

	var json = JSON.stringify(jsonObject);
	GPSystem.log(GPSystem.DEBUG, module.id, "post jsonObject \n" + json + "\nto " + url);
	var rsp = c.post(json);
	if (rsp)  {
		var rspJSON = JSON.parse(rsp);
		GPSystem.log(GPSystem.DEBUG, module.id, "Response from PKI-API (" + c.responseCode + "): \n" + JSON.stringify(rspJSON, null, 2));
	} else {
		var rspJSON = {};
		GPSystem.log(GPSystem.DEBUG, module.id, "Empty response from PKI-API");
	}

	rspJSON.responseCode = c.responseCode;

	var cookie = c.getHeaderField("Set-Cookie");

	if (cookie) {
		this.handleCookies(cookie);

		var sessionId = this.session.substring("JSESSIONID=".length);
		GPSystem.log(GPSystem.DEBUG, module.id, "PKI-API session id: " + sessionId);
		rspJSON.sessionId = sessionId;
	}

	return rspJSON;
}



/**
 * Get the service request
 *
 * @param {Number} srID the Service Request ID
 * @param {Number} userId the user id
 */
PKIAPIConnector.prototype.getServiceRequest = function(srID, userId, withForm) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getServiceRequest()");

	var path = "/sr/" + srID;

	if (withForm) {
		path += "/form";
	}

	var json = this.getResource(path, userId);

	return json;
}



/**
 * Get a list of service requests
 *
 * @param {Number} userId the user id
 * @param {PKIAPIListFilter[]} filter list of filter - optional
 * @param {PKIAPIListOrder[]} order list of order - optional
 * @param {Number} limit the query limit - optional
 * @param {Number} offset the query offset- optional
 */
PKIAPIConnector.prototype.getServiceRequestList = function(userId, filter, order, limit, offset) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getServiceRequestList()");

	var path = "/sr/";

	var json = this.getFilteredListResource(path, userId, filter, order, limit, offset)

	return json;
}



/**
 * Get the number of service requests
 *
 * @param {Number} userId the user id
 * @param {PKIAPIListFilter[]} filter list of filter - optional
 */
PKIAPIConnector.prototype.getServiceRequestListCount = function(userId, filter) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getServiceRequestListCount()");

	var path = "/sr/count";

	var json = this.getFilteredListResource(path, userId, filter)

	return json;
}



/**
 * Get the number of service requests
 *
 * @param {Number} userId the user id
 * @param {PKIAPIListFilter[]} filter list of filter - optional
 * @param {PKIAPIListOrder[]} order list of order - optional
 * @param {Number} limit the query limit - optional
 * @param {Number} offset the query offset- optional
 */
PKIAPIConnector.prototype.getInvolvedServiceRequestList = function(userId, filter, order, limit, offset) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getInvolvedServiceRequestList()");

	var path = "/sr/involved";

	var json = this.getFilteredListResource(path, userId, filter, order, limit, offset)

	return json;
}



/**
 * Get the number of service requests
 *
 * @param {Number} userId the user id
 * @param {PKIAPIListFilter[]} filter list of filter - optional
 */
PKIAPIConnector.prototype.getInvolvedServiceRequestListCount = function(userId, filter) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getInvolvedServiceRequestListCount()");

	var path = "/sr/involved/count";

	var json = this.getFilteredListResource(path, userId, filter)

	return json;
}



/**
 * Query status of a card action
 *
 * @param {Number} srID the Service Request ID
 */
PKIAPIConnector.prototype.getCardActionStatus = function(srID) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getCardActionStatus()");

	var path = "/sr/" + srID + "/action";

	var json = this.getResource(path, null, this.session);

	return json;
}



/**
 * Get all connected token for the given subject
 *
 * @param {Number} subjectId the subject ID
 * @param {Number} userId the user id
 */
PKIAPIConnector.prototype.getTokenList = function(subjectId, userId) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getTokenList()");

	var path = "/subject/" + subjectId + "/token";

	var json = this.getResource(path, userId);

	return json;
}



/**
 * Get all connected token for the given subject
 *
 * @param {Number} subjectId the subject ID
 * @param {Number} tokenId the token ID
 * @param {Number} userId the user id
 */
PKIAPIConnector.prototype.getToken = function(subjectId, tokenId, userId) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getToken()");

	var path = "/subject/" + subjectId + "/token/" + tokenId;

	var json = this.getResource(path, userId);

	return json;
}



/**
 * Get all connected token for the given subject
 *
 * @param {Number} subjectId the subject ID
 * @param {Number} tokenId the token ID
 * @param {Number} userId the user id
 */
PKIAPIConnector.prototype.getPKAChallenge = function(subjectId, tokenId, userId) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getTokenList()");

	var path = "/subject/" + subjectId + "/token/" + tokenId;

	var action = {
		"action":"pkaChallenge"
	}
	var json = this.postJSON(path, action, userId);

	return json;
}



/**
 * Reset the authentication state of the specified token
 *
 * @param {Number} subjectId the subject ID
 * @param {Number} tokenId the token ID
 * @param {Number} userId the user id
 */
PKIAPIConnector.prototype.postLogout = function(subjectId, tokenId, userId) {
	GPSystem.log(GPSystem.DEBUG, module.id, "postLogout()");

	var path = "/subject/" + subjectId + "/token/" + tokenId;

	var action = {
		"action":"logout"
	}
	var json = this.postJSON(path, action, userId);

	return json;
}



/**
 * Perform Public Key Authentication
 *
 * @param {Number} subjectId the subject ID
 * @param {Number} tokenId the token ID
 * @param {Number} userId the user id
 * @param {PublicKeyReference} chr the CHR of the public key used to verify the signature
 * @param {ByteString} signature the signature over the concatenation of the device id and challenge
 */
PKIAPIConnector.prototype.performPublicKeyAuthentication = function(subjectId, tokenId, userId, chr, signature) {
	GPSystem.log(GPSystem.DEBUG, module.id, "performPublicKeyAuthentication()");

	var path = "/subject/" + subjectId + "/token/" + tokenId;

	var action = {
		"action":"externalAuthenticate",
		"args": {
			"chr": chr.toString(),
			"signature": signature.toString(HEX)
		}
	}
	var json = this.postJSON(path, action, userId);

	return json;
}



/**
 * Get a JSON Resource for the given path
 *
 * @param {String} path the URL path of the resource
 */
PKIAPIConnector.prototype.getResource = function(path, userId, session) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getResource()");

	var url = this.cfg.apiURL + "/api" + path;

	var c = this.getConnection(url);

	if (typeof(session) != "undefined") {
		c.addHeaderField("Cookie", session);
	}

	c.addHeaderField("Accept", "application/json");

	if (typeof(userId) != "undefined") {
		c.addHeaderField("UserId", "" + userId);
	}

	var rsp = c.get();
	var json = JSON.parse(rsp);

	GPSystem.log(GPSystem.DEBUG, module.id, "Response from PKI-API: \n" + JSON.stringify(json, null, 2));

	var cookie = c.getHeaderField("Set-Cookie");

	if (cookie) {
		this.handleCookies(cookie);

		var sessionId = this.session.substring("JSESSIONID=".length);
		GPSystem.log(GPSystem.DEBUG, module.id, "PKI-API session id: " + sessionId);
		json.sessionId = sessionId;
	}

	return json;
}



/**
 * Get a JSON Resource for the given path
 *
 * @param {String} path the URL path of the resource
 */
PKIAPIConnector.prototype.getFilteredListResource = function(path, userId, filter, order, limit, offset) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getFilteredListResource()");

	var queryString = "";

	if (filter) {
		for (var i = 0; i < filter.length; i++) {
			queryString += filter[i].getEncoded() + "&";
		}
	}

	if (order) {
		for (var i = 0; i < order.length; i++) {
			queryString += order[i].getEncoded() + "&";
		}
	}

	if (limit != null) {
		queryString += "limit=" + limit + "&";
	}

	if (offset != null) {
		queryString += "offset=" + offset + "&";
	}

	if (queryString.length > 0) {
		queryString.substr(0, queryString.length - 1);
		path += "?" + queryString;
	}

	var url = this.cfg.apiURL + "/api" + path;

	var c = this.getConnection(url);

	c.addHeaderField("Accept", "application/json");

	if (typeof(userId) != "undefined") {
		c.addHeaderField("UserId", "" + userId);
	}

	var rsp = c.get();
	var json = JSON.parse(rsp);

	GPSystem.log(GPSystem.DEBUG, module.id, "Response from PKI-API: \n" + JSON.stringify(json, null, 2));

	var cookie = c.getHeaderField("Set-Cookie");

	if (cookie) {
		this.handleCookies(cookie);

		var sessionId = this.session.substring("JSESSIONID=".length);
		GPSystem.log(GPSystem.DEBUG, module.id, "PKI-API session id: " + sessionId);
		json.sessionId = sessionId;
	}

	return json;
}



function PKIAPIListFilter(key, value, op) {
	this.key = key;
	this.value = value;
	this.op = op
}
PKIAPIListFilter.constructor = PKIAPIListFilter;

exports.PKIAPIListFilter = PKIAPIListFilter;



PKIAPIListFilter.OP_LESS_THAN = "lt";
PKIAPIListFilter.OP_LESS_THAN_EQUAL = "lte";
PKIAPIListFilter.OP_EQUAL = "eq";
PKIAPIListFilter.OP_NOT_EQUAL = "ne";
PKIAPIListFilter.OP_GREATER_THAN = "gt";
PKIAPIListFilter.OP_GREATER_THAN_EQUAL = "gte";
PKIAPIListFilter.OP_LIKE = "like";



PKIAPIListFilter.prototype.getEncoded = function() {
	return this.key + "=" + this.op + ":" + this.value;
}



function PKIAPIListOrder(orderBy, asc) {
	this.orderBy = orderBy;
	this.asc = asc;
}
PKIAPIListOrder.constructor = PKIAPIListOrder;

exports.PKIAPIListOrder = PKIAPIListOrder;



PKIAPIListOrder.KEY_ORDER_BY = "sort_by";
PKIAPIListOrder.ORDER_ASC = "asc";
PKIAPIListOrder.ORDER_DESC = "desc";



PKIAPIListOrder.prototype.getEncoded = function() {
	var res = PKIAPIListOrder.KEY_ORDER_BY + "=" + this.orderBy + ":"
	if (this.asc) {
		res += PKIAPIListOrder.ORDER_ASC;
	} else {
		res += PKIAPIListOrder.ORDER_DESC;
	}
	return res;
}
