/* globals SAML:true */

const zlib = Npm.require('zlib');
const xml2js = Npm.require('xml2js');
const xmlCrypto = Npm.require('xml-crypto');
const crypto = Npm.require('crypto');
const xmldom = Npm.require('xmldom');
const querystring = Npm.require('querystring');
const xmlbuilder = Npm.require('xmlbuilder');

// var prefixMatch = new RegExp(/(?!xmlns)^.*:/);


SAML = function(options) {
	this.options = this.initialize(options);
};

// var stripPrefix = function(str) {
// 	return str.replace(prefixMatch, '');
// };

SAML.prototype.initialize = function(options) {
	if (!options) {
		options = {};
	}

	if (!options.protocol) {
		options.protocol = 'https://';
	}

	if (!options.path) {
		options.path = '/saml/consume';
	}

	if (!options.issuer) {
		options.issuer = 'onelogin_saml';
	}

	if (options.identifierFormat === undefined) {
		options.identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress';
	}

	if (options.authnContext === undefined) {
		options.authnContext = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport';
	}

	return options;
};

SAML.prototype.generateUniqueID = function() {
	const chars = 'abcdef0123456789';
	let uniqueID = '';
	for (let i = 0; i < 20; i++) {
		uniqueID += chars.substr(Math.floor((Math.random() * 15)), 1);
	}
	return uniqueID;
};

SAML.prototype.generateInstant = function() {
	return new Date().toISOString();
};

SAML.prototype.signRequest = function(xml) {
	const signer = crypto.createSign('RSA-SHA1');
	signer.update(xml);
	return signer.sign(this.options.privateKey, 'base64');
};

SAML.prototype.generateAuthorizeRequest = function(req) {
	let id = `_${ this.generateUniqueID() }`;
	const instant = this.generateInstant();

	// Post-auth destination
	let callbackUrl;
	if (this.options.callbackUrl) {
		callbackUrl = this.options.callbackUrl;
	} else {
		callbackUrl = this.options.protocol + req.headers.host + this.options.path;
	}

	if (this.options.id) {
		id = this.options.id;
	}

	let request =
		`<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="${ id }" Version="2.0" IssueInstant="${ instant
		}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="${ callbackUrl }" Destination="${
		this.options.entryPoint }">` +
		`<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${ this.options.issuer }</saml:Issuer>\n`;

	if (this.options.identifierFormat) {
		request += `<samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Format="${ this.options.identifierFormat
			}" AllowCreate="true"></samlp:NameIDPolicy>\n`;
	}

	request +=
		'<samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Comparison="exact">' +
		'<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n' +
		'</samlp:AuthnRequest>';

	return request;
};

SAML.prototype.generateLogoutRequest = function(options) {
	// options should be of the form
	// nameId: <nameId as submitted during SAML SSO>
	// sessionIndex: sessionIndex
	// --- NO SAMLsettings: <Meteor.setting.saml  entry for the provider you want to SLO from

	const id = `_${ this.generateUniqueID() }`;
	const instant = this.generateInstant();

	let request = `${ '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' +
		'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="' }${ id }" Version="2.0" IssueInstant="${ instant
		}" Destination="${ this.options.idpSLORedirectURL }">` +
		`<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${ this.options.issuer }</saml:Issuer>` +
		`<saml:NameID Format="${ this.options.identifierFormat }">${ options.nameID }</saml:NameID>` +
		'</samlp:LogoutRequest>';

	request = `${ '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"  ' +
		'ID="' }${ id }" ` +
		'Version="2.0" ' +
		`IssueInstant="${ instant }" ` +
		`Destination="${ this.options.idpSLORedirectURL }" ` +
		'>' +
		`<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${ this.options.issuer }</saml:Issuer>` +
		'<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ' +
		'NameQualifier="http://id.init8.net:8080/openam" ' +
		`SPNameQualifier="${ this.options.issuer }" ` +
		`Format="${ this.options.identifierFormat }">${
		options.nameID }</saml:NameID>` +
		`<samlp:SessionIndex xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">${ options.sessionIndex }</samlp:SessionIndex>` +
		'</samlp:LogoutRequest>';
	if (Meteor.settings.debug) {
		console.log('------- SAML Logout request -----------');
		console.log(request);
	}
	return {
		request,
		id
	};
};

SAML.prototype.requestToUrl = function(request, operation, callback) {
	const self = this;
	zlib.deflateRaw(request, function(err, buffer) {
		if (err) {
			return callback(err);
		}

		const base64 = buffer.toString('base64');
		let target = self.options.entryPoint;

		if (operation === 'logout') {
			if (self.options.idpSLORedirectURL) {
				target = self.options.idpSLORedirectURL;
			}
		}

		if (target.indexOf('?') > 0) {
			target += '&';
		} else {
			target += '?';
		}

		// TBD. We should really include a proper RelayState here
		let relayState;
		if (operation === 'logout') {
			// in case of logout we want to be redirected back to the Meteor app.
			relayState = Meteor.absoluteUrl();
		} else {
			relayState = self.options.provider;
		}

		const samlRequest = {
			SAMLRequest: base64,
			RelayState: relayState
		};

		if (self.options.privateCert) {
			samlRequest.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
			samlRequest.Signature = self.signRequest(querystring.stringify(samlRequest));
		}

		target += querystring.stringify(samlRequest);

		if (Meteor.settings.debug) {
			console.log(`requestToUrl: ${ target }`);
		}
		if (operation === 'logout') {
			// in case of logout we want to be redirected back to the Meteor app.
			return callback(null, target);

		} else {
			callback(null, target);
		}
	});
};

SAML.prototype.getAuthorizeUrl = function(req, callback) {
	const request = this.generateAuthorizeRequest(req);

	this.requestToUrl(request, 'authorize', callback);
};

SAML.prototype.getLogoutUrl = function(req, callback) {
	const request = this.generateLogoutRequest(req);

	this.requestToUrl(request, 'logout', callback);
};

SAML.prototype.certToPEM = function(cert) {
	cert = cert.match(/.{1,64}/g).join('\n');
	cert = `-----BEGIN CERTIFICATE-----\n${ cert }`;
	cert = `${ cert }\n-----END CERTIFICATE-----\n`;
	return cert;
};

// functionfindChilds(node, localName, namespace) {
// 	var res = [];
// 	for (var i = 0; i < node.childNodes.length; i++) {
// 		var child = node.childNodes[i];
// 		if (child.localName === localName && (child.namespaceURI === namespace || !namespace)) {
// 			res.push(child);
// 		}
// 	}
// 	return res;
// }

SAML.prototype.validateSignature = function(xml, cert) {
	const self = this;

	const doc = new xmldom.DOMParser().parseFromString(xml);
	const signature = xmlCrypto.xpath(doc, '//*[local-name(.)=\'Signature\' and namespace-uri(.)=\'http://www.w3.org/2000/09/xmldsig#\']')[0];

	const sig = new xmlCrypto.SignedXml();

	sig.keyInfoProvider = {
		getKeyInfo(/*key*/) {
			return '<X509Data></X509Data>';
		},
		getKey(/*keyInfo*/) {
			return self.certToPEM(cert);
		}
	};

	sig.loadSignature(signature);

	return sig.checkSignature(xml);
};

SAML.prototype.getElement = function(parentElement, elementName) {
	if (parentElement[`saml:${ elementName }`]) {
		return parentElement[`saml:${ elementName }`];
	} else if (parentElement[`samlp:${ elementName }`]) {
		return parentElement[`samlp:${ elementName }`];
	} else if (parentElement[`saml2p:${ elementName }`]) {
		return parentElement[`saml2p:${ elementName }`];
	} else if (parentElement[`saml2:${ elementName }`]) {
		return parentElement[`saml2:${ elementName }`];
	}
	return parentElement[elementName];
};

SAML.prototype.validateLogoutResponse = function(samlResponse, callback) {
	const self = this;

	const compressedSAMLResponse = new Buffer(samlResponse, 'base64');
	zlib.inflateRaw(compressedSAMLResponse, function(err, decoded) {

		if (err) {
			if (Meteor.settings.debug) {
				console.log(err);
			}
		} else {
			const parser = new xml2js.Parser({
				explicitRoot: true
			});
			parser.parseString(decoded, function(err, doc) {
				const response = self.getElement(doc, 'LogoutResponse');

				if (response) {
					// TBD. Check if this msg corresponds to one we sent
					const inResponseTo = response.$.InResponseTo;
					if (Meteor.settings.debug) {
						console.log(`In Response to: ${ inResponseTo }`);
					}
					const status = self.getElement(response, 'Status');
					const statusCode = self.getElement(status[0], 'StatusCode')[0].$.Value;
					if (Meteor.settings.debug) {
						console.log(`StatusCode: ${ JSON.stringify(statusCode) }`);
					}
					if (statusCode === 'urn:oasis:names:tc:SAML:2.0:status:Success') {
						// In case of a successful logout at IDP we return inResponseTo value.
						// This is the only way how we can identify the Meteor user (as we don't use Session Cookies)
						callback(null, inResponseTo);
					} else {
						callback('Error. Logout not confirmed by IDP', null);
					}
				} else {
					callback('No Response Found', null);
				}
			});
		}

	});
};

SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
	const self = this;
	const xml = new Buffer(samlResponse, 'base64').toString('utf8');
	// We currently use RelayState to save SAML provider
	if (Meteor.settings.debug) {
		console.log(`Validating response with relay state: ${ xml }`);
	}
	const parser = new xml2js.Parser({
		explicitRoot: true
	});

	parser.parseString(xml, function(err, doc) {
		// Verify signature
		if (Meteor.settings.debug) {
			console.log('Verify signature');
		}
		if (self.options.cert && !self.validateSignature(xml, self.options.cert)) {
			if (Meteor.settings.debug) {
				console.log('Signature WRONG');
			}
			return callback(new Error('Invalid signature'), null, false);
		}
		if (Meteor.settings.debug) {
			console.log('Signature OK');
		}
		const response = self.getElement(doc, 'Response');
		if (Meteor.settings.debug) {
			console.log('Got response');
		}
		if (response) {
			const assertion = self.getElement(response, 'Assertion');
			if (!assertion) {
				return callback(new Error('Missing SAML assertion'), null, false);
			}

			const profile = {};

			if (response.$ && response.$.InResponseTo) {
				profile.inResponseToId = response.$.InResponseTo;
			}

			const issuer = self.getElement(assertion[0], 'Issuer');
			if (issuer) {
				profile.issuer = issuer[0]._;
			}

			const subject = self.getElement(assertion[0], 'Subject');

			if (subject) {
				const nameID = self.getElement(subject[0], 'NameID');
				if (nameID) {
					profile.nameID = nameID[0]._;

					if (nameID[0].$.Format) {
						profile.nameIDFormat = nameID[0].$.Format;
					}
				}
			}

			const authnStatement = self.getElement(assertion[0], 'AuthnStatement');

			if (authnStatement) {
				if (authnStatement[0].$.SessionIndex) {

					profile.sessionIndex = authnStatement[0].$.SessionIndex;
					if (Meteor.settings.debug) {
						console.log(`Session Index: ${ profile.sessionIndex }`);
					}
				} else if (Meteor.settings.debug) {
					console.log('No Session Index Found');
				}


			} else if (Meteor.settings.debug) {
				console.log('No AuthN Statement found');
			}

			const attributeStatement = self.getElement(assertion[0], 'AttributeStatement');
			if (attributeStatement) {
				const attributes = self.getElement(attributeStatement[0], 'Attribute');

				if (attributes) {
					attributes.forEach(function(attribute) {
						const value = self.getElement(attribute, 'AttributeValue');
						if (typeof value[0] === 'string') {
							profile[attribute.$.Name] = value[0];
						} else {
							profile[attribute.$.Name] = value[0]._;
						}
					});
				}

				if (!profile.mail && profile['urn:oid:0.9.2342.19200300.100.1.3']) {
					// See http://www.incommonfederation.org/attributesummary.html for definition of attribute OIDs
					profile.mail = profile['urn:oid:0.9.2342.19200300.100.1.3'];
				}

				if (!profile.email && profile.mail) {
					profile.email = profile.mail;
				}
			}

			if (!profile.email && profile.nameID && profile.nameIDFormat && profile.nameIDFormat.indexOf('emailAddress') >= 0) {
				profile.email = profile.nameID;
			}
			if (Meteor.settings.debug) {
				console.log(`NameID: ${ JSON.stringify(profile) }`);
			}

			callback(null, profile, false);
		} else {
			const logoutResponse = self.getElement(doc, 'LogoutResponse');

			if (logoutResponse) {
				callback(null, null, true);
			} else {
				return callback(new Error('Unknown SAML response message'), null, false);
			}

		}
	});
};

let decryptionCert;
SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) {

	let keyDescriptor = null;

	if (!decryptionCert) {
		decryptionCert = this.options.privateCert;
	}

	if (this.options.privateKey) {
		if (!decryptionCert) {
			throw new Error(
				'Missing decryptionCert while generating metadata for decrypting service provider');
		}

		decryptionCert = decryptionCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, '');
		decryptionCert = decryptionCert.replace(/-+END CERTIFICATE-+\r?\n?/, '');
		decryptionCert = decryptionCert.replace(/\r\n/g, '\n');

		keyDescriptor = {
			'ds:KeyInfo': {
				'ds:X509Data': {
					'ds:X509Certificate': {
						'#text': decryptionCert
					}
				}
			},
			'#list': [
				// this should be the set that the xmlenc library supports
				{
					'EncryptionMethod': {
						'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
					}
				},
				{
					'EncryptionMethod': {
						'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
					}
				},
				{
					'EncryptionMethod': {
						'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'
					}
				}
			]
		};
	}

	if (!this.options.callbackUrl && !callbackUrl) {
		throw new Error(
			'Unable to generate service provider metadata when callbackUrl option is not set');
	}

	const metadata = {
		'EntityDescriptor': {
			'@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata',
			'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
			'@entityID': this.options.issuer,
			'SPSSODescriptor': {
				'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
				'KeyDescriptor': keyDescriptor,
				'SingleLogoutService': {
					'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
					'@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`,
					'@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`
				},
				'NameIDFormat': this.options.identifierFormat,
				'AssertionConsumerService': {
					'@index': '1',
					'@isDefault': 'true',
					'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
					'@Location': callbackUrl
				}
			}
		}
	};

	return xmlbuilder.create(metadata).end({
		pretty: true,
		indent: '  ',
		newline: '\n'
	});
};
