var http = require('http');
var https = require('https');
var tls = require('tls');
var url = require('url');
var fs = require('fs');
var path = require('path');
var zlib = require('zlib');

var _check302 = function (res) {
	var headers = res.headers
	var status = res.statusCode;
	var location = headers.location;
	return { status: [301, 302].indexOf(status) >= 0, location }
}

class HTTP {

	static FormatUrl({ requrl, query, params }) {
		if (params) {
			for (var k in params) {
				var reg = new RegExp(`\/:${k}`, "ig");
				requrl = requrl.replace(reg, `/${params[k]}`)
			}
		}
		query = query || {}
		var querys = []
		for (let k in query) {
			var v = query[k]
			if (v !== undefined && v !== null) {
				querys.push(`${k}=${v}`)
			}
		}
		var queryString = querys.join("&")
		if (queryString) {
			requrl = requrl + (requrl.indexOf('?') >= 0 ? '&' : '?') + queryString
		}
		return requrl;
	}

	static PrepareData(data) {
		if (data) {
			if (data.constructor.name == "Object" || data.constructor.name == "Array") {
				data = JSON.stringify(data);
			}
		}
		return data;
	}

	static GetOptions({ requrl, method, headers, auth, gzip }) {

		var _url = url.parse(requrl, true);
		var options = {
			host: null,
			port: -1,
			path: null
		};
		options.host = _url.hostname;
		options.port = _url.port;
		options.path = _url.pathname;
		options.method = method;
		options.timeout = 10000;
		options.headers = options.headers || {};

		if (headers) {
			Object.assign(options.headers, headers);
		}

		if (gzip) {
			Object.assign(options.headers, { "accept-encoding": "gzip" })
		}

		if (auth) {
			let authorization = "";
			if (auth.user && auth.pass) {
				authorization = "Basic " + Buffer.from(`${auth.user}:${auth.pass}`).toString('base64');
			}
			if (auth.bearer) {
				authorization = `Bearer ${auth.bearer}`;
			}
			if (authorization) {
				options.headers['authorization'] = authorization;
			}
		}

		_url.search && (options.path += _url.search)
		if (_url.protocol == "https:") {
			options.ecdhCurve = "auto"
			// options.ciphers = "ecdhe-ecdsa-aes128-gcm-sha256"  // tls.getCiphers()
		}
		return options;
	}

	static Send({ options, params, form, pipe = false, cert = [] }) {
		return new Promise(function (resolve, reject) {
			var timeStart = Date.now();

			var _http = null;
			if (options.ecdhCurve) {
				_http = https;
				options.checkServerIdentity = function (host, _cert) {
					try {
						var { pubkey, raw } = _cert
						if (pubkey) pubkey = pubkey.toString("base64")
						if (raw) raw = raw.toString("base64")
						var c = { pubkey, raw }
						c.issuer = _cert.issuer;
						c.host = options.host;
						c.from = new Date(_cert.valid_from)
						c.to = new Date(_cert.valid_to)
						cert.push(c);
					} catch (e) { }
					return undefined;
				}
			} else
				_http = http;

			var end = function (result) {
				try {
					if (result !== undefined)
						result = JSON.parse(result)
				} catch (e) { }

				return result;
			}
			var req = _http.request(options, function (res) {
				var resHeaders = res.headers;
				var contentEncoding = resHeaders['content-encoding'];
				// console.log("contentEncoding", contentEncoding)
				var gzip = false;
				if (contentEncoding == "gzip") {
					gzip = true;
				}
				if (pipe) {
					resolve(res);
				}
				if (gzip) {
					var gunzipStream = zlib.createGunzip();
					var result = '';
					var length = 0;
					res
						.on('data', (chunk) => {
							length += chunk.length;
						})
					res
						.pipe(gunzipStream)
						.on('data', (chunk) => {
							result += chunk;
						})
						.on('end', () => {
							resHeaders['content-length'] = length
							var rate = 0;
							try {
								rate = length / result.length;
							} catch (error) { }
							result = end(result);
							resolve({ statusCode: res.statusCode, body: result, time: Date.now() - timeStart, resHeaders, cert, rate })
						});
				}
				if (!pipe && !gzip) {
					res.setEncoding('utf8');
					var Is302 = _check302(res);
					if (Is302.status) {
						options.path = Is302.location;
						options.method = 'get';
						var _url = url.parse(options.path, true);
						options.host = _url.host;
						resolve(HTTP.Send({ options, params, form, pipe, cert }))
					} else {
						var result = '';
						res.on('data', function (chunk) {
							result += chunk;
						});
						res.on('end', function () {
							result = end(result);
							resolve({ statusCode: res.statusCode, body: result, time: Date.now() - timeStart, resHeaders, cert })
						});
						res.on('error', function (err) {
							end();
							let host = options.host
							let p = options.path
							let method = options.method
							let headers = options.headers
							resolve({ host, path: p, method, headers, statusCode: res.statusCode, body: err, time: Date.now() - timeStart, error: err, cert });
						});
					}
				}
			});

			if (options.timeout) {
				req.setTimeout(options.timeout, function () {
					req.emit('error', `TIMEOUT`);
				})
			}

			req
				.on('timeout', function () {
					// console.log("timeout", Date.now() - timeStart)
				})
				.on('error', function (err) {
					// console.log("error", Date.now() - timeStart)
					let host = options.host
					let p = options.path
					let method = options.method
					let headers = options.headers
					let time = Date.now() - timeStart;
					let body = null;
					let statusCode = null;
					if (err == `TIMEOUT`) {
						req.destroy();
						statusCode = 0;
					} else {
						statusCode = -1;
					}
					resolve({ host, path: p, method, headers, time, body, error: err, statusCode, cert })
				})
				.on('close', function () {
					this.destroy()
				})

			if (params) {
				req.write(params);
			}
			if (form) {
				form.pipe(req);
			}
			if (pipe === false) {
				if (form === undefined)
					req.end();
			} else {
				req.end();
			}

		});
	}

	static async Request({ requrl, method, data, query, params, headers, auth, timeout, pipe = false, rejectUnauthorized = true, gzip = false, opts = {} }) {
		requrl = this.FormatUrl({ requrl, query, params });
		data = this.PrepareData(data);
		method = (method || '').toLowerCase();
		var options = this.GetOptions({ requrl, method, headers, auth, gzip });
		if (data) {
			options.headers['Content-Length'] = Buffer.byteLength(data);
		}
		if (!isNaN(timeout) && timeout > 0) {
			options.timeout = timeout;
		}
		if (!rejectUnauthorized) options.rejectUnauthorized = false;
		Object.assign(options, opts);
		var resp = await this.Send({ options, params: data, pipe, gzip });
		resp.path = requrl;
		return resp;
	}

	static Post({ requrl, data, query, params, headers, pipe = false, gzip = false }) {
		return this.Request({ requrl, method: 'post', data, query, params, headers, pipe, gzip })
	}

	static Put({ requrl, data, query, params, headers, pipe = false, gzip = false }) {
		return this.Request({ requrl, method: 'put', data, query, params, headers, pipe, gzip })
	}

	static Get({ requrl, query, params, headers, pipe = false, gzip = false, rejectUnauthorized = true }) {
		return this.Request({ requrl, method: 'get', query, params, headers, pipe, gzip })
	}

	static Delete({ requrl, query, params, headers, pipe = false, gzip = false }) {
		return this.Request({ requrl, method: 'delete', query, params, headers, pipe, gzip })
	}

	static async Download(requrl, saveurl, notify) {
		let res = await this.Get({ requrl, pipe: true, rejectUnauthorized: false })
		var Is302 = _check302(res);
		while (Is302.status) {
			res = await this.Get({ requrl: Is302.location, pipe: true, rejectUnauthorized: false })
			Is302 = _check302(res);
		}
		if (res.statusCode <= 0 || res.statusCode >= 400) {
			throw new Error(`${requrl} 请求失败`)
		}
		if (res.statusCode >= 200 && res.statusCode < 400) {
			return new Promise((resolve) => {
				var receive = 0;
				var max = +res.headers['content-length'];
				var writer = fs.createWriteStream(saveurl, { flags: 'w+' });
				writer
					.on('error', function (err) {
						writer.end();
						fs.unlink(saveurl, () => { });
						resolve(err);
					})
					.on('close', async function () {
						resolve(true);
					})
				res.pipe(writer)
				res
					.on('error', function (err) {
						resolve(err);
					})
					.on('data', function (chunk) {
						if (notify) {
							receive += chunk.length;
							notify(receive, max);
						}
					})
					.on('end', async function () {
					})
			})
		}
	}

	static PostForm({ requrl, data, file, query, params, headers, auth, pipe = false }) {
		requrl = this.FormatUrl({ requrl, query, params });
		var FormData = require('form-data');
		var form = new FormData();
		if (data) {
			for (var k in data) {
				form.append(k, data[k]);
			}
		}
		if (file) {
			for (var k in file) {
				if (file[k].constructor == Object) {
					var { stream, filename, length } = file[k]
					form.append(k, stream, { filename, contentType: 'text/plain', knownLength: length });
				}
				if (file[k].constructor == String) {
					form.append(k, fs.createReadStream(file[k]));
				}
			}
		}
		var formHeader = form.getHeaders();
		Object.assign(formHeader, headers);
		var options = this.GetOptions({ requrl, method: 'post', headers: formHeader, auth });
		options.rejectUnauthorized = false;
		return this.Send({ options, form, pipe });
	}

	static ProxyTest({ requrl, proxy }) {
		return new Promise((resolve) => {
			var targetURI = url.parse(requrl)
			var proxyURI = url.parse(proxy)
			var enableTLS = targetURI.protocol === "https:"
			var port = targetURI.port || (enableTLS ? 443 : 80)
			var host = targetURI.hostname

			var options = {
				port: proxyURI.port,
				host: proxyURI.hostname,
				method: 'CONNECT',
				path: `${host}:${port}`
			};

			http
				.request(options)
				.on('connect', (res, socket, head) => {
					var _socket = socket;

					var resp = ""
					var statusCode = 0;
					var statusMessage = ""
					var msg = [
						`HEAD ${targetURI.pathname} HTTP/1.1`,
						`Host: ${host}`,
						`Connection: close`,
						``,
						``,
					].join('\r\n')

					if (enableTLS) {
						var tlsOpts = {
							headers: { host },
							servername: host,
							socket,
							checkServerIdentity: function (servername, _cert) { }
						}
						_socket = tls.connect(0, tlsOpts)
					}
					_socket
						.on('data', (chunk) => {
							resp += chunk
						})
						.on('end', () => {

						})
						.on('close', () => {
							if (resp) {
								var [p, s, ...m] = resp.split('\r\n')[0].split(' ');
								statusCode = parseInt(s)
								statusMessage = m.join(' ')
								resolve({ statusCode, statusMessage })
							}
						})
						.on('error', function (err) {

						})
						.write(msg)
				})
				.on('continue', function () { })
				.on('information', function () { })
				.on('response', function () { })
				.on('socket', function (socket) { })
				.end();
		})
	}

}

module.exports = HTTP;