var parseUrl = require("url").parse;
var querystring = require("./util/querystring");
var whenPromise = require("./promise").whenPromise;
var makeRequest = require("./http-client").request;
var crypto = require("crypto");

function encodeRfc3986(str){
	return !str ? "" : encodeURIComponent(str)
			.replace(/\!/g, "%21")
			.replace(/\'/g, "%27")
			.replace(/\(/g, "%28")
			.replace(/\)/g, "%29")
			.replace(/\*/g, "%2A");
}

function parseResponse(response){
	return response.body.join("").then(function(body){
		if(response.status == 200){
			return querystring.parse(body);
		}else{
			var err = new Error(response.status + ": " + body);
			err.status = response.status;
			err.headers = response.headers;
			err.body = body;
			throw err;
		}
	});
}

exports.Client = Client;
function Client(identifier, secret, tempRequestUrl, tokenRequestUrl, callback, version, signatureMethod, nonceGenerator, headers){
	this.identifier = identifier;
	this.tempRequestUrl = tempRequestUrl;
	this.tokenRequestUrl = tokenRequestUrl;
	this.callback = callback;
	this.version = version || false;
	// _createSignature actually uses the variable, not the instance property
	this.signatureMethod = signatureMethod = signatureMethod || "HMAC-SHA1";
	this.generateNonce = nonceGenerator || Client.makeNonceGenerator(32);
	this.headers = headers || Client.Headers;

	if(this.signatureMethod != "PLAINTEXT" && this.signatureMethod != "HMAC-SHA1"){
		throw new Error("Unsupported signature method: " + this.signatureMethod);
	}

	// We don't store the secrets on the instance itself, that way it can
	// be passed to other actors without leaking
	secret = encodeRfc3986(secret);
	this._createSignature = function(tokenSecret, baseString){
		if(baseString === undefined){
			baseString = tokenSecret;
			tokenSecret = "";
		}

		var key = secret + "&" + tokenSecret;
		if(signatureMethod == "PLAINTEXT"){
			return key;
		}else{
			return crypto.createHmac("SHA1", key).update(baseString).digest("base64");
		}
	};
}

Client.Headers = {
	Accept: "*/*",
	Connection: "close",
	"User-Agent": "promised-io/oauth"
};
// The default headers shouldn't change after clients have been created,
// but you're free to replace the object or pass headers to the Client
// constructor.
Object.freeze(Client.Headers);

Client.NonceChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Client.makeNonceGenerator = function(nonceSize){
	var nonce = Array(nonceSize + 1).join("-").split("");
	var chars = Client.NonceChars.split("");

	return function nonceGenerator(){
		return nonce.map(getRandomChar).join("");
	};

	function getRandomChar(){
		return chars[Math.floor(Math.random() * chars.length)];
	}
};

Client.getTimestamp = function(){
	return Math.floor(Date.now() / 1000).toString();
};

// Binds the client against a set of token credentials.
// The resulting object can be used to make signed requests.
// The secret won't be exposed on the object itself.
Client.prototype.bind = function(tokenIdentifier, tokenSecret){
	var bound = {
		identifier: this.identifier,
		tokenIdentifier: tokenIdentifier,
		signatureMethod: this.signatureMethod,
		version: this.version,
		headers: this.headers,
		generateNonce: this.generateNonce
	};
	bound._createSignature = this._createSignature.bind(this, encodeRfc3986(tokenSecret));
	bound._createSignatureBase = this._createSignatureBase;
	bound._normalizeUrl = this._normalizeUrl;
	bound._collectOAuthParams = this._collectOAuthParams;
	bound._normalizeParams = this._normalizeParams;
	bound._signRequest = this._signRequest;
	bound.request = this.request;
	return bound;
};

// Wrapper for `http-client.request` which signs the request
// with the client credentials, and optionally token credentials if bound.
Client.prototype.request = function(originalRequest){
	var request = {};
	for(var key in originalRequest){
		if(originalRequest.hasOwnProperty(key)){
			request[key] = originalRequest[key];
		}
	}

	// Normalize the request. `engines/node/http-client.request` is
	// quite flexible, but this should do it.
	if(request.url){
		var parsed = parseUrl(request.url);
		parsed.pathInfo = parsed.pathname;
		parsed.queryString = parsed.query;
		for(var i in parsed){
			request[i] = parsed[i];
		}
	}
	request.pathname = request.pathname || request.pathInfo || "/";
	request.queryString = request.queryString || request.query || "";
	request.method = (request.method || "GET").toUpperCase();
	request.protocol = request.protocol.toLowerCase();
	request.hostname = (request.host || request.hostname).toLowerCase();
	request.headers = {};
	for(var h in this.headers){
		request.headers[h] = this.headers[h];
	}
	for(var h in originalRequest.headers){
		request.headers[h] = originalRequest.headers[h];
	}
	// We'll be setting the Authorization header; due to how `engines/node/http-client.request`
	// is implemented we need to set the Host header as well.
	request.headers.host = request.headers.host || request.hostname + (request.port ? ":" + request.port : "");

	// Parse all request parameters into a flattened array of parameter pairs.
	// Note that this array contains munged parameter names.
	var requestParams = [], uncombinedRequestParams = [];
	// Start with parameters that were defined in the query string
	if(request.queryString){
		querystring.parseToArray(requestParams, request.queryString);
	}
	// Allow parameters to be defined in object notation, this is *not* part of `http-client.request`!
	// It saves an extra stringify+parse though.
	if(request.requestParams){
		for(var i in request.requestParams){
			if(request.requestParams.hasOwnProperty(i)){
				querystring.addToArray(uncombinedRequestParams, i, request.requestParams[i]);
			}
		}
	}
	// Send the parameters from `request.requestParams` in the query string
	// for GET and DELETE requests. We immediately concat to the `requestParams` array,
	// which is then built into the query string.
	if(request.method == "GET" || request.method == "DELETE"){
		requestParams = requestParams.concat(uncombinedRequestParams);
	}
	// Rebuild the query string
	request.queryString = requestParams.reduce(function(qs, v, i){
		return qs + (i % 2 ? "=" + querystring.escape(v) : (qs.length ? "&" : "") + querystring.escape(v));
	}, "");

	// Depending on the request content type, look for request parameters in the body
	var waitForBody = false;
	if(request.headers && request.headers["Content-Type"] == "application/x-www-form-urlencoded"){
		waitForBody = whenPromise(request.body.join(""), function(body){
			querystring.parseToArray(requestParams, body);
			return body;
		});
	}

	// If we're a POST or PUT and are not sending any content, or are sending urlencoded content,
	// add the `request.request` to the request body. If we are sending non-urlencoded content through
	// a POST or PUT, the `request.requestParams` are ignored.
	if(request.requestParams && (request.method == "POST" || request.method == "PUT") && (!request.headers || !request.headers["Content-Type"] || request.headers["Content-Type"] == "application/x-www-form-urlencoded")){
		waitForBody = whenPromise(waitForBody, function(body){
			requestParams = requestParams.concat(uncombinedRequestParams);
			body = (body ? body + "&" : "") + querystring.stringify(request.requestParams);
			request.body = [body];
			request.headers["Content-Type"] = "application/x-www-form-urlencoded";
		});
	}

	// Sign the request and then actually make it.
	return whenPromise(waitForBody, function(){
		this._signRequest(request, requestParams);
		return makeRequest(request);
	}.bind(this));
};

Client.prototype._normalizeUrl = function(request){
	var normalized = request.protocol + "//" + request.hostname;
	if(request.protocol == "http:" && request.port && (request.port + "") != "80"){
		normalized += ":" + request.port;
	}
	if(request.protocol == "https:" && request.port && (request.port + "") != "443"){
		normalized += ":" + request.port;
	}
	return normalized + request.pathname;
};

Client.prototype._collectOAuthParams = function(request, requestParams){
	var oauthParams = {};
	if(request.oauthParams){
		for(var p in request.oauthParams){
			// Don't allow `request.oauthParams` to override standard values.
			// `oauth_token` and `oauth_version` are conditionally added,
			// the other parameters are always set. Hence we just test for
			// the first two.
			if(p != "oauth_token" && p != "oauth_version"){
				oauthParams[p] = request.oauthParams[p];
			}
		}
	}
	oauthParams.oauth_consumer_key = this.identifier;
	oauthParams.oauth_signature_method = this.signatureMethod;
	oauthParams.oauth_timestamp = Client.getTimestamp();
	oauthParams.oauth_nonce = this.generateNonce();
	if(this.tokenIdentifier){
		oauthParams.oauth_token = this.tokenIdentifier;
	}
	if(this.version){
		oauthParams.oauth_version = this.version;
	}
	for(var i in oauthParams){
		requestParams.push(i, oauthParams[i]);
	}
	return oauthParams;
};

Client.prototype._normalizeParams = function(requestParams){
	// Encode requestParams
	requestParams = requestParams.map(encodeRfc3986);
	// Unflatten the requestParams for sorting
	requestParams = requestParams.reduce(function(result, _, i, arr){
		if(i % 2 == 0){
			result.push(arr.slice(i, i + 2));
		}
		return result;
	}, []);
	// Sort the unflattened requestParams
	requestParams.sort(function(a, b){
		if(a[0] == b[0]){
			return a[1] < b[1] ? -1 : 1;
		}else{
			return a[0] < b[0] ? -1 : 1;
		}
	});
	return requestParams.map(function(pair){ return pair.join("="); }).join("&");
};

Client.prototype._createSignatureBase = function(requestMethod, baseUri, params){
	return [requestMethod, baseUri, params].map(encodeRfc3986).join("&");
};

Client.prototype._signRequest = function(request, requestParams){
	// Calculate base URI string
	var baseUri = this._normalizeUrl(request);

	// Register OAuth parameters and add to the request parameters
	// Additional parameters can be specified via the `request.oauthParams` object
	var oauthParams = this._collectOAuthParams(request, requestParams);

	// Generate parameter string
	var params = this._normalizeParams(requestParams);

	// Sign the base string
	var baseString = this._createSignatureBase(request.method, baseUri, params);
	oauthParams.oauth_signature = this._createSignature(baseString);

	// Add Authorization header
	request.headers.authorization = "OAuth " + Object.keys(oauthParams).map(function(name){
		return encodeRfc3986(name) + "=\"" + encodeRfc3986(oauthParams[name]) + "\"";
	}).join(",");

	// Now the request object can be used to make a signed request
	return request;
};

Client.prototype.obtainTempCredentials = function(oauthParams, extraParams){
	oauthParams = oauthParams || {};
	if(this.callback && !oauthParams.oauth_callback){
		oauthParams.oauth_callback = this.callback;
	}

	return this.request({
		method: "POST",
		url: this.tempRequestUrl,
		oauthParams: oauthParams,
		requestParams: extraParams || {}
	}).then(parseResponse);
};

Client.prototype.obtainTokenCredentials = function(tokenIdentifier, tokenSecret, verifierToken, extraParams){
	return this.bind(tokenIdentifier, tokenSecret).request({
		method: "POST",
		url: this.tokenRequestUrl,
		oauthParams: { oauth_verifier: verifierToken },
		requestParams: extraParams
	}).then(parseResponse);
};
