var os = require("os"),
fs = require("fs"),
path = require("path"),
url = require("url"),
http = require("http"),
https = require("https"),
zlib = require("zlib"),
cookie = "",
//cookie = "sid_tt=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
//user_agent = "okhttp",
user_agent = "com.zhiliaoapp.musically/2022400030 (Linux; U; Android 7.1.2; en_US; SM-G973N; Build/PPR1.190810.011;tt-ok/3.10.0.2)",
//user_agents_random = false,
user_agents_random = true,
help = "node " + path.basename(__filename) + " t=following|follower i=0 [m=\"metadata_file.json\"] [skip_empty] [v] [r] [l=-1] [o=0] [w=200] [c=1] [x] [y] [a] [n=100] [error_delay=1000] [j=\"sid_tt=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"] [ignore_known] [ignore_banned] [aweme_host=\"*.tiktokv.com\"] [debug]"
+ os.EOL + "t - following (users user is following)"
+ os.EOL + "    follower (users following user)"
+ os.EOL + "i - id of a user"
+ os.EOL + "m - save metadata to file - if not passed an output file then uses a default construction"
+ os.EOL + "v - unfiltered metadata"
+ os.EOL + "r - reverse order (list sorted oldest>newest)"
+ os.EOL + "l - max number of items to retrieve (integer)"
+ os.EOL + "o - cursor (integer) - unix time in milliseconds"
+ os.EOL + "w - cursor batch size"
+ os.EOL + "c - concurrent requests"
+ os.EOL + "x - log current cursor - if passed a value then also shows time elapsed between cursors"
+ os.EOL + "y - override the limit set by the \"l\" flag to finish the final cursor's batch"
+ os.EOL + "a - abort if trying to fetch followers for an account with >5k (api limit)"
+ os.EOL + "p - if aborted from flag \"a\", save output as [0]"
+ os.EOL + "n - re-attempt requests on server error - takes a max amount of errors before a success to ignore (0 = no limit)"
+ os.EOL + "j - private cookie from \".tiktok.com\""
+ os.EOL
+ os.EOL + "ignore_known - skip saving files to disk if the contents are an empty array"
+ os.EOL + "ignore_banned - skip saving files to disk if the contents are an empty array"
+ os.EOL
+ os.EOL + "debug - shows http requests"
+ os.EOL + "aweme_host - host to attempt first for api calls"
+ os.EOL + "skip_empty - skip saving files to disk if the contents are an empty array"
+ os.EOL + "error_delay - interval between re-attempts with flag \"n\" (ms)",
limit = -1,
break_limit = false,
cursor = 0,
current_cursor = cursor,
current_cursor_count = 0,
current_cursor_requests = [],
cursor_print = false,
cursor_times = false,
cursors_printed = new Set(),
count = 200,
max_threads = 1,
reverse_order = false,
ignore_errors = false,
//ignore_errors_delay = 1000,
ignore_errors_delay = 0,
ignore_errors_limit = 100,
ignore_errors_count = 0,
debug = false,
skip_empty = false,
abort_on_cap = false,
abort_on_cap_log = false,
ignore_known = false,
ignore_banned = false,
metadata,
metadata_file,
metadata_verbose = false,
example_metadata,
running = true,
timeout = null,
idRegex = /^[1-9][0-9]{0,19}$/,
aweme_hosts = ["api16-normal-c-useast1a.tiktokv.com","api16-normal-c-useast2a.tiktokv.com","api16-normal-useast5.us.tiktokv.com","api19-normal-c-useast1a.tiktokv.com","api-va.tiktokv.com","api-t2.tiktokv.com","api-h2.tiktokv.com","api.tiktokv.com","tiktokv.com"],
deviceId = generateDeviceId(),
//iId = generateDeviceId(),
deviceVersion = generateBase62(8);
function generateBase62(length) {
	var i = 0, r = "", base62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	while (i < length) {
		r += base62.charAt(Math.floor(Math.random() * 62));
		i++;
	}
	return r;
}
function generateDeviceId() {
	var i = 1, a = new Array(19);//Math.floor(Math.random() * 9) + 11
	a[0] = 54;//Math.floor(Math.random() * 9) + 49
	while (i < a.length) {
		a[i++] = Math.floor(Math.random() * 10) + 48;
	}
	return String.fromCharCode.apply(null, a);
}
function generateVerify() {
	var i = 0, d = new Date(), t = d.getTime() - d.getTimezoneOffset() * 60000, r = "", base62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	do {
		switch (i) {
			case 8:
			case 13:
			case 18:
			case 23:
				r += "_";
				break;
			case 14:
				r += "4";
				break;
			default:
				r += base62.charAt(Math.floor(Math.random() * 62));
				break;
		}
		i++;
	} while (i < 36);
	return "verify_" + t.toString(36) + "_" + r;
}
function generateCookie() {
	var t = generateDeviceId();
	return "tt_webid=" + t + "; tt_webid_v2=" + t + "; tt_csrf_token=" + generateBase62(24) + "; s_v_web_id=" + generateVerify();
}
function parseArgv(arguments) {
	var i = 2, e, r = {};
	while (i < arguments.length) {
		e = arguments[i].indexOf("=");
		if (e === -1) {
			r[arguments[i]] = "";
		} else {
			r[arguments[i].substring(0, e)] = arguments[i].substring(e + 1);
		}
		i++;
	}
	return r;
}
function random_user_agent() {
	return "com.zhiliaoapp.musically/" + Math.random().toString().substr(2,10) + " (Linux; U; Android 7.1.2; en_US; SM-G973N; Build/PPR1.190810.011;tt-ok/3.10.0.2)";
}
function requestGet(uri, headers, callback) {
	try {
		var u = typeof uri === "string" ? new URL(uri) : uri, h, r;
		if (u.protocol === "http:") {
			h = http;
		} else if (u.protocol === "https:") {
			h = https;
		} else {
			process.nextTick(callback, new Error("Only http and https is supported."));
			return;
		}
		if (debug) console.log("http/get: " + String(uri));
		r = h.request({
			method: "GET",
			host: u.host,
			port: +u.port || 0,
			path: u.pathname + u.search,//+ u.hash
			timeout: 10000,
			headers: {
				...headers,
				"Cookie": cookie + (cookie.length > 0 ? "; " : "") + generateCookie(),
				"User-Agent": user_agents_random ? random_user_agent() : user_agent
			}
		}, function(response) {
			if (callback === null) {
				return;
			} else if (response.statusCode >= 300 && response.statusCode < 400) {
				var l = response.headers["location"];
				if (l === undefined) {
					callback(new Error("Redirection without location."));
					callback = null;
				} else {
					try {
						if (debug) console.log("http/" + response.statusCode + ": " + String(uri));
						requestGet(new URL(l, String(uri)), headers, callback);
					} catch (e) {
						callback(e);
						callback = null;
					}
				}
				return;
			} else if (response.statusCode !== 200) {
				callback(new Error(response.statusMessage));
				callback = null;
				return;
			}
			if (debug) console.log("http/" + response.statusCode + (!isNaN(+response.headers["content-length"]) ? " (" + response.headers["content-length"] + " bytes)" : "") + ": " + String(uri));
			callback(null, response, r, String(uri));
			callback = null;
		}).on("error", function(error) {
			if (callback === null) {
				return;
			}
			callback(error);
			callback = null;
		}).on("timeout", function() {
			r.destroy();
			console.error("timed_out: " + String(uri));
		}).end();
		current_cursor_requests.push(r);
	} catch (e) {
		process.nextTick(callback, e);
		return;
	}
}
function requestGetJson(uri, headers, callback) {
	requestGet(uri, {
		...headers,
		"Accept-Encoding": "gzip, deflate, *"
	}, function(error, response, request) {
		if (error !== null) {
			callback(error);
			return;
		}
		var data_stream, d = "";
		switch(response.headers["content-encoding"]) {
			case "gzip":
				data_stream = zlib.createGunzip();
				response.pipe(data_stream);
				break;
			case "deflate":
				data_stream = zlib.createInflate();
				response.pipe(data_stream);
				break;
			default:
				data_stream = response;
				data_stream.setEncoding("utf8");
				break;
		}
		data_stream.on("data", function(data) {
			d += data;
		}).on("error", function(error) {
			if (callback === null) {
				return;
			}
			callback(error);
			callback = null;
		}).on("end", function() {
			if (callback === null) {
				return;
			}
			if (d.length === 0) {
				callback(new Error("Empty response."));
				return;
			}
			try {
				var d2 = d.replace(/([\[:])?(?<![\d.]|\\".)(\d{15,})([,\}\]])/g, "$1\"$2\"$3"); // rancid
				callback(null, JSON.parse(d2));
			} catch (e) {
				try {
					callback(null, JSON.parse(d));
//					console.error(new Error("RegEx for preserving big numbers failed"));
//					fs.writeFileSync("/" + path.basename(__filename) + "_" + output_time + "_pass.json", d);
//					fs.writeFileSync("/" + path.basename(__filename) + "_" + output_time + "_fail.json", d2);
				} catch (e) {
					callback(e);
				}
			}
			callback = null;
		});
	});
}
function requestAweme(uri, thread_cursor, callback) {
	var f = function(error, repeat) {
		if (thread_cursor !== current_cursor || repeat > (aweme_hosts.length*5) || !running) {
			callback(error !== null ? error : new Error("Exited unexpectedly."), thread_cursor);
			return;
		}
		var d = new Date(), t = d.getTime() - d.getTimezoneOffset() * 60000, h = aweme_hosts[repeat % aweme_hosts.length];
		requestGetJson("https://" + h + uri
		+ "&address_book_access=1"
		+ "&region=US"
		+ "&ts=" + Math.floor(t / 1000)
		+ "&timezone_name=Etc%2FGMT"
		+ "&device_type=Pixel%20" + deviceVersion
//		+ "&iid=" + iId
		+ "&locale=en"
		+ "&app_type=normal"
		+ "&resolution=1080*1920"
		+ "&aid=1233"
		+ "&app_name=musical_ly"
		+ "&_rticket=" + t
		+ "&device_platform=android"
		+ "&dpi=441"
		+ "&cpu_support64=false"
		+ "&sys_region=US"
		+ "&timezone_offset=0"
		+ "&device_id=" + deviceId
		+ "&pass-route=1"
		+ "&device_brand=google"
		+ "&os_version=10"
		+ "&op_region=US"
		+ "&app_language=en"
		+ "&pass-region=1"
		+ "&language=en"
		+ "&channel=googleplay"
		+ "&manifest_version_code=240903"
		+ "&update_version_code=240903"
		+ "&version_code=240903"
		+ "&ab_version=24.9.3"
		+ "&build_number=24.9.3"
		+ "&version_name=24.9.3", {}, function(error, data) {
			if (error === null) {
				if (typeof data !== "object" || data === null || typeof data.status_code !== "number") {
					error = new Error("Unexpected response.");
				} else if (![0,9,204,2096,3170,3002060].includes(data.status_code)) { // success / account suspended / account suspended / private list / invalid user / hiding list
					error = awemeError(data);
				}
			}
			if (error !== null) {
				deviceId = generateDeviceId();
//				iId = generateDeviceId();
				deviceVersion = generateBase62(8);
				f(error, repeat + 1);
			} else {
				if (aweme_hosts.indexOf(h) > 0) {
					aweme_hosts.splice(aweme_hosts.indexOf(h), 1);
					aweme_hosts.unshift(h);
				}
				callback(null, thread_cursor, data);
			}
		});
	};
	f(null, 0);
}
function awemeError(data) {
	return new Error(data.status_code + (typeof data.status_msg === "string" && data.status_msg.length > 0 ? " (" + data.status_msg + ")" : ""));
}
function userInfo(idOrName) {
	var y = 0, m = true, n, cursor_start, cursor_end, delay = 0, f0 = function() {
		if (running) {
			if (m) {
				m = false;
				if (cursor_print && !cursors_printed.has(current_cursor)) {
					console.log("cursor: " + current_cursor + " (" + (new Date(+current_cursor*1000)).toISOString() + ")");
					if (cursor_times) cursor_end = Date.now();
					if (current_cursor !== cursor) {
						console.log("item count: " + metadata.length + "/" + total_count);
						if (cursor_times) console.log("duration: " + ((cursor_end-cursor_start)/1000).toFixed(3) + "s");
					}
					if (cursor_times) cursor_start = cursor_end;
					cursors_printed.add(current_cursor);
				}
				for (var i = 0; current_cursor_count < max_threads; i++) {
					current_cursor_count += 1;
					setTimeout(requestAweme, delay+i*25, "/aweme/v1/user/" + type + "/list/"
					+ "?user_id=" + idOrName
					+ "&max_time=" + current_cursor
					+ "&count=" + count
					+ "&offset=0"
					+ "&source_type=1", current_cursor, f1);
				}
			} else {
				if (cursor_print) {
					console.log("cursor: " + current_cursor + " (" + (new Date(+current_cursor*1000)).toISOString() + ")");
					console.log("item count: " + metadata.length + "/" + total_count);
					if (cursor_times) console.log("duration: " + ((Date.now()-cursor_start)/1000).toFixed(3) + "s");
				}
				current_cursor = null;
				f2();
				metadata_save_to_file();
			}
		}
	}, f1 = function(error, thread_cursor, data) {
		if (thread_cursor !== current_cursor) {
//			console.log("ignoring: " + thread_cursor);
			return;
		} else if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1,
				delay = ignore_errors_delay,
				m = true;
				console.error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
			} else {
				console.error(error);
			}
		} else if (data.status_code !== 0) { // success
			console.error(awemeError(data));
		} else if (typeof data !== "object" || data === null
		|| typeof data.min_time !== "number"
		|| typeof data.has_more !== "boolean") {
			console.error(new Error("Unexpected response."));
			console.error(data);
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1,
				delay = ignore_errors_delay,
				m = true;
				console.error("ignoring: " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
			}
		} else {
			if (current_cursor === cursor) {
				total_count = data.total;
				if (cursor_print) {
					console.log("total count: " + total_count);
				}
				if (abort_on_cap && total_count > 5000) {
					m = false;
					if (abort_on_cap_log) metadata = [0];
					f0();
					return;
				}
				if (cursor_print && limit > 0) {
					total_count += " [" + limit + (break_limit ? "+" : "") + "]";
				}
			}
//			var time_range = (new Date(+data.min_time*1000)).toISOString() + " > " + (new Date(+current_cursor*1000)).toISOString();
			delay = 0,
			n = data.has_more,
			current_cursor = data.min_time,
			ignore_errors_count = 0;
			if (typeof data[type_return] === "object" && data[type_return] !== null) {
				for (var j = 0, r = ""; j < data[type_return].length; j++) {
					if (typeof data[type_return][j] === "object" && data[type_return][j] !== null) {
//						data[type_return][j]["time_range"] = time_range;
						if (ignore_known && data[type_return][j]["follow_status"] !== 0) continue;
						if (ignore_banned && data[type_return][j]["nickname"] === "Tik Toker" && data[type_return][j]["signature"] === "No bio yet" && data[type_return][j]["avatar_uri"] === "musically-maliva-obj/1594805258216454") continue;
						if (metadata_verbose) {
							metadata.push(data[type_return][j]);
						} else {
							metadata.push(obj_filter(data[type_return][j], example_metadata));
						}
						if (++y === limit) {
							n = false;
							if (!break_limit) break;
						}
					}
				}
			}
			f2();
			m = n;
		}
		current_cursor_count -= 1;
		f0();
	}, f2 = function() {
		for (var j = 0; j < current_cursor_requests.length; j++) {
			current_cursor_requests[j].destroy();
		}
		current_cursor_count = 1,
		current_cursor_requests = [];
	};
	f0();
}
function obj_filter(obj_in, obj_match) {
	var obj_out = {};
	for (const [key, value] of Object.entries(obj_in)) {
		if (obj_match.hasOwnProperty(key)) {
			if (typeof value === "object") {
				obj_out[key] = obj_filter(value, obj_match[key]);
			} else if (obj_match[key] !== false || value.length > 0) {
				obj_out[key] = value;
			}
		}
	}
	return obj_out;
}
function sanitizeFilename(value) {
	return value.replace(/[\x00-\x1f"*/:<>?\\|]/g, "");
}
function createTimeToFilename(value) {
	var t = (new Date(value * 1000)).toISOString();
	return t.substring(0, t.length === 24 ? 19 : 22).replace(/[-:]/g, "") + "Z";
}
function metadata_file_name(a, params) {
	var obj_out = [];
//	obj_out.push(path.basename(__filename, path.extname(__filename)));
	obj_out.push(path.basename(__filename));
	obj_out.push(createTimeToFilename(Date.now()/1000));
	for (const [key, value] of Object.entries(params.split(""))) {
		if (a[value] !== undefined) {
			if (a[value] === "") {
				obj_out.push(value);
			} else {
				obj_out.push([value,sanitizeFilename(a[value])].join("="));
			}
		}
	}
	return obj_out.join("_") + ".json";
}
function metadata_save_to_file() {
	if (!skip_empty || metadata.length > 0) {
		fs.writeFile(metadata_file, JSON.stringify(reverse_order ? metadata.reverse() : metadata), function (error) {
			if (error !== null) throw error;
			console.log("output file: " + metadata_file);
			console.log("item count: " + metadata.length);
		});
	}
}
process.on("SIGINT", function() {
	running = false;
	if (timeout !== null) {
		clearTimeout(timeout);
		timeout = null;
	}
});
(function() {
	var i, a = parseArgv(process.argv), f;
	if (a.t === undefined || a.i === undefined) {
		console.log(help);
		return;
	}
	type = a.t.toLowerCase();
	if ((type !== "following") && (type !== "follower")) {
		console.error(new Error("type \"" + type + "\" is not valid."));
		return;
	}
	type_return = type + "s";
	if ((a.i.length === 0) || (!idRegex.test(a.i))) {
		console.error(new Error("id \"" + a.i + "\" is not valid."));
		return;
	}
	if (a.o !== undefined && !isNaN(i = parseInt(a.o, 10))) {
		cursor = i;
	}
	if (cursor === 0) {
		cursor = Math.floor(Date.now() / 1000);
	}
	current_cursor = cursor;
	if (a.j !== undefined) {
		cookie = a.j;
	}
	if (a.l !== undefined) {
		if (a.l.length > 0 && !isNaN(i = parseInt(a.l, 10))) {
			limit = i;
		} else {
			console.log(help);
			return;
		}
	}
	if ((a.w !== undefined) && (a.w.length > 0) && (!isNaN(i = parseInt(a.w, 10)))) {
		count = Math.min(Math.max(i, 1), 200);
	}
	if ((a.n !== undefined) && (a.n.length > 0) && (!isNaN(i = parseInt(a.n, 10)))) {
		ignore_errors_limit = Math.max(i, 0);
	}
	if ((a.error_delay !== undefined) && (a.error_delay.length > 0) && (!isNaN(i = parseInt(a.error_delay, 10)))) {
		ignore_errors_delay = Math.max(i, 0);
	}
	if ((a.c !== undefined) && (a.c.length > 0) && (!isNaN(i = parseInt(a.c, 10)))) {
		max_threads = Math.min(Math.max(i, 1), 100);
	}
	if (a.aweme_host !== undefined && a.aweme_host.length > 0) {
		var api_pos = aweme_hosts.indexOf(a.aweme_host);
		if (api_pos > 0) aweme_hosts.splice(api_pos, 1);
		if (api_pos !== 0) aweme_hosts.unshift(a.aweme_host);
	}
	cursor_print = cursor_print || a.x !== undefined,
	cursor_times = cursor_times || a.x !== undefined && a.x.length > 0,
	user_agents_random = user_agents_random || a.ur !== undefined,
	break_limit = break_limit || a.y !== undefined,
	ignore_errors = ignore_errors || a.n !== undefined,
	abort_on_cap = abort_on_cap || a.a !== undefined && type_return === "followers",
	abort_on_cap_log = abort_on_cap_log || a.p !== undefined,
	debug = debug || a.debug !== undefined,
	ignore_known = ignore_known || a.ignore_known !== undefined,
	ignore_banned = ignore_banned || a.ignore_banned !== undefined,
	skip_empty = skip_empty || a.skip_empty !== undefined,
	reverse_order = reverse_order || a.r !== undefined,
	metadata = [],
	metadata_verbose = metadata_verbose || a.v !== undefined;
	if (a.m !== undefined && a.m.length > 0) {
		metadata_file = a.m;
	} else {
		metadata_file = metadata_file_name(a, "tiloy");
	}
	example_metadata =
	{
//		"time_range": "",
		"uid": "",
		"sec_uid": "",
		"unique_id": "",
		"nickname": "",
		"signature": "",
		"ins_id": false,
		"twitter_name": false,
		"youtube_channel_id": false,
//		"follow_status": 0,
//		"follower_status": 0,
		"aweme_count": 0,
		"favoriting_count": 0,
		"follower_count": 0,
		"following_count": 0
	};
	userInfo(a.i);
})();