// Il presente modulo � l'equivalente offline della funzionalit� di Unlock
// delle SmartKeyApi. L'interfaccia fornita dalle classi esposte dal presente modulo
// consente:
// L'inizializzazione e il caching sicuro delle informazioni relative alle smart keys
// la gestione (offline semplificata) delle policies di accesso
// la richiesta di Unlock.

var util = require('util');
var uuid = require('uuid-js'); // per la manipolazione dei guid
var events = require('events');
var crypto = require('crypto'); // per l'utilizzo delle funzioni di crittografia

// Setup new logger object
var log = require('./sthal_logger.js');   
var logger = log.logger(); 

var _$tenantId = undefined;
var _$controllerId = undefined;


var config = require('./sthal_config.js');                // configuration singleton object
var configWie = undefined;

/*
if (config.Wiegand() !== null && config.Wiegand() !== undefined && config.Wiegand().length >0) 
{
    
    configWie = config.Wiegand()[0];
}
*/
if (config.Wiegand() !== null && config.Wiegand() !== undefined && config.Wiegand().length >0) 
{
    
    configWie = config.Wiegand();
}

// L'uso della cifratura di sessione in memory per le sk � solo un modo di limitare
// la possibilit� di ottenere, in chiaro testo, il valore degli Shared Secrets
// relativi alle SmartKeys in caso di memory dump o di accesso alla memoria del processo
// da parte di un attaccker. Tuttavia, visto e considerato che anche la chiave
// utilizzata per tale cifratura � disponibile nella memoria del processo
// un potenziale attaccker sarebbe comunque in grado di ottenere l'informazione.
// chiave di cifratura e IV usati per la cifratura degli Shared Secret associati alle Smart Keys
var sessionKey = crypto.randomBytes(32);
var sessionIV = crypto.randomBytes(16);

// offline dictionary che contiene la copia cache delle Smart Keys
// Si tratta di un'unica copia utilizzata dalla modalit� offline
// Il dictionary � inizializzato quando viene abilitata la modalit� offline
// e refreshato ogni volta che si decide effettuare una sincronizzazione delle chiavi e degli accessy
var keys = {};
var policyMap = {};
var badges = {};

function retrieveWiegandFormList (serraturaId) {
	if (configWie == null || configWie == undefined) {
		return undefined;
	}
	if (serraturaId !== null && serraturaId !== undefined) {
        for (var k = 0; k < configWie.length; k++){
            if (configWie[k].serraturaName  == serraturaId){
                found = true;
                break;
            }
        } 
    }
    if (found == true) {
        return k;
    }    
    else {
        return undefined;
    }
}


function _$internalSyncKeys(keyArray) {
	// TODO: Irrobustire dal punto di vista della Security, questa parte in qanto
	// l'input ricevuto è comunque in chiaro testo e quindi leggibile in caso di lettura
	// della memoria del processo da parte di un attaccker
	var tmpKeys = {};
	var tmpBadges = {};
	keyArray.forEach(function (sk) {
		// TODO: Effettuare una corretta validazione di ogni elemento dell'array.
		// N.B. Sebbene ogni sk porti con essa il valore del CompanyId, questo dovrebbe
		// essere lo stesso per tutte le chiavi e coincidere con tenantId
		// TODO: Controllare la suddetta condizione.
		if (sk.Enabled) {
			// evitiamo di cacheare le chiavi disabilitate in quanto comunque l'effetto
			// dell'utilizzo di una tale chiave è comunque quello dell'accesso negato
			if (sk.Shs !== null)  // Dobbiamo considerare di gestire Shs null e badge valorizzato
			{			
				var encryptor = crypto.createCipheriv("aes256", sessionKey, sessionIV);
				var secret = Buffer.concat([encryptor.update(new Buffer(sk.Shs, 'base64')), encryptor.final()]);
				encryptor = null;

				tmpKeys[sk.KeyId.toLowerCase()] = {
					keyType: sk.KeyType,
					shs: {
						expiresUtc: sk.ExpireTimeUtc,
						getSecret: function () {
							var decryptor = crypto.createDecipheriv("aes256", sessionKey, sessionIV);
							return Buffer.concat([decryptor.update(secret), decryptor.final()]);
						}
					},
					uuid: sk.Uuid,
					gpIds: sk.GenericPolicyIds
				};
			}
			else {
				if (sk.Uuid !== null) {
					// memorizziamo badge e pin
					// dobbiamo trasformare tutto iln lower case o upper case ... scegliamo Lower case
					tmpBadges[sk.Uuid.toLowerCase()] = {
						keyType: sk.KeyType,
						uuid: sk.Uuid,
						expiresUtc: sk.ExpireTimeUtc,
						gpIds: sk.GenericPolicyIds
					}
				}
			}
		}
	});
	keys = tmpKeys;
	badges = tmpBadges;
}

// La presente classe rappresenta il concetto di chiave di accesso
function AccessKey(keyId, ts) {
	var kId = uuid.fromURN(keyId);
	if (kId == null){
		throw new Error("KeyId shall be a valid Guid string");
	}

	this.createMessage = function() {
		// return String.concat(_$tenantId, '\n', _$controllerId, '\n', kId.toString().toLowerCase(), '\n', ts.toString());
		return String().concat(_$tenantId, '\n', _$controllerId, '\n', kId.toString().toLowerCase(), '\n', ts.toString());
		// return String().concat(_$tenantId, '\n', _$controllerId, '\n', keyId, '\n', ts.toString());
	}

}

// questa funzione implementa esattamente la logica offline di processing di una richiesta di Unlock.
function _$processUnlockRequest(unlockRequest, typeOfDevice) {
	
    var sk = undefined;
	var now = new Date(); 
	var utcNow = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(),  now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds());
    switch (typeOfDevice) {
			
		case 'pin':
			// dobbiamo trovare il wiegand di riferiemnto a partire dalla seratura
			var idenWie = undefined;
			idenWie = retrieveWiegandFormList(unlockRequest.serraturaId);
			configCurrentWie = configWie[idenWie];

			logger.debug('OfflineUnlock - Unlock - pin ' + JSON.stringify(configCurrentWie));

		    var longValue = parseInt(unlockRequest.id,16);
			var bitRepresentation = longValue.toString(2);
			
			// logger.debug('bitrapresentation before padding ' +  bitRepresentation + ' lunghezza ' + bitRepresentation.length);

			while (bitRepresentation.length < (configCurrentWie.cnBit + configCurrentWie.fcBit))
			{
				bitRepresentation = "0" + bitRepresentation;
			}
			
			// logger.debug('bitrapresentation padded with 0 ' +  bitRepresentation);
			var fcNumberBit = bitRepresentation.substring(0, configCurrentWie.fcBit);
			
			// logger.debug('primo elemento ' + parseInt(fcNumberBit, 2) + ' secondo elemento ' + parseInt(configWie.fcNumber))
			
			if (parseInt(fcNumberBit, 2) != parseInt(configCurrentWie.fcNumber))
			{
    			throw new Error('Autorizzazione PIN Fallita: 404');
			}
			var cardNumberBit = bitRepresentation.substring(configCurrentWie.fcBit);
			unlockRequest.id = parseInt(cardNumberBit, 2).toString(16);
			sk = badges[unlockRequest.id];
			break;
		case 'badge':
			// sk = badges[unlockRequest.id.toLowerCase()];badgeUuid
			// var newBadgeId = unlockRequest.id.toLowerCase();
			// dobbiamo trovare il wiegand di riferiemnto a partire dalla seratura
			var idenWie = undefined;
			idenWie = retrieveWiegandFormList(unlockRequest.serraturaId);
			configCurrentWie = configWie[idenWie];

			logger.debug('OfflineUnlock - Unlock - badge ' + JSON.stringify(configCurrentWie));

			var newBadgeId = unlockRequest.badgeUuid.toLowerCase();
			sk = badges[newBadgeId];
			if (sk === undefined) {
				logger.error('OfflineUnlock - processUnlockrequest badge not found ' + newBadgeId);
				// la chiave non esiste.
				// Generiamo un errore compatibile con il comportamento delle API
				throw new Error('Autorizzazione Fallita: 404');
			}
			break;
		case 'phone': 
			var newKeyID = unlockRequest.keyId.toLowerCase();
			var i = (unlockRequest.keyId).indexOf('-');
			if (i == -1){
				newKeyID = unlockRequest.keyId.substr(0,8) + '-' + unlockRequest.keyId.substr(8,4) + '-' + unlockRequest.keyId.substr(12,4) + '-' + unlockRequest.keyId.substr(16,4) + '-'+ unlockRequest.keyId.substr(20);
				// logger.debug('OfflineUnlock - chiave ' + newKeyID);
			}
			//var uKeyId = uuid.fromURN(newKeyID);
			sk = keys[newKeyID];
			// logger.debug(keys);
			if (sk === undefined) {
				logger.error('OfflineUnlock - processUnlockrequest not found ' + newKeyID);
				// la chiave non esiste.
				// Generiamo un errore compatibile con il comportamento delle API
				throw new Error('Autorizzazione Fallita: 404');
			}
			// verifichiamo la validità della chiave implementando il medesimo ClockSkew di 30 secondi
			// (la chiave non deve essere scaduta o essere scaduta da meno di 30 secondi).
			// N.B.: ovviamente, in modalità offline, la scadenza è determinata sulla base
			// del clock del controller.	
			if ((sk.shs.expiresUtc - utcNow) < 30000) {
				return { result: "ShsExpired" };
			}

			// Creiamo la classe che ci consente di calcolare l'HMAC
			var hmac = crypto.createHmac("sha256", sk.shs.getSecret());
			// impostiamo il messaggio per il quale determinare l'HMAC generandolo via la logica implementata
			// dalla classe AccessKey
			hmac.update(new AccessKey(newKeyID, unlockRequest.ts).createMessage(), 'utf8');
			// calcoliamo l'HMAC
			var accessKeyBuffer = hmac.digest();
			hmac = null;
			// Confrontiamo l'HMAC calcolato con quello inviato nella richiesta
			if (accessKeyBuffer.compare(new Buffer(unlockRequest.accessKey, 'base64')) != 0) {
				return { result: "InvalidAccessKey" };
			}
			
			break;	
	}	
      		
	if (ValidateRequest(unlockRequest, sk, utcNow) === true) { // TODO Check policies
	// if (ValidateRequest(sk, utcNow) === true) { // TODO Check policies
		return { result: "OK" };
	}

	return { result: "AccessDenied" };
}

// function ValidateRequest(unlockData, currentDate){
function ValidateRequest(unlockData, currentKey, currentDate){
	var authorized = false;
	// var currentKey = keys[unlockData.keyId];
	var currenPolicy = undefined;
	// logger.debug('OfflineUnlock - ValidateRequest currentKey' + JSON.stringify(currentKey));
	// logger.debug('OfflineUnlock - ValidateRequest unlockData' + JSON.stringify(unlockData));
	/*
	var newKeyID = unlockData.keyId.toLowerCase();
	var i = (unlockData.keyId).indexOf('-');
	if (i == -1){
		newKeyID = unlockData.keyId.substr(0,8) + '-' + unlockData.keyId.substr(8,4) + '-' + unlockData.keyId.substr(12,4) + '-' + unlockData.keyId.substr(16,4) + '-'+ unlockData.keyId.substr(20);
	}
	
	var currentKey = keys[newKeyID];
	*/
	for (var i = 0; i < currentKey.gpIds.length ; i++){
		currentPolicy = policyMap[currentKey.gpIds[i]];
		for (var LocationId in currentPolicy.Location2Oggetto) {
			var OggettoId = currentPolicy.Location2Oggetto[LocationId];
			if (OggettoId == unlockData.serraturaId) 
			// if (OggettoId == currentKey.serraturaId) 
			{
				if (currentPolicy.IsEveryDay){
					return true;
				}
				else {
					logger.debug(currentPolicy.StartUtc);
					var Start = new Date(currentPolicy.StartUtc);
					var End = new Date(currentPolicy.EndUtc);
					var utcStart = new Date(Start.getUTCFullYear(), Start.getUTCMonth(), Start.getUTCDate(),  Start.getUTCHours(), Start.getUTCMinutes(), Start.getUTCSeconds());
					var utcEnd = new Date(End.getUTCFullYear(), End.getUTCMonth(), End.getUTCDate(),  End.getUTCHours(), End.getUTCMinutes(), End.getUTCSeconds());
					if ((currentDate >= utcStart) && (currentDate <= utcEnd)){
						return true;	
					}	

				}
			}
		}	
	}
	return false;
}

// Superfluo, implementato solo per compatibilità di interazione con la versione online
util.inherits(AccessRequest, events.EventEmitter);

// Rappresenta una richiesta di accesso offline
function AccessRequest() {
	if (_$tenantId == undefined) {
		throw new Error("Offline module Uninitialized. call init first!!");
	}
}

// Implementiamo la stessa interfaccia della versione online. La modalità offline
AccessRequest.prototype.SendRequestForAccess = function (payload, typeOfDevice, callback) {
	try {
		// Gotcha!!
		// La versione online si occupa di trasformare il payload della richiesta nell'equivalente
		// JSON. Per tale ragione è necessario riottenere l'oggetto parsando il JSON
		var unlockRequest = JSON.parse(payload);
		logger.debug('OfflineUnlock -  SendRequestForAccess - payload ' + payload);
		// return JSON.stringify(_$processUnlockRequest(unlockRequest));
		callback(null, JSON.stringify(_$processUnlockRequest(unlockRequest, typeOfDevice)));
	} catch (e) {
		callback(e);
	}
}

module.exports = {

	// Inizializza il modulo offline.
	// Nessuna delle funzionalità di tale modulo è invocabile senza aver prima correttamente
	// inizializzato il modulo
	init: function(tenantId, controllerId){
		// TODO verificare che il tenantId e il controllerId rappresentino correttamente dei GUID
		var tmp = null;
		
		if (tenantId == undefined || !('string' ==  typeof tenantId )
			|| (tmp = uuid.fromURN(tenantId)) == null) {
			throw new Error("tenantId shall be a valid string representing a GUID"); 
		}
		_$tenantId = tmp.toString().toLowerCase();
		tmp = null;
		if (controllerId == undefined || !('string' ==  typeof controllerId )
			|| (tmp = uuid.fromURN(controllerId)) == null) {
			throw new Error("controllerId shall be a valid string representing a GUID");
		}
		_$controllerId = tmp.toString().toLowerCase();
	},

	// La presente function fa parte dell'interfaccia di interazione con l'offline
	// si aspetta in input una opportuna struttura dati in formato JSON
	// che contenga: Un elenco di SmartKeys con associato il riferimento alle policies di accesso applicabili
	// per ogni chiave e di seguito una sequenza di policies.
	// La struttura attesa è la seguente:
	// {
	//	KeyList: [
	// 		{
	// 			CompanyId: <guid>,
	// 			KeyId: <guid>,
	// 			KeyType: <tipo di chiave>,
	// 			ExpireTimeUtc: <scadenza della chiave in UTC>,
	// 			Enabled: true,
	// 			Shs: <byte array>,
	// 			Uuid: null,
	// 			GenericPolicyIds: <array di riferimento alle policies
	//		}
	//	],
	//	GenericPolicyMap: {
	//
	//	}	
	// }
	sync: function (accessData) {
		if (_$tenantId == undefined) {
			throw new Error("Offline module Uninitialized. call init first!!");
		}
		if (accessData == null || accessData.KeyList === undefined || !(accessData.KeyList instanceof Array)
			|| accessData.GenericPolicyMap === undefined) {
			// TODO Log something
			logger.debug('OfflineUnlock - sync - Non ci sono dati da caricare in cache')
			return;
		}
		logger.debug('OfflineUnlock - sync - Caricamento dati in cache')
		_$internalSyncKeys(accessData.KeyList);
		policyMap = accessData.GenericPolicyMap;
	},

	// Esponiamo un oggetto che rappresenta una richiesta di accesso offline.
	// L'oggetto implementa la medesima interfaccia di una richiesta di accesso online
	// e può essere utilizzata in vece della versione online.
	offlineAccessRequest: AccessRequest
};