# Copyright (c) 2022, JINGROW
# For license information, please see license.txt

from __future__ import annotations

from datetime import datetime
from datetime import timezone as tz
from typing import TYPE_CHECKING

import jingrow
import requests
from jingrow.utils import convert_utc_to_timezone, flt
from jingrow.utils.password import get_decrypted_password

from jcloud.api.bench import all as all_benches
from jcloud.api.site import protected
from jcloud.jcloud.pagetype.site_plan.plan import Plan
from jcloud.jcloud.pagetype.team.team import get_child_team_members
from jcloud.utils import get_current_team

if TYPE_CHECKING:
	from jcloud.jcloud.pagetype.cluster.cluster import Cluster


def poly_get_pg(pagetypes, name):
	for pagetype in pagetypes:
		if jingrow.db.exists(pagetype, name):
			return jingrow.get_pg(pagetype, name)
	return jingrow.get_pg(pagetypes[-1], name)


MOUNTPOINT_REGEX = "(/|/opt/volumes/mariadb|/opt/volumes/benches)"


@jingrow.whitelist()
def all(server_filter=None):  # noqa: C901
	if server_filter is None:
		server_filter = {"server_type": "", "tag": ""}

	team = get_current_team()
	child_teams = [team.name for team in get_child_team_members(team)]
	teams = [team, *child_teams]

	db_server = jingrow.qb.PageType("Database Server")
	app_server = jingrow.qb.PageType("Server")
	res_tag = jingrow.qb.PageType("Resource Tag")

	if server_filter["server_type"] != "Database Servers":
		app_server_query = (
			jingrow.qb.from_(app_server)
			.select(
				app_server.name,
				app_server.title,
				app_server.status,
				app_server.creation,
				app_server.cluster,
			)
			.where(((app_server.team).isin(teams)) & (app_server.status != "Archived"))
		)

		if server_filter["tag"]:
			app_server_query = app_server_query.inner_join(res_tag).on(
				(res_tag.parent == app_server.name) & (res_tag.tag_name == server_filter["tag"])
			)

	if server_filter["server_type"] != "App Servers":
		database_server_query = (
			jingrow.qb.from_(db_server)
			.select(
				db_server.name,
				db_server.title,
				db_server.status,
				db_server.creation,
				db_server.cluster,
			)
			.where(((db_server.team).isin(teams)) & (db_server.status != "Archived"))
		)

		if server_filter["tag"]:
			database_server_query = database_server_query.inner_join(res_tag).on(
				(res_tag.parent == db_server.name) & (res_tag.tag_name == server_filter["tag"])
			)

	if server_filter["server_type"] == "App Servers":
		query = app_server_query
	elif server_filter["server_type"] == "Database Servers":
		query = database_server_query
	else:
		query = app_server_query + database_server_query

	# union isn't supported in qb for run method
	# http://git.jingrow.com/jingrow/jingrow/issues/15609
	servers = jingrow.db.sql(query.get_sql(), as_dict=True)
	for server in servers:
		server_plan_name = jingrow.get_value("Server", server.name, "plan")
		server["plan"] = jingrow.get_pg("Server Plan", server_plan_name) if server_plan_name else None
		server["app_server"] = f"f{server.name[1:]}"
		server["tags"] = jingrow.get_all("Resource Tag", {"parent": server.name}, pluck="tag_name")
		server["region_info"] = jingrow.db.get_value(
			"Cluster", server.cluster, ["title", "image"], as_dict=True
		)
	return servers


@jingrow.whitelist()
def server_tags():
	team = get_current_team()
	return jingrow.get_all("Jcloud Tag", {"team": team, "pagetype_name": "Server"}, pluck="tag")


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def get(name):
	server = poly_get_pg(["Server", "Database Server"], name)
	return {
		"name": server.name,
		"title": server.title,
		"status": server.status,
		"team": server.team,
		"app_server": server.name
		if server.is_self_hosted
		else f"f{server.name[1:]}",  # Don't use `f` series if self hosted
		"region_info": jingrow.db.get_value(
			"Cluster", server.cluster, ["name", "title", "image"], as_dict=True
		),
		"server_tags": [{"name": x.tag, "tag": x.tag_name} for x in server.tags],
		"tags": jingrow.get_all("Jcloud Tag", {"team": server.team, "pagetype_name": "Server"}, ["name", "tag"]),
		"type": "database-server" if server.meta.name == "Database Server" else "server",
	}


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def overview(name):
	server = poly_get_pg(["Server", "Database Server"], name)
	plan = jingrow.get_pg("Server Plan", server.plan) if server.plan else None
	if plan:
		# override plan disk size with the actual disk size
		# TODO: Remove this once we remove old dashboard
		plan.disk = jingrow.db.get_value("Virtual Machine", name, "disk_size")

	return {
		"plan": plan if plan else None,
		"info": {
			"owner": jingrow.db.get_value(
				"User",
				jingrow.get_value("Team", server.team, "user"),
				["first_name", "last_name", "user_image"],
				as_dict=True,
			),
			"created_on": server.creation,
		},
	}


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def archive(name):
	server = poly_get_pg(["Server", "Database Server"], name)
	server.drop_server()


@jingrow.whitelist()
def new(server):
	team = get_current_team(get_pg=True)
	if not team.enabled:
		jingrow.throw("You cannot create a new server because your account is disabled")

	cluster: Cluster = jingrow.get_pg("Cluster", server["cluster"])

	db_plan = jingrow.get_pg("Server Plan", server["db_plan"])
	db_server, job = cluster.create_server("Database Server", server["title"], db_plan, team=team.name)

	proxy_server = jingrow.get_all(
		"Proxy Server",
		{"status": "Active", "cluster": cluster.name, "is_primary": True},
		limit=1,
	)[0]

	# to be used by app server
	cluster.database_server = db_server.name
	cluster.proxy_server = proxy_server.name

	app_plan = jingrow.get_pg("Server Plan", server["app_plan"])
	app_server, job = cluster.create_server("Server", server["title"], app_plan, team=team.name)

	return {"server": app_server.name, "job": job.name}


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def usage(name):
	query_map = {
		"vcpu": (
			f"""((count(count(node_cpu_seconds_total{{instance="{name}",job="node"}}) by (cpu))) - avg(sum by (mode)(rate(node_cpu_seconds_total{{mode='idle',instance="{name}",job="node"}}[120s])))) / count(count(node_cpu_seconds_total{{instance="{name}",job="node"}}) by (cpu))""",
			lambda x: x,
		),
		"disk": (
			f"""sum(node_filesystem_size_bytes{{instance="{name}", job="node", mountpoint=~"{MOUNTPOINT_REGEX}"}} - node_filesystem_avail_bytes{{instance="{name}", job="node", mountpoint=~"{MOUNTPOINT_REGEX}"}}) by ()/ (1024 * 1024 * 1024)""",
			lambda x: x,
		),
		"memory": (
			f"""(node_memory_MemTotal_bytes{{instance="{name}",job="node"}} - node_memory_MemFree_bytes{{instance="{name}",job="node"}} - (node_memory_Cached_bytes{{instance="{name}",job="node"}} + node_memory_Buffers_bytes{{instance="{name}",job="node"}})) / (1024 * 1024)""",
			lambda x: x,
		),
	}

	result = {}
	for usage_type, query in query_map.items():
		response = prometheus_query(query[0], query[1], "Asia/Kolkata", 120, 120)["datasets"]
		if response:
			result[usage_type] = response[0]["values"][-1]
	return result


@protected(["Server", "Database Server"])
def total_resource(name):
	query_map = {
		"vcpu": (
			f"""(count(count(node_cpu_seconds_total{{instance="{name}",job="node"}}) by (cpu)))""",
			lambda x: x,
		),
		"disk": (
			f"""sum(node_filesystem_size_bytes{{instance="{name}", job="node", mountpoint=~"{MOUNTPOINT_REGEX}"}}) by () / (1024 * 1024 * 1024)""",
			lambda x: x,
		),
		"memory": (
			f"""(node_memory_MemTotal_bytes{{instance="{name}",job="node"}}) / (1024 * 1024)""",
			lambda x: x,
		),
	}

	result = {}
	for usage_type, query in query_map.items():
		response = prometheus_query(query[0], query[1], "Asia/Kolkata", 120, 120)["datasets"]
		if response:
			result[usage_type] = response[0]["values"][-1]
	return result


def calculate_swap(name):
	query_map = {
		"swap_used": (
			f"""((node_memory_SwapTotal_bytes{{instance="{name}",job="node"}} - node_memory_SwapFree_bytes{{instance="{name}",job="node"}}) / node_memory_SwapTotal_bytes{{instance="{name}",job="node"}}) * 100""",
			lambda x: x,
		),
		"swap": (
			f"""node_memory_SwapTotal_bytes{{instance="{name}",job="node"}} / (1024 * 1024 * 1024)""",
			lambda x: x,
		),
		"required": (
			f"""(
					(node_memory_MemTotal_bytes{{instance="{name}",job="node"}} +
						node_memory_SwapTotal_bytes{{instance="{name}",job="node"}}
					) -
					(node_memory_MemFree_bytes{{instance="{name}",job="node"}} +
						node_memory_SwapFree_bytes{{instance="{name}",job="node"}} +
						node_memory_Cached_bytes{{instance="{name}",job="node"}} +
						node_memory_Buffers_bytes{{instance="{name}",job="node"}} +
						node_memory_SwapCached_bytes{{instance="{name}",job="node"}}
					)
				) /
				(1024 * 1024 * 1024)""",
			lambda x: x,
		),
	}

	result = {}
	for usage_type, query in query_map.items():
		response = prometheus_query(query[0], query[1], "Asia/Kolkata", 120, 120)["datasets"]
		if response:
			result[usage_type] = response[0]["values"][-1]
	return result


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def analytics(name, query, timezone, duration):
	timespan, timegrain = get_timespan_timegrain(duration)

	query_map = {
		"cpu": (
			f"""sum by (mode)(rate(node_cpu_seconds_total{{instance="{name}", job="node"}}[{timegrain}s])) * 100""",
			lambda x: x["mode"],
		),
		"network": (
			f"""rate(node_network_receive_bytes_total{{instance="{name}", job="node", device=~"ens.*"}}[{timegrain}s]) * 8""",
			lambda x: x["device"],
		),
		"iops": (
			f"""rate(node_disk_reads_completed_total{{instance="{name}", job="node"}}[{timegrain}s])""",
			lambda x: x["device"],
		),
		"space": (
			f"""100 - ((node_filesystem_avail_bytes{{instance="{name}", job="node", mountpoint=~"{MOUNTPOINT_REGEX}"}} * 100) / node_filesystem_size_bytes{{instance="{name}", job="node", mountpoint=~"{MOUNTPOINT_REGEX}"}})""",
			lambda x: x["mountpoint"],
		),
		"loadavg": (
			f"""{{__name__=~"node_load1|node_load5|node_load15", instance="{name}", job="node"}}""",
			lambda x: f"Load Average {x['__name__'][9:]}",
		),
		"memory": (
			f"""node_memory_MemTotal_bytes{{instance="{name}",job="node"}} - node_memory_MemFree_bytes{{instance="{name}",job="node"}} - (node_memory_Cached_bytes{{instance="{name}",job="node"}} + node_memory_Buffers_bytes{{instance="{name}",job="node"}})""",
			lambda x: "Used",
		),
		"database_uptime": (
			f"""mysql_up{{instance="{name}",job="mariadb"}}""",
			lambda x: "Uptime",
		),
		"database_commands_count": (
			f"""sum(round(increase(mysql_global_status_commands_total{{instance='{name}', command=~"select|update|insert|delete|begin|commit|rollback"}}[{timegrain}s]))) by (command)""",
			lambda x: x["command"],
		),
		"database_connections": (
			f"""{{__name__=~"mysql_global_status_threads_connected|mysql_global_variables_max_connections", instance="{name}"}}""",
			lambda x: "Max Connections"
			if x["__name__"] == "mysql_global_variables_max_connections"
			else "Connected Clients",
		),
		"innodb_bp_size": (
			f"""mysql_global_variables_innodb_buffer_pool_size{{instance='{name}'}}""",
			lambda x: "Buffer Pool Size",
		),
		"innodb_bp_size_of_total_ram": (
			f"""avg by (instance) ((mysql_global_variables_innodb_buffer_pool_size{{instance=~"{name}"}} * 100)) / on (instance) (avg by (instance) (node_memory_MemTotal_bytes{{instance=~"{name}"}}))""",
			lambda x: "Buffer Pool Size of Total Ram",
		),
		"innodb_bp_miss_percent": (
			f"""
avg by (instance) (
        rate(mysql_global_status_innodb_buffer_pool_reads{{instance=~"{name}"}}[{timegrain}s])
		/
		rate(mysql_global_status_innodb_buffer_pool_read_requests{{instance=~"{name}"}}[{timegrain}s])
)
""",
			lambda x: "Buffer Pool Miss Percentage",
		),
		"innodb_avg_row_lock_time": (
			f"""(rate(mysql_global_status_innodb_row_lock_time{{instance="{name}"}}[{timegrain}s]) / 1000)/rate(mysql_global_status_innodb_row_lock_waits{{instance="{name}"}}[{timegrain}s])""",
			lambda x: "Avg Row Lock Time",
		),
	}

	return prometheus_query(query_map[query][0], query_map[query][1], timezone, timespan, timegrain)


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def get_request_by_site(name, query, timezone, duration):
	from jcloud.api.analytics import ResourceType, get_request_by_

	timespan, timegrain = get_timespan_timegrain(duration)

	return get_request_by_(name, query, timezone, timespan, timegrain, ResourceType.SERVER)


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def get_slow_logs_by_site(name, query, timezone, duration, normalize=False):
	from jcloud.api.analytics import ResourceType, get_slow_logs

	timespan, timegrain = get_timespan_timegrain(duration)

	return get_slow_logs(name, query, timezone, timespan, timegrain, ResourceType.SERVER, normalize)


def prometheus_query(query, function, timezone, timespan, timegrain):
	monitor_server = jingrow.db.get_single_value("Jcloud Settings", "monitor_server")
	if not monitor_server:
		return {"datasets": [], "labels": []}

	url = f"https://{monitor_server}/prometheus/api/v1/query_range"
	password = get_decrypted_password("Monitor Server", monitor_server, "grafana_password")

	end = datetime.utcnow().replace(tzinfo=tz.utc)
	start = jingrow.utils.add_to_date(end, seconds=-timespan)
	query = {
		"query": query,
		"start": start.timestamp(),
		"end": end.timestamp(),
		"step": f"{timegrain}s",
	}

	response = requests.get(url, params=query, auth=("jingrow", password)).json()

	datasets = []
	labels = []

	if not response["data"]["result"]:
		return {"datasets": datasets, "labels": labels}

	for timestamp, _ in response["data"]["result"][0]["values"]:
		labels.append(
			convert_utc_to_timezone(
				datetime.fromtimestamp(timestamp, tz=tz.utc).replace(tzinfo=None), timezone
			)
		)

	for index in range(len(response["data"]["result"])):
		dataset = {
			"name": function(response["data"]["result"][index]["metric"]),
			"values": [],
		}
		for _, value in response["data"]["result"][index]["values"]:
			dataset["values"].append(flt(value, 2))
		datasets.append(dataset)

	return {"datasets": datasets, "labels": labels}


@jingrow.whitelist()
def options():
	if not get_current_team(get_pg=True).servers_enabled:
		jingrow.throw("Servers feature is not yet enabled on your account")
	regions = jingrow.get_all(
		"Cluster",
		{"cloud_provider": ("!=", "Generic"), "public": True},
		["name", "title", "image", "beta"],
	)
	return {
		"regions": regions,
		"app_plans": plans("Server"),
		"db_plans": plans("Database Server"),
	}


@jingrow.whitelist()
def plans(name, cluster=None, platform="x86_64"):
	return Plan.get_plans(
		pagetype="Server Plan",
		fields=[
			"name",
			"title",
			"price_usd",
			"price_cny",
			"vcpu",
			"memory",
			"disk",
			"cluster",
			"instance_type",
			"premium",
		],
		filters={"server_type": name, "platform": platform, "cluster": cluster}
		if cluster
		else {
			"server_type": name,
			"platform": platform,
		},
	)


@jingrow.whitelist()
def play(play):
	play = jingrow.get_pg("Ansible Play", play)
	play = play.as_dict()
	whitelisted_fields = [
		"name",
		"play",
		"creation",
		"status",
		"start",
		"end",
		"duration",
	]
	for key in list(play.keys()):
		if key not in whitelisted_fields:
			play.pop(key, None)

	play.steps = jingrow.get_all(
		"Ansible Task",
		filters={"play": play.name},
		fields=["task", "status", "start", "end", "duration", "output"],
		order_by="creation",
	)
	return play


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def change_plan(name, plan):
	poly_get_pg(["Server", "Database Server"], name).change_plan(plan)


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def jcloud_jobs(name):
	jobs = []
	for job in jingrow.get_all("Jcloud Job", {"server": name}, pluck="name"):
		jobs.append(jingrow.get_pg("Jcloud Job", job).detail())
	return jobs


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def jobs(filters=None, order_by=None, limit_start=None, limit_page_length=None):
	jobs = jingrow.get_all(
		"Agent Job",
		fields=["name", "job_type", "creation", "status", "start", "end", "duration"],
		filters=filters,
		start=limit_start,
		limit=limit_page_length,
		order_by=order_by or "creation desc",
	)

	for job in jobs:
		job["status"] = "Pending" if job["status"] == "Undelivered" else job["status"]

	return jobs


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def plays(filters=None, order_by=None, limit_start=None, limit_page_length=None):
	return jingrow.get_all(
		"Ansible Play",
		fields=["name", "play", "creation", "status", "start", "end", "duration"],
		filters=filters,
		start=limit_start,
		limit=limit_page_length,
		order_by=order_by or "creation desc",
	)


@jingrow.whitelist()
@protected("Server")
def get_title_and_cluster(name):
	return jingrow.db.get_value("Server", name, ["title", "cluster"], as_dict=True)


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def groups(name):
	server = poly_get_pg(["Server", "Database Server"], name)
	if server.pagetype == "Database Server":
		app_server = jingrow.db.get_value("Server", {"database_server": server.name}, "name")
		server = jingrow.get_pg("Server", app_server)

	return all_benches(server=server.name)


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def reboot(name):
	return poly_get_pg(["Server", "Database Server"], name).reboot()


@jingrow.whitelist()
@protected(["Server", "Database Server"])
def rename(name, title):
	pg = poly_get_pg(["Server", "Database Server"], name)
	pg.title = title
	pg.save()


def get_timespan_timegrain(duration: str) -> tuple[int, int]:
	timespan, timegrain = {
		"1 Hour": (60 * 60, 2 * 60),
		"6 Hour": (6 * 60 * 60, 5 * 60),
		"24 Hour": (24 * 60 * 60, 30 * 60),
		"7 Days": (7 * 24 * 60 * 60, 2 * 30 * 60),
		"15 Days": (15 * 24 * 60 * 60, 3 * 30 * 60),
	}[duration]

	return timespan, timegrain
