#!/usr/bin/env python3
import argparse
import os
import json
import pprint
import random
import re
import datetime
import time
import requests
from requests.auth import HTTPBasicAuth

def parse_cmdline():
	# Parse args
	parser = argparse.ArgumentParser(
		formatter_class=argparse.RawDescriptionHelpFormatter,
		description='''Manage ElasticSearch.''')

	subparsers = parser.add_subparsers(help='commands')

	# An load_index_template command
	load_template_parser = subparsers.add_parser('load_index_template', help='loads index templates into ElasticSearch')
	load_template_parser.add_argument('DIR', action='store', help='a directory to seek for template')
	load_template_parser.add_argument('URL', action='store', help='a ElasticSearch URL to write to')
	load_template_parser.set_defaults(COMMAND='load_index_template')

	# An cleanup command
	cleanup_parser = subparsers.add_parser('cleanup', help='Closes old and deletes empty indexes')
	cleanup_parser.add_argument('URL', action='store', help='a ElasticSearch URL')
	cleanup_parser.add_argument('--min-date',
		action='store',
		help='Min date (format yyyy-mm-dd)',
		default='1970-01-01')
	cleanup_parser.add_argument('--max-date',
		action='store',
		help='Max date (format yyyy-mm-dd)',
		default='1970-01-01')
	cleanup_parser.add_argument('--exclude',
		action='store',
		help='Indicies with matching prefix will be excluded',
		default=None)
	cleanup_parser.set_defaults(COMMAND='cleanup')

	# An reopen command
	reopen_parser = subparsers.add_parser('reopen', help='reopen closed indexes in time range')
	reopen_parser.add_argument('URL', action='store', help='a ElasticSearch URL')
	reopen_parser.add_argument('--min-date',
		action='store',
		help='Min date (format yyyy-mm-dd)',
		default='1970-01-01')
	reopen_parser.add_argument('--max-date',
		action='store',
		help='Max date (format yyyy-mm-dd)',
		default='1970-01-01')
	reopen_parser.add_argument('--exclude',
		action='store',
		help='Indicies with matching prefix will be excluded',
		default=None)
	reopen_parser.set_defaults(COMMAND='reopen')

	# Close command
	close_parser = subparsers.add_parser('close', help='close open indexes in time range')
	close_parser.add_argument('URL', action='store', help='a ElasticSearch URL')
	close_parser.add_argument('--min-date',
		action='store',
		help='Min date (format yyyy-mm-dd)',
		default='1970-01-01')
	close_parser.add_argument('--max-date',
		action='store',
		help='Max date (format yyyy-mm-dd)',
		default='1970-01-01')
	close_parser.add_argument('--exclude',
		action='store',
		help='Indicies with matching prefix will be excluded',
		default=None)
	close_parser.set_defaults(COMMAND='close')

	# Delete command
	delete_parser = subparsers.add_parser('delete', help='delete indexes in time range')
	delete_parser.add_argument('URL', action='store', help='a ElasticSearch URL')
	delete_parser.add_argument('--min-date',
		action='store',
		help='Min date (format yyyy-mm-dd)',
		default='1970-01-01')
	delete_parser.add_argument('--max-date',
		action='store',
		help='Max date (format yyyy-mm-dd)',
		default='1970-01-01')
	delete_parser.add_argument('--exclude',
		action='store',
		help='Indicies with matching prefix will be excluded',
		default=None)
	delete_parser.set_defaults(COMMAND='delete')

	# Force merge command
	forcemerge_parser = subparsers.add_parser('force-merge', help='forces merge and shrinks all indices')
	forcemerge_parser.add_argument('URL', action='store', help='an ElasticSearch URL')
	forcemerge_parser.add_argument('--index',
		action='store',
		help='Index to force merge')
	forcemerge_parser.set_defaults(COMMAND='force-merge')

	# Shrink command
	shrink_parser = subparsers.add_parser('shrink', help='shrink a specified index')
	shrink_parser.add_argument(
		'URL',
		action='store',
		help='an ElasticSearch URL'
	)
	shrink_parser.add_argument(
		'--username',
		action='store',
		help='an ElasticSearch username'
		)
	shrink_parser.add_argument(
		'--password',
		action='store',
		help='an ElasticSearch password'
		)
	shrink_parser.add_argument(
		'--index',
		action='store',
		help='Index to shrink'
	)
	shrink_parser.add_argument(
		'--target-node',
		action='store',
		help='Node to allocate the shrunk index shard to (by default, a warm node is picked at random)',
		default=None
	)
	shrink_parser.add_argument(
		'--suffix-separator',
		action='store',
		help='Prefix of the resulting shrunk index',
		default='_'
	)
	shrink_parser.set_defaults(COMMAND='shrink')

	# Auto shrink command
	auto_shrink_parser = subparsers.add_parser('auto-shrink', help='shrink a specified index')
	auto_shrink_parser.add_argument(
		'URL',
		action='store',
		help='an ElasticSearch URL'
	)
	auto_shrink_parser.add_argument(
		'--max-daytime',
		action='store',
		help='Maximum time of day (HH:MM). No new shrink process is started after this time.',
		default='5:00'
	)
	auto_shrink_parser.add_argument(
		'--max-load',
		action='store',
		help='Maximum server load. No new shrink process is started while server load is above this threshold.',
		type=float,
		default=30
	)
	auto_shrink_parser.add_argument(
		'--min-shards',
		action='store',
		help='Minimum number of shards to consider the index eligible for shrink',
		type=int,
		default=3
	)
	auto_shrink_parser.set_defaults(COMMAND='auto-shrink')

	return parser.parse_args()


def req_close_index(es_url, index_name):
	url_close = es_url + '{}/_close'.format(index_name)
	r = requests.post(url_close, json={})
	return r.json()


def req_delete_index(es_url, index_name):
	url_close = es_url + '{}'.format(index_name)
	r = requests.delete(url_close, json={})
	return r.json()


def req_maxmin_timestamp(es_url, index_name):
	maxmin_request = {
		"aggs" : {
			"max_timestamp" : { "max" : { "field" : "@timestamp" } },
			"min_timestamp" : { "min" : { "field" : "@timestamp" } }
		}
	}
	url_maxmin = es_url + '{}/_search?size=0'.format(index_name)
	r = requests.post(url_maxmin, json=maxmin_request)
	return r.json()


def min_datetime_from_maxmin(maxmin_obj):
	v = maxmin_obj['aggregations']['min_timestamp']['value']
	if v is not None:
		min_date = datetime.datetime.utcfromtimestamp(v/1000.0)
	else:
		min_date = None
	return min_date


def max_datetime_from_maxmin(maxmin_obj):
	v = maxmin_obj['aggregations']['max_timestamp']['value']
	if v is not None:
		max_date = datetime.datetime.utcfromtimestamp(v/1000.0)
	else:
		max_date = None
	return max_date


def datetime_ranges_collide(a_min_datetime, a_max_datetime, b_min_datetime, b_max_datetime):
	assert a_min_datetime <= a_max_datetime
	assert b_min_datetime <= b_max_datetime

	if b_min_datetime < a_min_datetime and b_max_datetime > a_min_datetime:
		# A   |---| ||   |--|
		# B |---|   || |------|
		#===============
		return True
	elif b_min_datetime > a_min_datetime and b_min_datetime < a_max_datetime:
		# A |---|   || |-------|
		# B   |---| ||   |---|
		return True
	else:
		return False


def datetime_range_a_is_within_b(a_min_datetime, a_max_datetime, b_min_datetime, b_max_datetime):
	assert a_min_datetime <= a_max_datetime
	assert b_min_datetime <= b_max_datetime

	# A |------|
	# B |--------|
	return a_min_datetime >= b_min_datetime and a_max_datetime < b_max_datetime


def COMMAND_load_index_template(DIR, URL):

	# Compile list of templates
	template_files = []
	for root, subdirs, files in os.walk(DIR):
		if 'es_index_template.json' in files:
			template_files.append(os.path.join(root, 'es_index_template.json'))

	for tf in template_files:
		print("Loading {}".format(tf))
		obj = None
		try:
			b = open(tf,'r').read()
			# Strip comments
			b = re.sub(r"//.*$", "", b, flags=re.M)

			obj = json.loads(b)
		except Exception as e:
			print("Failed to load {}: {}".format(tf, e))
			continue

		deploy_to = obj.pop('!deploy_to')


		url = URL
		if url[-1:] != '/': url += '/'

		url += '_template/'+deploy_to
		print(" to {}".format(url))

		r = requests.put(url, json=obj)
		print(r.text)


def COMMAND_cleanup(URL, args_min_date, args_max_date, exclude_prefix):
	url = URL
	if url[-1:] != '/': url += '/'

	args_min_datetime = datetime.datetime.strptime(args_min_date, "%Y-%m-%d")
	args_max_datetime = datetime.datetime.strptime(args_max_date, "%Y-%m-%d")
	args_max_datetime = args_max_datetime + datetime.timedelta(days=1)
	assert args_min_datetime < args_max_datetime

	print("Communicate with: {}".format(url))

	url_indices = url + '_cat/indices?format=json'
	r = requests.get(url_indices)
	indices = r.json()
	#json.dump(indices, open('dump.json', 'wt'))
	#indices = json.load(open('dump.json', 'rt'))

	empty_indices = []
	old_count = 0
	not_green_count = 0
	closed_count = 0

	indices = sorted(indices, key=lambda index:index['index'].rsplit('_')[-1], reverse=False)

	for index in indices:
		try:
			name = index['index']

			if name.startswith('.'):
				print("Index {} will be excluded. Indexes that start with '.' are always excluded".format(name))
				continue

			if exclude_prefix is not None and name.startswith(exclude_prefix):
				print("Index {0} will be excluded because it matches prefix {1}".format(name, exclude_prefix))
				continue

			if index['health'] != 'green':
				#print("Index {index} health is {health} - skipping".format(**index))
				not_green_count += 1
				continue

			if index['status'] != 'open':
				continue

			if int(index['docs.count']) == 0:
				# Delete empty indicies
				print("Deleting '{}' (doc.count: {})".format(name, index['docs.count']))
				url_delete_index = url + '{}'.format(name)
				r = requests.delete(url_delete_index)
				result = r.json()
				if result.get('acknowledged') != True:
					print("Failed to delete index '{}': {}".format(name, result))
				continue

			maxmin_obj = req_maxmin_timestamp(url, name)
			max_datetime = max_datetime_from_maxmin(maxmin_obj)
			min_datetime = min_datetime_from_maxmin(maxmin_obj)


			if  min_datetime is not None \
				and max_datetime is not None \
				and datetime_range_a_is_within_b(
					min_datetime, max_datetime,
					args_min_datetime, args_max_datetime
				):
				old_count += 1
				# Delete close old indicies

				print("Closing index: {}\n\tMax: {}\n\tMin: {}\n\tCount: {}\n".format(
					name,
					max_datetime, min_datetime,
					index['docs.count']
				))

				url_close = url + '{}/_close'.format(name)
				r = requests.post(url_close, json={})
				result = r.json()

				if result.get('acknowledged') != True:
					print("Failed to close index '{}': {}".format(name, result))

				continue


		except KeyboardInterrupt:
			return

		except:
			print("Error in this index:")
			pprint.pprint(index)

	print("Count of total indicies: {}".format(len(indices)))
	print("Count of old indicies: {}".format(old_count))
	print("Count of empty indicies: {}".format(len(empty_indices)))
	print("Count of non-green indicies: {}".format(not_green_count))


def COMMAND_reopen(URL, args_min_date, args_max_date, exclude_prefix, max_server_load):
	url = URL
	if url[-1:] != '/': url += '/'

	args_min_datetime = datetime.datetime.strptime(args_min_date, "%Y-%m-%d")
	args_max_datetime = datetime.datetime.strptime(args_max_date, "%Y-%m-%d")
	args_max_datetime = args_max_datetime + datetime.timedelta(days=1)
	assert args_min_datetime < args_max_datetime

	print("Communicate with: {}".format(url))

	url_indices = url + '_cat/indices?format=json'
	r = requests.get(url_indices)
	indices = r.json()
	# json.dump(indices, open('dump.json', 'wt'))
	# indices = json.load(open('dump.json', 'rt'))

	closed_count = 0
	opened_count = 0

	indices = sorted(indices, key=lambda index:index['index'].rsplit('_')[-1], reverse=False)

	for index in indices:
		try:
			name = index['index']

			if name.startswith('.'):
				print("Index {} will be excluded. Indexes that start with '.' are always excluded".format(name))
				continue

			if exclude_prefix is not None and name.startswith(exclude_prefix):
				print("Index {0} will be excluded because it matches prefix {1}".format(name, exclude_prefix))
				continue

			if index['status'] != 'close':
				continue

			closed_count+=1

			# Open
			print("Opening index {}".format(name))
			url_open = url + '{}/_open'.format(name)
			r = requests.post(url_open, json={})
			result = r.json()


			# Wait until open
			url_stats = url + '_cat/indices/{}?format=json'.format(name)
			is_open = False
			while not is_open:
				r = requests.get(url_stats)
				result = r.json()
				assert len(result) == 1
				is_open = result[0]['status'] == 'open'
				print("Index '{}' {} open.".format(name, "is" if is_open else "is not"))
				if not is_open:
					time.sleep(1)


			# Get max and min date
			res_success = False
			retry_count = 0
			while not res_success:
				maxmin_obj = req_maxmin_timestamp(url, name)
				res_success = "aggregations" in maxmin_obj
				if not res_success:
					retry_count+=1
					if retry_count > 5:
						raise RuntimeError("Max retries exceeded while requesting maxmin timestamp on index '{}'".format(name))
					time.sleep(1)

			max_datetime = max_datetime_from_maxmin(maxmin_obj)
			min_datetime = min_datetime_from_maxmin(maxmin_obj)


			# Close if found time range doesn't collide with selected time range
			if  min_datetime is None \
				or max_datetime is None \
				or not datetime_range_a_is_within_b(
					min_datetime, max_datetime,
					args_min_datetime, args_max_datetime
				):
				print("Closing index {}".format(name))
				result = req_close_index(url, name)
			else:
				opened_count+=1

		except KeyboardInterrupt:
			return

		except Exception as e:
			print("Error in this index:")
			pprint.pprint(index)

	print("Count of total indicies: {}".format(len(indices)))
	print("Count of closed indicies: {}".format(closed_count))
	print("Count of indices that were reopened: {}".format(opened_count))


def COMMAND_close(URL, args_min_date, args_max_date, exclude_prefix):
	url = URL
	if url[-1:] != '/': url += '/'

	args_min_datetime = datetime.datetime.strptime(args_min_date, "%Y-%m-%d")
	args_max_datetime = datetime.datetime.strptime(args_max_date, "%Y-%m-%d")
	args_max_datetime = args_max_datetime + datetime.timedelta(days=1)
	assert args_min_datetime < args_max_datetime

	print("Communicate with: {}".format(url))

	url_indices = url + '_cat/indices?format=json'
	r = requests.get(url_indices)
	indices = r.json()
	# json.dump(indices, open('dump.json', 'wt'))
	# indices = json.load(open('dump.json', 'rt'))

	open_count = 0
	not_green_count = 0
	closed_count = 0

	indices = sorted(indices, key=lambda index:index['index'].rsplit('_')[-1], reverse=False)

	for index in indices:
		try:
			name = index['index']

			if name.startswith('.'):
				print("Index {} will be excluded. Indexes that start with '.' are always excluded".format(name))
				continue

			if exclude_prefix is not None and name.startswith(exclude_prefix):
				print("Index {0} will be excluded because it matches prefix {1}".format(name, exclude_prefix))
				continue

			if index['health'] != 'green' and index['health'] != 'yellow':
				#print("Index {index} health is {health} - skipping".format(**index))
				not_green_count += 1
				continue

			if index['status'] != 'open':
				continue


			open_count+=1

			# Get max and min date
			res_success = False
			retry_count = 0
			while not res_success:
				maxmin_obj = req_maxmin_timestamp(url, name)
				res_success = "aggregations" in maxmin_obj
				if not res_success:
					retry_count+=1
					if retry_count > 5:
						raise RuntimeError("Max retries exceeded while requesting maxmin timestamp on index '{}'".format(name))
					time.sleep(1)

			max_datetime = max_datetime_from_maxmin(maxmin_obj)
			min_datetime = min_datetime_from_maxmin(maxmin_obj)


			# Close if found time range doesn't collide with selected time range
			if  min_datetime is not None \
				and max_datetime is not None \
				and datetime_range_a_is_within_b(
					min_datetime, max_datetime,
					args_min_datetime, args_max_datetime
				):
				print("Closing index {}".format(name))
				result = req_close_index(url, name)
				closed_count+=1
			else:
				print("Index {} will remain open.".format(name))

		except KeyboardInterrupt:
			return

		except Exception as e:
			print("Error in this index:")
			pprint.pprint(index)

	print("Count of total indicies: {}".format(len(indices)))
	print("Count of open indicies: {}".format(open_count))
	print("Count of indices that were closed: {}".format(closed_count))


def COMMAND_delete(URL, args_min_date, args_max_date, exclude_prefix):
	url = URL
	if url[-1:] != '/': url += '/'

	args_min_datetime = datetime.datetime.strptime(args_min_date, "%Y-%m-%d")
	args_max_datetime = datetime.datetime.strptime(args_max_date, "%Y-%m-%d")
	args_max_datetime = args_max_datetime + datetime.timedelta(days=1)
	assert args_min_datetime < args_max_datetime

	print("Communicate with: {}".format(url))

	url_indices = url + '_cat/indices?format=json'
	r = requests.get(url_indices)
	indices = r.json()
	# json.dump(indices, open('dump.json', 'wt'))
	# indices = json.load(open('dump.json', 'rt'))

	delete_count = 0

	indices = sorted(indices, key=lambda index:index['index'].rsplit('_')[-1], reverse=False)

	for index in indices:
		try:
			name = index['index']
			was_closed = False

			if name.startswith('.'):
				print("Index {} will be excluded. Indexes that start with '.' are always excluded".format(name))
				continue

			if exclude_prefix is not None and name.startswith(exclude_prefix):
				print("Index {0} will be excluded because it matches prefix {1}".format(name, exclude_prefix))
				continue

			if index['status'] == 'close':
				was_closed = True

				# Open
				print("Opening index {} to analyze max and min dates".format(name))
				url_open = url + '{}/_open'.format(name)
				r = requests.post(url_open, json={})
				result = r.json()


				# Wait until open
				url_stats = url + '_cat/indices/{}?format=json'.format(name)
				is_open = False
				while not is_open:
					r = requests.get(url_stats)
					result = r.json()
					assert len(result) == 1
					is_open = result[0]['status'] == 'open'
					print("Index '{}' {} open.".format(name, "is" if is_open else "is not"))
					if not is_open:
						time.sleep(1)

			# Get max and min date
			res_success = False
			retry_count = 0
			while not res_success:
				maxmin_obj = req_maxmin_timestamp(url, name)
				res_success = "aggregations" in maxmin_obj
				if not res_success:
					retry_count+=1
					if retry_count > 5:
						raise RuntimeError("Max retries exceeded while requesting maxmin timestamp on index '{}'".format(name))
					time.sleep(1)

			max_datetime = max_datetime_from_maxmin(maxmin_obj)
			min_datetime = min_datetime_from_maxmin(maxmin_obj)

			# Delete if found time range doesn't collide with selected time range
			if  min_datetime is not None \
				and max_datetime is not None \
				and datetime_range_a_is_within_b(
					min_datetime, max_datetime,
					args_min_datetime, args_max_datetime
				):
				print("Deleting index {}".format(name))
				result = req_delete_index(url, name)
				delete_count+=1
			else:
				print("Index {} won't be deleted.".format(name))
				if was_closed:
					print("Closing index {}".format(name))
					result = req_close_index(url, name)

		except KeyboardInterrupt:
			return

		except Exception as e:
			print("Error in this index:")
			pprint.pprint(index)

	print("Original count of indicies: {}".format(len(indices)))
	print("Count of deleted indicies: {}".format(delete_count))
	print("Count of reamining indicies: {}".format(len(indices)-delete_count))


def COMMAND_shrink(URL, index, username, password, target_node=None, suffix_separator="_"):
	HEALTH_CHECK_PERIOD = 5
	SHRINK_SUFFIX = "s"
	url = URL
	if url[-1:] == '/':
		url = url[:-1]

	# check index health, must be green
	result = requests.get(
							"{}/_cluster/health/{}".format(url, index),
							auth=HTTPBasicAuth(username, password)
							).json()
	if result["status"] != "green":
		print("Health status of index '{}' is {}. Aborting.".format(index, result["status"]))

	# set shrunk index name
	parts = index.split(suffix_separator)
	if re.fullmatch("[0-9]+", parts[-1]):
		parts.insert(-1, SHRINK_SUFFIX)
	else:
		parts.append(SHRINK_SUFFIX)
	new_index = suffix_separator.join(parts)
	print(f"Shrunk index name: {new_index}")

	# pick a node for allocation
	if not target_node:
		# check if allocation is needed
		r = requests.get(
						"{}/_cat/shards/{}?h=n".format(url, index),
						auth=HTTPBasicAuth(username, password)
						).text.split("\n")[:-1]
		if r.count(r[0]) == len(r):
			target_node = r[0]
		# if allocation is needed, pick a random warm node
		else:
			r = requests.get(
							"{}/_nodes?filter_path=nodes.*.name,nodes.*.roles,nodes.*.attributes.data".format(url),
							auth=HTTPBasicAuth(username, password)
							).json()
			available_nodes = {
								"has_specialized_nodes": [],
								"no_specialized_nodes": [],
								}

			for i in r["nodes"]:
				if "data" in r["nodes"][i]["roles"] and "cold" in r["nodes"][i].get("attributes", {}).get("data", {}):
					available_nodes["has_specialized_nodes"].append(r["nodes"][i]["name"])

				elif "data" in r["nodes"][i]["roles"]:
					available_nodes["no_specialized_nodes"].append(r["nodes"][i]["name"])
			if available_nodes["has_specialized_nodes"]:
				target_node = random.choice(available_nodes["has_specialized_nodes"])
				prepare_for_shrink = {
					"settings": {
						"index.number_of_replicas": 0,
						"index.routing.allocation.require._name": target_node,
						"index.routing.allocation.require.data": "cold",
						"index.blocks.write": True
					}
				}
			elif available_nodes["no_specialized_nodes"]:
				target_node = random.choice(available_nodes["no_specialized_nodes"])
				prepare_for_shrink = {
					"settings": {
						"index.number_of_replicas": 0,
						"index.routing.allocation.require._name": target_node,
						"index.blocks.write": True
					}
				}
			else:
				print("No data nodes. Stopping.")
				return False

	print("Using '{}' for allocation.".format(target_node))

	# prepare for shrink
	r = requests.put(
					"{}/{}/_settings".format(url, index),
					json=prepare_for_shrink,
					auth=HTTPBasicAuth(username, password)
					)
	result = r.json()
	if not result.get("acknowledged"):
		print("Pre-shrink preparation failed for index '{}': \n{}".format(index, result))
		return False

	# wait until all shards are allocated to target_node
	while "RELOCATING" in requests.get(
										"{}/_cat/shards/{}".format(url, index),
										auth=HTTPBasicAuth(username, password)
										).text:
		print("Shard relocation of {} in progress. Waiting...".format(index))
		time.sleep(HEALTH_CHECK_PERIOD)

	# shrink
	shrink_settings = {
		"settings": {
		"index.number_of_replicas": 0,
		"index.routing.allocation.require._name": None,
		"index.blocks.write": None
		}
	}
	r = requests.post(
		"{url}/{index}/_shrink/{shrunk_index}".format(url=url, index=index, shrunk_index=new_index),
		json=shrink_settings,
		auth=HTTPBasicAuth(username, password)
	)
	result = r.json()
	if not result.get("acknowledged"):
		print("Shrink of index '{}' returned error: \n{}".format(index, result))
		return False
	print("Index '{}' shrunk successfully.".format(index))

	# wait until all shards are allocated
	result = requests.get(
							"{}/_cluster/health/{}".format(url, new_index),
							auth=HTTPBasicAuth(username, password)
							).json()
	while result["status"] != "green":
		print("Health status of index '{}' is {}. Waiting...".format(index, result["status"]))
		time.sleep(HEALTH_CHECK_PERIOD)
		result = requests.get(
								"{}/_cluster/health/{}".format(url, index),
								auth=HTTPBasicAuth(username, password)
								).json()

	# delete old index
	r = requests.delete(
						"{url}/{index}".format(url=url, index=index),
						auth=HTTPBasicAuth(username, password)
						)
	result = r.json()
	if not result.get("acknowledged"):
		print("Cannot delete index '{}': \n{}".format(index, result))
		return False
	print("Index '{}' deleted.".format(index))

	return True


def COMMAND_auto_shrink(URL, max_daytime, max_load, min_shards):
	"""
	Cycles through warm indices, shrinking each of them, until either:
	- no more shrinkable indices, or
	- current time exceeds <alarm_time>.
	Shrinking is paused if server load exceeds <max_load> and resumes
	once it's back in safe numbers.
	"""
	HEALTH_CHECK_PERIOD = 10

	url = URL
	if url[-1:] == '/':
		url = url[:-1]

	hour, minute = map(int, max_daytime.split(":"))
	max_daytime = datetime.datetime.now().replace(hour=hour, minute=minute)
	if max_daytime < datetime.datetime.now():
		max_daytime += datetime.timedelta(days=1)

	# build list of shrink candidates
	result = requests.get("{}/_settings".format(url)).json()
	candidates = []
	for name, index in result.items():
		index_stage = ""
		try:
			index_stage = index["settings"]["index"]["routing"]["allocation"]["require"]["data"] == "warm"
		except KeyError:
			pass
		if index_stage != "warm":
			continue
		n_shards = index["settings"]["index"]["number_of_shards"]
		if n_shards < min_shards:
			continue
		candidates.append((n_shards, name))

	# sort so that indices with more shards are processed first
	candidates.sort()

	failed_indices = []
	successful_indices = []

	while True:
		if not candidates:
			print("No more indices to shrink. Terminating.")
			break
		if datetime.datetime.now() > max_daytime:
			print("Ring ring! It's {}. Terminating.".format(datetime.datetime.now().strftime("%H:%M")))
			break

		# check if all shards are allocated
		result = requests.get("{}/_cluster/health".format(url)).json()
		if result["status"] != "green":
			print("Cluster health is {}. Waiting...".format(result["status"]))
			time.sleep(HEALTH_CHECK_PERIOD)
			continue

		# check CPU load
		result = requests.get("{}/_cluster/stats?filter_path=nodes.process.cpu".format(url)).json()
		server_load = result["nodes"]["process"]["cpu"]["percent"]
		if server_load > max_load:
			print("Server load is too high ({}%). Waiting...".format(server_load))
			time.sleep(HEALTH_CHECK_PERIOD)
			continue

		# perform shrink
		n_shards, index = candidates.pop()
		success = COMMAND_shrink(url, index)
		if success:
			successful_indices.append(index)
		else:
			failed_indices.append(index)

	print("Successfully shrunk indices: {}".format(len(successful_indices)))
	print("Failed to shrink indices: {}".format(len(failed_indices)))
	print("Unprocessed shrink candidates: {}".format(len(candidates)))


def COMMAND_forcemerge(URL, index):
	url = URL
	if url[-1:] == '/':
		url = url[:-1]

	# From ES docs: Force merge should only be called against an index after you have finished writing to it.

	block_write = {
		"settings": {
			"index.blocks.write": True
		}
	}

	r = requests.put("{}/{}/_settings".format(url, index), json=block_write)
	result = r.json()
	if not result.get("acknowledged"):
		print("Pre-merge preparation failed for index '{}': {}".format(index, result))
		return

	r = requests.post("{}/{}/_forcemerge?&max_num_segments=1".format(url, index))
	result = r.json()
	print("Index '{}': {}".format(index, result))


def main():
	# Get arguments
	args = parse_cmdline()

	# Call the command
	if 'COMMAND' not in args:
		print("Please select a command: load_index_template, cleanup, reopen, close, shrink, force-merge.")
		print("For more information see --help")
		return 1

	if args.COMMAND == 'load_index_template':
		return COMMAND_load_index_template(args.DIR, args.URL)

	elif args.COMMAND == 'cleanup':
		return COMMAND_cleanup(args.URL, args.min_date, args.max_date, args.exclude)

	elif args.COMMAND == 'reopen':
		return COMMAND_reopen(args.URL, args.min_date, args.max_date, args.exclude)

	elif args.COMMAND == 'close':
		return COMMAND_close(args.URL, args.min_date, args.max_date, args.exclude)

	elif args.COMMAND == 'delete':
		return COMMAND_delete(args.URL, args.min_date, args.max_date, args.exclude)

	elif args.COMMAND == 'force-merge':
		return COMMAND_forcemerge(args.URL, args.index)

	elif args.COMMAND == 'shrink':
		return COMMAND_shrink(args.URL, args.index, args.username, args.password, args.target_node,
							args.suffix_separator)

	elif args.COMMAND == 'auto-shrink':
		return COMMAND_auto_shrink(args.URL, args.max_daytime, args.max_load, args.min_shards)


if __name__ == '__main__':
	main()
