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

from __future__ import annotations

import json
import shlex
import typing
from datetime import timedelta
from functools import cached_property

import boto3
import jingrow
from jingrow import _
from jingrow.core.utils import find, find_all
from jingrow.installer import subprocess
from jingrow.model.page import Page
from jingrow.utils import cint
from jingrow.utils.user import is_system_user

from jcloud.agent import Agent
from jcloud.api.client import dashboard_whitelist
from jcloud.exceptions import VolumeResizeLimitError
from jcloud.overrides import get_permission_query_conditions_for_pagetype
from jcloud.jcloud.pagetype.resource_tag.tag_helpers import TagHelpers
from jcloud.runner import Ansible
from jcloud.telegram_utils import Telegram
from jcloud.utils import fmt_timedelta, log_error

if typing.TYPE_CHECKING:
	from jcloud.jcloud.pagetype.bench.bench import Bench
	from jcloud.jcloud.pagetype.virtual_machine.virtual_machine import VirtualMachine


class BaseServer(Page, TagHelpers):
	dashboard_fields = (
		"title",
		"plan",
		"cluster",
		"status",
		"team",
		"database_server",
		"is_self_hosted",
		"auto_add_storage_min",
		"auto_add_storage_max",
	)

	@staticmethod
	def get_list_query(query):
		Server = jingrow.qb.PageType("Server")

		query = query.where(Server.status != "Archived").where(Server.team == jingrow.local.team().name)
		results = query.run(as_dict=True)

		for result in results:
			db_plan_name = jingrow.db.get_value("Database Server", result.database_server, "plan")
			result.db_plan = (
				jingrow.db.get_value(
					"Server Plan", db_plan_name, ["title", "price_cny", "price_usd"], as_dict=True
				)
				if db_plan_name
				else None
			)

		return results

	def get_pg(self, pg):
		from jcloud.api.client import get
		from jcloud.api.server import usage

		if self.plan:
			pg.current_plan = get("Server Plan", self.plan)
		else:
			if virtual_machine := jingrow.db.get_value(
				"Virtual Machine", self.virtual_machine, ["vcpu", "ram", "disk_size"], as_dict=True
			):
				pg.current_plan = {
					"vcpu": virtual_machine.vcpu,
					"memory": virtual_machine.ram,
					"disk": virtual_machine.disk_size,
				}

		pg.storage_plan = jingrow.db.get_value(
			"Server Storage Plan",
			{"enabled": 1},
			["price_cny", "price_usd"],
			as_dict=True,
		)
		pg.usage = usage(self.name)
		pg.actions = self.get_actions()
		pg.disk_size = jingrow.db.get_value("Virtual Machine", self.virtual_machine, "disk_size")
		pg.replication_server = jingrow.db.get_value(
			"Database Server",
			{"primary": pg.database_server, "is_replication_setup": 1},
			"name",
		)

		return pg

	@dashboard_whitelist()
	def increase_disk_size_for_server(
		self, server: str, increment: int, mountpoint: str | None = None
	) -> None:
		if server == self.name:
			self.increase_disk_size(increment=increment, mountpoint=mountpoint)
			self.create_subscription_for_storage(increment)
		else:
			server_pg = jingrow.get_pg("Database Server", server)
			server_pg.increase_disk_size(increment=increment, mountpoint=mountpoint)
			server_pg.create_subscription_for_storage(increment)

	@dashboard_whitelist()
	def configure_auto_add_storage(self, server: str, min: int, max: int) -> None:
		if min < 0 or max < 0:
			jingrow.throw(_("Minimum and maximum storage sizes must be positive"))
		if min > max:
			jingrow.throw(_("Minimum storage size must be less than the maximum storage size"))

		if server == self.name:
			self.auto_add_storage_min = min
			self.auto_add_storage_max = max
			self.save()
		else:
			server_pg = jingrow.get_pg("Database Server", server)
			server_pg.auto_add_storage_min = min
			server_pg.auto_add_storage_max = max
			server_pg.save()

	@staticmethod
	def on_not_found(name):
		# If name is of a db server then redirect to the app server
		app_server = jingrow.db.get_value("Server", {"database_server": name}, "name")
		if app_server:
			jingrow.response.message = {
				"redirect": f"/dashboard/servers/{app_server}",
			}
		raise

	def get_actions(self):
		server_type = ""
		if self.pagetype == "Server":
			server_type = "application server"
		elif self.pagetype == "Database Server":
			if self.is_replication_setup:
				server_type = "replication server"
			else:
				server_type = "database server"

		actions = [
			{
				"action": "Rename server",
				"description": f"Rename the {server_type}",
				"button_label": "Rename",
				"condition": self.status == "Active",
				"pg_method": "rename",
				"group": f"{server_type.title()} Actions",
			},
			{
				"action": "Reboot server",
				"description": f"Reboot the {server_type}",
				"button_label": "Reboot",
				"condition": self.status == "Active",
				"pg_method": "reboot",
				"group": f"{server_type.title()} Actions",
			},
			{
				"action": "Drop server",
				"description": "Drop both the application and database servers",
				"button_label": "Drop",
				"condition": self.status == "Active" and self.pagetype == "Server",
				"pg_method": "drop_server",
				"group": "Dangerous Actions",
			},
		]

		for action in actions:
			action["server_pagetype"] = self.pagetype
			action["server_name"] = self.name

		return [action for action in actions if action.get("condition", True)]

	@dashboard_whitelist()
	def drop_server(self):
		if self.pagetype == "Database Server":
			app_server_name = jingrow.db.get_value("Server", {"database_server": self.name}, "name")
			app_server = jingrow.get_pg("Server", app_server_name)
			db_server = self
		else:
			app_server = self
			db_server = jingrow.get_pg("Database Server", self.database_server)

		app_server.archive()
		db_server.archive()

	def autoname(self):
		if not self.domain:
			self.domain = jingrow.db.get_single_value("Jcloud Settings", "domain")
		self.name = f"{self.hostname}.{self.domain}"
		if self.pagetype in ["Database Server", "Server", "Proxy Server"] and self.is_self_hosted:
			self.name = f"{self.hostname}.{self.self_hosted_server_domain}"

	def validate(self):
		self.validate_cluster()
		self.validate_agent_password()
		if self.pagetype == "Database Server" and not self.self_hosted_mariadb_server:
			self.self_hosted_mariadb_server = self.private_ip

		if not self.hostname_abbreviation:
			self._set_hostname_abbreviation()

		self.validate_mounts()

	def _set_hostname_abbreviation(self):
		self.hostname_abbreviation = get_hostname_abbreviation(self.hostname)

	def after_insert(self):
		if self.ip and (
			self.pagetype not in ["Database Server", "Server", "Proxy Server"] or not self.is_self_hosted
		):
			self.create_dns_record()
			self.update_virtual_machine_name()

	def create_dns_record(self):
		try:
			domain = jingrow.get_pg("Root Domain", self.domain)
			
			if domain.dns_provider == "DNSPod":
				self.create_dns_record_dnspod(domain)
			else:
				self.create_dns_record_route53()
				
		except Exception as e:
			log_error("DNS记录创建异常", domain=self.domain, server=self.name, error=str(e))
	
	def create_dns_record_dnspod(self, domain):
		"""
		使用腾讯云API创建或更新DNS记录
		"""
		try:
			# 获取凭证
			secret_id = domain.dnspod_app_id or ""
			secret_key = domain.get_password("dnspod_app_token") or ""
			
			if not secret_id or not secret_key:
				log_error("未配置DNSPod凭证", domain=domain.name)
				return
				
			# 分离子域名
			if self.name.endswith("." + domain.name):
				sub_domain = self.name[:-len(domain.name)-1]
				if not sub_domain:
					log_error("主机记录为空，拒绝将主域名作为A记录", domain=domain.name, server=self.name)
					return
			else:
				log_error("域名配置错误", domain=domain.name, server=self.name)
				return
				
			# 使用腾讯云SDK
			try:
				from tencentcloud.common import credential
				from tencentcloud.common.profile.client_profile import ClientProfile
				from tencentcloud.common.profile.http_profile import HttpProfile
				from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
				from tencentcloud.dnspod.v20210323 import dnspod_client, models
			except ImportError:
				log_error("腾讯云SDK未安装，请运行: pip install --user tencentcloud-sdk-python")
				return
				
			# 初始化认证
			cred = credential.Credential(secret_id, secret_key)
			httpProfile = HttpProfile()
			httpProfile.endpoint = "dnspod.tencentcloudapi.com"
			
			clientProfile = ClientProfile()
			clientProfile.httpProfile = httpProfile
			client = dnspod_client.DnspodClient(cred, "", clientProfile)
			
			# 查询记录是否存在
			record_id = None
			try:
				req = models.DescribeRecordListRequest()
				params = {
					"Domain": domain.name,
					"Subdomain": sub_domain,
					"RecordType": "A"
				}
				req.from_json_string(json.dumps(params))
				resp = client.DescribeRecordList(req)
				resp_dict = json.loads(resp.to_json_string())
				
				records = resp_dict.get("RecordList", [])
				for record in records:
					if record.get("Name") == sub_domain and record.get("Type") == "A":
						record_id = record.get("RecordId")
						break
			except TencentCloudSDKException as err:
				# 如果是"记录列表为空"错误，这是正常的，表示记录不存在
				if err.code == "ResourceNotFound.NoDataOfRecord":
					record_id = None
				else:
					# 其他错误需要记录并抛出
					log_error("腾讯云SDK查询记录错误", code=err.code, message=err.message)
					return
			
			# 腾讯云DNSPod的TTL值
			# 免费版最低TTL可能是600秒
			ttl_value = 3600 if self.pagetype == "Proxy Server" else 600
						
			# 根据是否存在记录决定创建或修改
			try:
				if record_id:
					# 修改记录
					req = models.ModifyRecordRequest()
					params = {
						"Domain": domain.name,
						"RecordId": record_id,
						"RecordType": "A",
						"RecordLine": "默认",
						"Value": self.ip,
						"SubDomain": sub_domain,  # 添加子域名参数，修复主机记录变为@的问题
						"TTL": ttl_value
					}
					req.from_json_string(json.dumps(params))
					response = client.ModifyRecord(req)
				else:
					# 创建记录
					req = models.CreateRecordRequest()
					params = {
						"Domain": domain.name,
						"SubDomain": sub_domain,
						"RecordType": "A",
						"RecordLine": "默认",
						"Value": self.ip,
						"TTL": ttl_value
					}
					req.from_json_string(json.dumps(params))
					response = client.CreateRecord(req)
			except TencentCloudSDKException as err:
				# 如果是TTL限制错误，尝试更高的值
				if err.code == "LimitExceeded.RecordTtlLimit":
					log_error("TTL值超限，尝试更高的TTL值", current_ttl=ttl_value)
					
					# 尝试更高的TTL值 (1800秒 = 30分钟)
					ttl_value = 1800
					try:
						if record_id:
							# 修改记录
							req = models.ModifyRecordRequest()
							params = {
								"Domain": domain.name,
								"RecordId": record_id,
								"RecordType": "A",
								"RecordLine": "默认",
								"Value": self.ip,
								"SubDomain": sub_domain,  # 同样在这里也添加子域名参数
								"TTL": ttl_value
							}
							req.from_json_string(json.dumps(params))
							response = client.ModifyRecord(req)
						else:
							# 创建记录
							req = models.CreateRecordRequest()
							params = {
								"Domain": domain.name,
								"SubDomain": sub_domain,
								"RecordType": "A",
								"RecordLine": "默认",
								"Value": self.ip,
								"TTL": ttl_value
							}
							req.from_json_string(json.dumps(params))
							response = client.CreateRecord(req)
					except TencentCloudSDKException as inner_err:
						log_error("即使提高TTL后仍然失败", code=inner_err.code, message=inner_err.message, ttl=ttl_value)
				else:
					log_error("DNS操作失败", code=err.code, message=err.message, action="创建或修改记录")
				
		except Exception as e:
			log_error("DNS记录创建异常", domain=domain.name, server=self.name, error=str(e))

	def create_dns_record_route53(self):
		try:
			domain = jingrow.get_pg("Root Domain", self.domain)
			client = boto3.client(
				"route53",
				aws_access_key_id=domain.aws_access_key_id,
				aws_secret_access_key=domain.get_password("aws_secret_access_key"),
			)
			zones = client.list_hosted_zones_by_name()["HostedZones"]
			# list_hosted_zones_by_name returns a lexicographically ordered list of zones
			# i.e. x.example.com comes after example.com
			# Name field has a trailing dot
			hosted_zone = find(reversed(zones), lambda x: domain.name.endswith(x["Name"][:-1]))["Id"]
			client.change_resource_record_sets(
				ChangeBatch={
					"Changes": [
						{
							"Action": "UPSERT",
							"ResourceRecordSet": {
								"Name": self.name,
								"Type": "A",
								"TTL": 3600 if self.pagetype == "Proxy Server" else 300,
								"ResourceRecords": [{"Value": self.ip}],
							},
						}
					]
				},
				HostedZoneId=hosted_zone,
			)
		except Exception:
			log_error("Route 53 Record Creation Error", domain=domain.name, server=self.name)

	@jingrow.whitelist()
	def enable_server_for_new_benches_and_site(self):
		if not self.public:
			jingrow.throw("Action only allowed for public servers")

		server = self.get_server_enabled_for_new_benches_and_sites()

		if server:
			jingrow.msgprint(_("Server {0} is already enabled for new benches and sites").format(server))

		else:
			self.use_for_new_benches = True
			self.use_for_new_sites = True
			self.save()

	def get_server_enabled_for_new_benches_and_sites(self):
		return jingrow.db.get_value(
			"Server",
			{
				"name": ("!=", self.name),
				"is_primary": True,
				"status": "Active",
				"use_for_new_benches": True,
				"use_for_new_sites": True,
				"public": True,
				"cluster": self.cluster,
			},
			pluck="name",
		)

	@jingrow.whitelist()
	def disable_server_for_new_benches_and_site(self):
		self.use_for_new_benches = False
		self.use_for_new_sites = False
		self.save()

	def validate_cluster(self):
		if not self.cluster:
			self.cluster = jingrow.db.get_value("Root Domain", self.domain, "default_cluster")
		if not self.cluster:
			jingrow.throw("Default Cluster not found", jingrow.ValidationError)

	def validate_agent_password(self):
		if not self.agent_password:
			self.agent_password = jingrow.generate_hash(length=32)

	def get_agent_repository_url(self):
		settings = jingrow.get_single("Jcloud Settings")
		repository_owner = settings.agent_repository_owner or "jingrow"
		return f"http://git.jingrow.com/{repository_owner}/agent"

	def get_agent_repository_branch(self):
		settings = jingrow.get_single("Jcloud Settings")
		return settings.branch or "master"

	@jingrow.whitelist()
	def ping_agent(self):
		agent = Agent(self.name, self.pagetype)
		return agent.ping()

	@jingrow.whitelist()
	def ping_agent_job(self):
		agent = Agent(self.name, self.pagetype)
		return agent.create_agent_job("Ping Job", "ping_job").name

	@jingrow.whitelist()
	def update_agent(self):
		agent = Agent(self.name, self.pagetype)
		return agent.update()

	@jingrow.whitelist()
	def prepare_server(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_prepare_server", queue="long", timeout=2400)

	def _prepare_server(self):
		try:
			if self.provider == "Scaleway":
				ansible = Ansible(
					playbook="scaleway.yml",
					server=self,
					user="ubuntu",
					variables={
						"private_ip": self.private_ip,
						"private_mac_address": self.private_mac_address,
						"private_vlan_id": self.private_vlan_id,
					},
				)
			elif self.provider == "AWS EC2":
				ansible = Ansible(playbook="aws.yml", server=self, user="ubuntu")
			elif self.provider == "OCI":
				ansible = Ansible(playbook="oci.yml", server=self, user="ubuntu")

			ansible.run()
			self.reload()
			self.is_server_prepared = True
			self.save()
		except Exception:
			log_error("Server Preparation Exception", server=self.as_dict())

	@jingrow.whitelist()
	def setup_server(self):
		self.status = "Installing"
		self.save()
		jingrow.enqueue_pg(self.pagetype, self.name, "_setup_server", queue="long", timeout=2400)

	@jingrow.whitelist()
	def install_nginx(self):
		self.status = "Installing"
		self.save()
		jingrow.enqueue_pg(self.pagetype, self.name, "_install_nginx", queue="long", timeout=1200)

	def _install_nginx(self):
		try:
			ansible = Ansible(
				playbook="nginx.yml",
				server=self,
			)
			play = ansible.run()
			self.reload()
			if play.status == "Success":
				self.status = "Active"
			else:
				self.status = "Broken"
		except Exception:
			self.status = "Broken"
			log_error("NGINX Install Exception", server=self.as_dict())
		self.save()

	@jingrow.whitelist()
	def install_filebeat(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_install_filebeat", queue="long", timeout=1200)

	def _install_filebeat(self):
		log_server = jingrow.db.get_single_value("Jcloud Settings", "log_server")
		if log_server:
			kibana_password = jingrow.get_pg("Log Server", log_server).get_password("kibana_password")
		else:
			kibana_password = None

		try:
			ansible = Ansible(
				playbook="filebeat.yml",
				server=self,
				user=self._ssh_user(),
				port=self._ssh_port(),
				variables={
					"server_type": self.pagetype,
					"server": self.name,
					"log_server": log_server,
					"kibana_password": kibana_password,
				},
			)
			ansible.run()
		except Exception:
			log_error("Filebeat Install Exception", server=self.as_dict())

	@jingrow.whitelist()
	def install_exporters(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_install_exporters", queue="long", timeout=1200)

	@jingrow.whitelist()
	def ping_ansible(self):
		try:
			ansible = Ansible(
				playbook="ping.yml",
				server=self,
				user=self._ssh_user(),
				port=self._ssh_port(),
			)
			ansible.run()
		except Exception:
			log_error("Server Ping Exception", server=self.as_dict())

	@jingrow.whitelist()
	def update_agent_ansible(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_update_agent_ansible")

	def _update_agent_ansible(self):
		try:
			ansible = Ansible(
				playbook="update_agent.yml",
				variables={
					"agent_repository_url": self.get_agent_repository_url(),
					"agent_repository_branch": self.get_agent_repository_branch(),
				},
				server=self,
				user=self._ssh_user(),
				port=self._ssh_port(),
			)
			ansible.run()
		except Exception:
			log_error("Agent Update Exception", server=self.as_dict())

	@jingrow.whitelist()
	def fetch_keys(self):
		try:
			ansible = Ansible(playbook="keys.yml", server=self)
			ansible.run()
		except Exception:
			log_error("Server Key Fetch Exception", server=self.as_dict())

	@jingrow.whitelist()
	def ping_ansible_unprepared(self):
		try:
			if self.provider == "Scaleway" or self.provider in ("AWS EC2", "OCI"):
				ansible = Ansible(
					playbook="ping.yml",
					server=self,
					user="ubuntu",
				)
			ansible.run()
		except Exception:
			log_error("Unprepared Server Ping Exception", server=self.as_dict())

	@jingrow.whitelist()
	def cleanup_unused_files(self):
		if self.is_build_server():
			return

		jingrow.enqueue_pg(self.pagetype, self.name, "_cleanup_unused_files", queue="long", timeout=2400)

	def is_build_server(self) -> bool:
		# Not a field in all subclasses
		if getattr(self, "use_for_build", False):
			return True

		name = jingrow.db.get_single_value("Jcloud Settings", "build_server")
		if name == self.name:
			return True

		# Whether build_server explicitly set on Release Group
		count = jingrow.db.count(
			"Release Group",
			{
				"enabled": True,
				"build_server": self.name,
			},
		)
		if isinstance(count, (int, float)):
			return count > 0
		return False

	def _cleanup_unused_files(self):
		agent = Agent(self.name, self.pagetype)
		if agent.should_skip_requests():
			return
		agent.cleanup_unused_files()

	def on_trash(self):
		plays = jingrow.get_all("Ansible Play", filters={"server": self.name})
		for play in plays:
			jingrow.delete_pg("Ansible Play", play.name)

	def break_glass(self):
		"""
		Remove glass file with simple ssh command to make free space

		Space is required for playbooks to run, growpart command, etc.
		"""
		try:
			subprocess.check_output(
				shlex.split(
					f"ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@{self.ip} -t rm /root/glass"
				),
				stderr=subprocess.STDOUT,
			)
		except subprocess.CalledProcessError as e:
			log_error(f"Error removing glassfile: {e.output.decode()}")

	@jingrow.whitelist()
	def extend_ec2_volume(self, device=None):
		if self.provider not in ("AWS EC2", "OCI"):
			return
		# Restart MariaDB if MariaDB disk is full
		mountpoint = self.guess_data_disk_mountpoint()
		restart_mariadb = self.pagetype == "Database Server" and self.is_disk_full(
			mountpoint
		)  # check before breaking glass to ensure state of mariadb
		self.break_glass()
		if not device:
			# Try the best guess. Try extending the data volume
			volume = self.find_mountpoint_volume(mountpoint)
			device = self.get_device_from_volume_id(volume.volume_id)
		try:
			ansible = Ansible(
				playbook="extend_ec2_volume.yml",
				server=self,
				variables={"restart_mariadb": restart_mariadb, "device": device},
			)
			ansible.run()
		except Exception:
			log_error("EC2 Volume Extend Exception", server=self.as_dict())

	def enqueue_extend_ec2_volume(self, device):
		jingrow.enqueue_pg(self.pagetype, self.name, "extend_ec2_volume", device=device)

	@cached_property
	def time_to_wait_before_updating_volume(self) -> timedelta | int:
		if self.provider != "AWS EC2":
			return 0
		if not (
			last_updated_at := jingrow.get_value(
				"Virtual Machine Volume",
				{"parent": self.virtual_machine, "idx": 1},  # first volume is likely main
				"last_updated_at",
			)
		):
			return 0
		diff = jingrow.utils.now_datetime() - last_updated_at
		return diff if diff < timedelta(hours=6) else 0

	@jingrow.whitelist()
	def increase_disk_size(self, increment=50, mountpoint=None) -> bool:
		if self.provider not in ("AWS EC2", "OCI"):
			return
		if self.provider == "AWS EC2" and self.time_to_wait_before_updating_volume:
			jingrow.throw(
				f"Please wait {fmt_timedelta(self.time_to_wait_before_updating_volume)} before resizing volume",
				VolumeResizeLimitError,
			)
		if not mountpoint:
			mountpoint = self.guess_data_disk_mountpoint()

		volume = self.find_mountpoint_volume(mountpoint)

		virtual_machine: "VirtualMachine" = jingrow.get_pg("Virtual Machine", self.virtual_machine)
		virtual_machine.increase_disk_size(volume.volume_id, increment)
		if self.provider == "AWS EC2":
			device = self.get_device_from_volume_id(volume.volume_id)
			self.enqueue_extend_ec2_volume(device)
		elif self.provider == "OCI":
			# TODO: Add support for volumes on OCI
			# Non-boot volumes might not need resize
			self.break_glass()
			self.reboot()

	def guess_data_disk_mountpoint(self) -> str:
		if not self.has_data_volume:
			return "/"

		volumes = self.get_volume_mounts()
		if volumes:
			if self.pagetype == "Server":
				mountpoint = "/opt/volumes/benches"
			elif self.pagetype == "Database Server":
				mountpoint = "/opt/volumes/mariadb"
		else:
			mountpoint = "/"
		return mountpoint

	def find_mountpoint_volume(self, mountpoint):
		machine: "VirtualMachine" = jingrow.get_pg("Virtual Machine", self.virtual_machine)

		if len(machine.volumes) == 1:
			# If there is only one volume,
			# then all mountpoints are on the same volume
			return machine.volumes[0]

		volumes = self.get_volume_mounts()
		volume = find(volumes, lambda x: x.mount_point == mountpoint)
		if volume:
			# If the volume is in `mounts`, that means it's a data volume
			return volume
		# Otherwise it's a root volume
		return find(machine.volumes, lambda v: v.device == "/dev/sda1")

	def update_virtual_machine_name(self):
		if self.provider not in ("AWS EC2", "OCI"):
			return None
		virtual_machine = jingrow.get_pg("Virtual Machine", self.virtual_machine)
		return virtual_machine.update_name_tag(self.name)

	def create_subscription(self, plan):
		self._create_initial_plan_change(plan)

	def _create_initial_plan_change(self, plan):
		jingrow.get_pg(
			{
				"pagetype": "Plan Change",
				"document_type": self.pagetype,
				"document_name": self.name,
				"from_plan": "",
				"to_plan": plan,
				"type": "Initial Plan",
				"timestamp": self.creation,
			}
		).insert(ignore_permissions=True)

	@property
	def subscription(self):
		name = jingrow.db.get_value(
			"Subscription",
			{
				"document_type": self.pagetype,
				"document_name": self.name,
				"team": self.team,
				"plan_type": "Server Plan",
			},
		)
		return jingrow.get_pg("Subscription", name) if name else None

	@property
	def add_on_storage_subscription(self):
		name = jingrow.db.get_value(
			"Subscription",
			{
				"document_type": self.pagetype,
				"document_name": self.name,
				"team": self.team,
				"plan_type": "Server Storage Plan",
			},
		)
		return jingrow.get_pg("Subscription", name) if name else None

	def create_subscription_for_storage(self, increment: int) -> None:
		plan_type = "Server Storage Plan"
		plan = jingrow.get_value(plan_type, {"enabled": 1}, "name")

		if existing_subscription := jingrow.db.get_value(
			"Subscription",
			{
				"document_type": self.pagetype,
				"document_name": self.name,
				"team": self.team,
				"plan_type": plan_type,
				"plan": plan,
			},
			["name", "additional_storage"],
			as_dict=True,
		):
			jingrow.db.set_value(
				"Subscription",
				existing_subscription.name,
				"additional_storage",
				increment + cint(existing_subscription.additional_storage),
			)
		else:
			jingrow.get_pg(
				{
					"pagetype": "Subscription",
					"document_type": self.pagetype,
					"document_name": self.name,
					"team": self.team,
					"plan_type": plan_type,
					"plan": plan,
					"additional_storage": increment,
				}
			).insert()

	@jingrow.whitelist()
	def rename_server(self):
		self.status = "Installing"
		self.save()
		jingrow.enqueue_pg(self.pagetype, self.name, "_rename_server", queue="long", timeout=2400)

	@jingrow.whitelist()
	def archive(self):
		if jingrow.get_all(
			"Site",
			filters={"server": self.name, "status": ("!=", "Archived")},
			ignore_ifnull=True,
		):
			jingrow.throw(_("Cannot archive server with sites"))
		if jingrow.get_all(
			"Bench",
			filters={"server": self.name, "status": ("!=", "Archived")},
			ignore_ifnull=True,
		):
			jingrow.throw(_("Cannot archive server with benches"))
		self.status = "Pending"
		self.save()
		if self.is_self_hosted:
			self.status = "Archived"
			self.save()

			if self.pagetype == "Server":
				jingrow.db.set_value("Self Hosted Server", {"server": self.name}, "status", "Archived")

		else:
			jingrow.enqueue_pg(self.pagetype, self.name, "_archive", queue="long")
		self.disable_subscription()

		jingrow.db.delete("Jcloud Role Permission", {"server": self.name})

	def _archive(self):
		self.run_jcloud_job("Archive Server")

	def disable_subscription(self):
		subscription = self.subscription
		if subscription:
			subscription.disable()

		# disable add-on storage subscription
		add_on_storage_subscription = self.add_on_storage_subscription
		if add_on_storage_subscription:
			add_on_storage_subscription.disable()

	def can_change_plan(self, ignore_card_setup):
		if is_system_user(jingrow.session.user):
			return

		if ignore_card_setup:
			return

		team = jingrow.get_pg("Team", self.team)

		if team.parent_team:
			team = jingrow.get_pg("Team", team.parent_team)

		if team.payment_mode == "Paid By Partner" and team.billing_team:
			team = jingrow.get_pg("Team", team.billing_team)

		if team.is_defaulter():
			jingrow.throw("Cannot change plan because you have unpaid invoices")

		if not (team.default_payment_method or team.get_balance()):
			jingrow.throw("Cannot change plan because you haven't added a card and not have enough balance")

	@dashboard_whitelist()
	def change_plan(self, plan, ignore_card_setup=False):
		self.can_change_plan(ignore_card_setup)
		plan = jingrow.get_pg("Server Plan", plan)
		self._change_plan(plan)
		self.run_jcloud_job("Resize Server", {"machine_type": plan.instance_type})

	def _change_plan(self, plan):
		self.ram = plan.memory
		self.save()
		self.reload()
		jingrow.get_pg(
			{
				"pagetype": "Plan Change",
				"document_type": self.pagetype,
				"document_name": self.name,
				"from_plan": self.plan,
				"to_plan": plan.name,
			}
		).insert()

	@jingrow.whitelist()
	def create_image(self):
		self.run_jcloud_job("Create Server Snapshot")

	def run_jcloud_job(self, job_name, arguments=None):
		if arguments is None:
			arguments = {}
		return jingrow.get_pg(
			{
				"pagetype": "Jcloud Job",
				"job_type": job_name,
				"server_type": self.pagetype,
				"server": self.name,
				"virtual_machine": self.virtual_machine,
				"arguments": json.dumps(arguments, indent=2, sort_keys=True),
			}
		).insert()

	def get_certificate(self):
		certificate_name = jingrow.db.get_value(
			"TLS Certificate", {"wildcard": True, "domain": self.domain}, "name"
		)

		if not certificate_name and self.is_self_hosted:
			certificate_name = jingrow.db.get_value("TLS Certificate", {"domain": f"{self.name}"}, "name")

			if not certificate_name:
				self_hosted_server = jingrow.db.get_value(
					"Self Hosted Server", {"server": self.name}, ["hostname", "domain"], as_dict=1
				)

				certificate_name = jingrow.db.get_value(
					"TLS Certificate",
					{"domain": f"{self_hosted_server.hostname}.{self_hosted_server.domain}"},
					"name",
				)

		return jingrow.get_pg("TLS Certificate", certificate_name)

	def get_log_server(self):
		log_server = jingrow.db.get_single_value("Jcloud Settings", "log_server")
		if log_server:
			kibana_password = jingrow.get_pg("Log Server", log_server).get_password("kibana_password")
		else:
			kibana_password = None
		return log_server, kibana_password

	def get_monitoring_password(self):
		return jingrow.get_pg("Cluster", self.cluster).get_password("monitoring_password")

	@jingrow.whitelist()
	def increase_swap(self, swap_size=4):
		jingrow.enqueue_pg(
			self.pagetype,
			self.name,
			"_increase_swap",
			queue="long",
			timeout=1200,
			**{"swap_size": swap_size},
		)

	def add_glass_file(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_add_glass_file")

	def _add_glass_file(self):
		try:
			ansible = Ansible(playbook="glass_file.yml", server=self)
			ansible.run()
		except Exception:
			log_error("Add Glass File Exception", pg=self)

	def _increase_swap(self, swap_size=4):
		"""Increase swap by size defined in playbook"""
		from jcloud.api.server import calculate_swap

		existing_swap_size = calculate_swap(self.name).get("swap", 0)
		# We used to do 4 GB minimum swap files, to avoid conflict, name files accordingly
		swap_file_name = "swap" + str(int((existing_swap_size // 4) + 1))
		try:
			ansible = Ansible(
				playbook="increase_swap.yml",
				server=self,
				variables={
					"swap_size": swap_size,
					"swap_file": swap_file_name,
				},
			)
			ansible.run()
		except Exception:
			log_error("Increase swap exception", pg=self)

	@jingrow.whitelist()
	def setup_mysqldump(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_setup_mysqldump")

	def _setup_mysqldump(self):
		try:
			ansible = Ansible(
				playbook="mysqldump.yml",
				server=self,
			)
			ansible.run()
		except Exception:
			log_error("MySQLdump Setup Exception", pg=self)

	@jingrow.whitelist()
	def set_swappiness(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_set_swappiness")

	def _set_swappiness(self):
		try:
			ansible = Ansible(
				playbook="swappiness.yml",
				server=self,
			)
			ansible.run()
		except Exception:
			log_error("Swappiness Setup Exception", pg=self)

	def update_filebeat(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_update_filebeat")

	def _update_filebeat(self):
		try:
			ansible = Ansible(
				playbook="filebeat_update.yml",
				server=self,
			)
			ansible.run()
		except Exception:
			log_error("Filebeat Update Exception", pg=self)

	@jingrow.whitelist()
	def update_tls_certificate(self):
		from jcloud.jcloud.pagetype.tls_certificate.tls_certificate import (
			update_server_tls_certifcate,
		)

		filters = {"wildcard": True, "status": "Active", "domain": self.domain}

		if (
			hasattr(self, "is_self_hosted")
			and self.is_self_hosted
			and self.domain != self.self_hosted_server_domain
		):
			filters["domain"] = self.name
			del filters["wildcard"]

		certificate = jingrow.get_last_pg("TLS Certificate", filters)

		update_server_tls_certifcate(self, certificate)

	@jingrow.whitelist()
	def show_agent_password(self):
		return self.get_password("agent_password")

	@property
	def agent(self):
		return Agent(self.name, server_type=self.pagetype)

	@jingrow.whitelist()
	def fetch_security_updates(self):
		from jcloud.jcloud.pagetype.security_update.security_update import SecurityUpdate

		jingrow.enqueue(SecurityUpdate.fetch_security_updates, server_obj=self)

	@jingrow.whitelist()
	def configure_ssh_logging(self):
		try:
			ansible = Ansible(
				playbook="configure_ssh_logging.yml",
				server=self,
			)
			ansible.run()
		except Exception:
			log_error("Set SSH Session Logging Exception", server=self.as_dict())

	@property
	def real_ram(self):
		"""Ram detected by OS after h/w reservation"""
		return 0.972 * self.ram - 218

	@jingrow.whitelist()
	def reboot_with_serial_console(self):
		if self.provider != "AWS EC2":
			raise NotImplementedError
		console = jingrow.new_pg("Serial Console Log")
		console.server_type = self.pagetype
		console.server = self.name
		console.virtual_machine = self.virtual_machine
		console.action = "reboot"
		console.save()
		console.reload()
		console.run_sysrq()

	@dashboard_whitelist()
	def reboot(self):
		if self.provider in ("AWS EC2", "OCI"):
			virtual_machine = jingrow.get_pg("Virtual Machine", self.virtual_machine)
			virtual_machine.reboot()

	@dashboard_whitelist()
	def rename(self, title):
		self.title = title
		self.save()

	def validate_mounts(self):
		if not self.virtual_machine:
			return
		machine = jingrow.get_pg("Virtual Machine", self.virtual_machine)
		if machine.has_data_volume and len(machine.volumes) > 1 and not self.mounts:
			self.fetch_volumes_from_virtual_machine()
			self.set_default_mount_points()
			self.set_mount_properties()

	def fetch_volumes_from_virtual_machine(self):
		machine = jingrow.get_pg("Virtual Machine", self.virtual_machine)
		for volume in machine.volumes:
			if volume.device == "/dev/sda1":
				# Skip root volume. This is for AWS other providers may have different root volume
				continue
			self.append("mounts", {"volume_id": volume.volume_id})

	def set_default_mount_points(self):
		first = self.mounts[0]
		if self.pagetype == "Server":
			first.mount_point = "/opt/volumes/benches"
			self.append(
				"mounts",
				{
					"mount_type": "Bind",
					"mount_point": "/home/jingrow/benches",
					"source": "/opt/volumes/benches/home/jingrow/benches",
					"mount_point_owner": "jingrow",
					"mount_point_group": "jingrow",
				},
			)
		elif self.pagetype == "Database Server":
			first.mount_point = "/opt/volumes/mariadb"
			self.append(
				"mounts",
				{
					"mount_type": "Bind",
					"mount_point": "/var/lib/mysql",
					"source": "/opt/volumes/mariadb/var/lib/mysql",
				},
			)
			self.append(
				"mounts",
				{
					"mount_type": "Bind",
					"mount_point": "/etc/mysql",
					"source": "/opt/volumes/mariadb/etc/mysql",
				},
			)

	def set_mount_properties(self):
		for mount in self.mounts:
			# set_defaults doesn't seem to work on children in a controller hook
			default_fields = find_all(jingrow.get_meta("Server Mount").fields, lambda x: x.default)
			for field in default_fields:
				fieldname = field.fieldname
				if not mount.get(fieldname):
					mount.set(fieldname, field.default)

			mount_options = "defaults,nofail"  # Set default mount options
			if mount.mount_options:
				mount_options = f"{mount_options},{mount.mount_options}"

			mount.mount_options = mount_options
			if mount.mount_type == "Bind":
				mount.filesystem = "none"
				mount.mount_options = f"{mount.mount_options},bind"

			if mount.volume_id:
				# EBS volumes are named by their volume id
				# There's likely a better way to do this
				# https://docs.aws.amazon.com/ebs/latest/userguide/ebs-using-volumes.html
				stripped_id = mount.volume_id.replace("-", "")
				mount.source = self.get_device_from_volume_id(mount.volume_id)
				if not mount.mount_point:
					# If we don't know where to mount, mount it in /mnt/<volume_id>
					mount.mount_point = f"/mnt/{stripped_id}"

	def get_device_from_volume_id(self, volume_id):
		stripped_id = volume_id.replace("-", "")
		return f"/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_{stripped_id}"

	def get_mount_variables(self):
		return {
			"all_mounts_json": json.dumps([mount.as_dict() for mount in self.mounts], indent=4, default=str),
			"volume_mounts_json": json.dumps(
				self.get_volume_mounts(),
				indent=4,
				default=str,
			),
			"bind_mounts_json": json.dumps(
				[mount.as_dict() for mount in self.mounts if mount.mount_type == "Bind"],
				indent=4,
				default=str,
			),
		}

	def get_volume_mounts(self):
		return [mount.as_dict() for mount in self.mounts if mount.mount_type == "Volume"]

	@jingrow.whitelist()
	def mount_volumes(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_mount_volumes", queue="short", timeout=1200)

	def _mount_volumes(self):
		try:
			ansible = Ansible(
				playbook="mount.yml",
				server=self,
				variables={**self.get_mount_variables()},
			)
			play = ansible.run()
			self.reload()
			if self._set_mount_status(play):
				self.save()
		except Exception:
			log_error("Server Mount Exception", server=self.as_dict())

	def _set_mount_status(self, play):
		tasks = jingrow.get_all(
			"Ansible Task",
			["result", "task"],
			{
				"play": play.name,
				"status": ("in", ("Success", "Failure")),
				"task": ("in", ("Mount Volumes", "Mount Bind Mounts", "Show Block Device UUIDs")),
			},
		)
		mounts_changed = False
		for task in tasks:
			result = json.loads(task.result)
			for row in result.get("results", []):
				mount = find(self.mounts, lambda x: x.name == row.get("item", {}).get("name"))
				if not mount:
					mount = find(
						self.mounts, lambda x: x.name == row.get("item", {}).get("item", {}).get("name")
					)
				if not mount:
					continue
				if task.task == "Show Block Device UUIDs":
					mount.uuid = row.get("stdout", "").strip()
					mounts_changed = True
				else:
					mount_status = {True: "Failure", False: "Success"}[row.get("failed", False)]
					if mount.status != mount_status:
						mount.status = mount_status
						mounts_changed = True
		return mounts_changed

	def wait_for_cloud_init(self):
		jingrow.enqueue_pg(
			self.pagetype,
			self.name,
			"_wait_for_cloud_init",
			queue="short",
		)

	def _wait_for_cloud_init(self):
		try:
			ansible = Ansible(
				playbook="wait_for_cloud_init.yml",
				server=self,
			)
			ansible.run()
		except Exception:
			log_error("Cloud Init Wait Exception", server=self.as_dict())

	def free_space(self, mountpoint: str) -> int:
		from jcloud.api.server import prometheus_query

		response = prometheus_query(
			f"""node_filesystem_avail_bytes{{instance="{self.name}", job="node", mountpoint="{mountpoint}"}}""",
			lambda x: x["mountpoint"],
			"Asia/Kolkata",
			60,
			60,
		)["datasets"]
		if response:
			return response[0]["values"][-1]
		return 50 * 1024 * 1024 * 1024  # Assume 50GB free space

	def is_disk_full(self, mountpoint: str) -> bool:
		return self.free_space(mountpoint) == 0

	def space_available_in_6_hours(self, mountpoint: str) -> int:
		from jcloud.api.server import prometheus_query

		response = prometheus_query(
			f"""predict_linear(
node_filesystem_avail_bytes{{instance="{self.name}", mountpoint="{mountpoint}"}}[3h], 6*3600
			)""",
			lambda x: x["mountpoint"],
			"Asia/Kolkata",
			120,
			120,
		)["datasets"]
		if not response:
			return -20 * 1024 * 1024 * 1024
		return response[0]["values"][-1]

	def disk_capacity(self, mountpoint: str) -> int:
		from jcloud.api.server import prometheus_query

		response = prometheus_query(
			f"""node_filesystem_size_bytes{{instance="{self.name}", job="node", mountpoint="{mountpoint}"}}""",
			lambda x: x["mountpoint"],
			"Asia/Kolkata",
			120,
			120,
		)["datasets"]
		if response:
			return response[0]["values"][-1]
		return jingrow.db.get_value("Virtual Machine", self.virtual_machine, "disk_size") * 1024 * 1024 * 1024

	def size_to_increase_by_for_20_percent_available(self, mountpoint: str):  # min 50 GB, max 250 GB
		return int(
			min(
				self.auto_add_storage_max,
				max(
					self.auto_add_storage_min,
					abs(self.disk_capacity(mountpoint) - self.space_available_in_6_hours(mountpoint) * 5)
					/ 4
					/ 1024
					/ 1024
					/ 1024,
				),
			)
		)

	def calculated_increase_disk_size(
		self,
		additional: int = 0,
		mountpoint: str | None = None,
	):
		telegram = Telegram("Information")
		buffer = self.size_to_increase_by_for_20_percent_available(mountpoint)
		telegram.send(
			f"Increasing disk (mount point {mountpoint})on [{self.name}]({jingrow.utils.get_url_to_form(self.pagetype, self.name)}) by {buffer + additional}G"
		)
		self.increase_disk_size_for_server(self.name, buffer + additional, mountpoint)

	def prune_docker_system(self):
		jingrow.enqueue_pg(
			self.pagetype,
			self.name,
			"_prune_docker_system",
			queue="long",
			timeout=8000,
		)

	def _prune_docker_system(self):
		try:
			ansible = Ansible(
				playbook="docker_system_prune.yml",
				server=self,
			)
			ansible.run()
		except Exception:
			log_error("Prune Docker System Exception", pg=self)

	def reload_nginx(self):
		agent = Agent(self.name, server_type=self.pagetype)
		agent.reload_nginx()

	def _ssh_user(self):
		if not hasattr(self, "ssh_user"):
			return "root"
		return self.ssh_user or "root"

	def _ssh_port(self):
		if not hasattr(self, "ssh_port"):
			return 22
		return self.ssh_port or 22

	def get_primary_jingrow_public_key(self):
		if primary_public_key := jingrow.db.get_value(self.pagetype, self.primary, "jingrow_public_key"):
			return primary_public_key

		primary = jingrow.get_pg(self.pagetype, self.primary)
		ansible = Ansible(
			playbook="fetch_jingrow_public_key.yml",
			server=primary,
		)
		play = ansible.run()
		if play.status == "Success":
			return jingrow.db.get_value(self.pagetype, self.primary, "jingrow_public_key")
		jingrow.throw(f"Failed to fetch {primary.name}'s Jingrow public key")
		return None

	def copy_files(self, source, destination):
		jingrow.enqueue_pg(
			self.pagetype,
			self.name,
			"_copy_files",
			source=source,
			destination=destination,
			queue="long",
			timeout=7200,
		)

	def _copy_files(self, source, destination):
		try:
			ansible = Ansible(
				playbook="copy.yml",
				server=self,
				variables={
					"source": source,
					"destination": destination,
				},
			)
			ansible.run()
		except Exception:
			log_error("Sever File Copy Exception", server=self.as_dict())


class Server(BaseServer):
	# begin: auto-generated types
	# This code is auto-generated. Do not modify anything in this block.

	from typing import TYPE_CHECKING

	if TYPE_CHECKING:
		from jingrow.types import DF

		from jcloud.jcloud.pagetype.resource_tag.resource_tag import ResourceTag
		from jcloud.jcloud.pagetype.server_mount.server_mount import ServerMount

		agent_password: DF.Password | None
		auto_add_storage_max: DF.Int
		auto_add_storage_min: DF.Int
		cluster: DF.Link | None
		database_server: DF.Link | None
		disable_agent_job_auto_retry: DF.Check
		domain: DF.Link | None
		jingrow_public_key: DF.Code | None
		jingrow_user_password: DF.Password | None
		has_data_volume: DF.Check
		hostname: DF.Data
		hostname_abbreviation: DF.Data | None
		ignore_incidents_since: DF.Datetime | None
		ip: DF.Data | None
		is_managed_database: DF.Check
		is_primary: DF.Check
		is_replication_setup: DF.Check
		is_self_hosted: DF.Check
		is_server_prepared: DF.Check
		is_server_renamed: DF.Check
		is_server_setup: DF.Check
		is_standalone: DF.Check
		is_standalone_setup: DF.Check
		is_upstream_setup: DF.Check
		managed_database_service: DF.Link | None
		mounts: DF.Table[ServerMount]
		new_worker_allocation: DF.Check
		plan: DF.Link | None
		primary: DF.Link | None
		private_ip: DF.Data | None
		private_mac_address: DF.Data | None
		private_vlan_id: DF.Data | None
		provider: DF.Literal["Generic", "Scaleway", "AWS EC2", "OCI"]
		proxy_server: DF.Link | None
		public: DF.Check
		ram: DF.Float
		root_public_key: DF.Code | None
		self_hosted_mariadb_root_password: DF.Password | None
		self_hosted_mariadb_server: DF.Data | None
		self_hosted_server_domain: DF.Data | None
		set_bench_memory_limits: DF.Check
		skip_scheduled_backups: DF.Check
		ssh_port: DF.Int
		ssh_user: DF.Data | None
		staging: DF.Check
		status: DF.Literal["Pending", "Installing", "Active", "Broken", "Archived"]
		tags: DF.Table[ResourceTag]
		team: DF.Link | None
		title: DF.Data | None
		use_for_build: DF.Check
		use_for_new_benches: DF.Check
		use_for_new_sites: DF.Check
		virtual_machine: DF.Link | None
	# end: auto-generated types

	GUNICORN_MEMORY = 150  # avg ram usage of 1 gunicorn worker
	BACKGROUND_JOB_MEMORY = 3 * 80  # avg ram usage of 3 sets of bg workers

	def on_update(self):
		# If Database Server is changed for the server then change it for all the benches
		if not self.is_new() and (
			self.has_value_changed("database_server") or self.has_value_changed("managed_database_service")
		):
			benches = jingrow.get_all("Bench", {"server": self.name, "status": ("!=", "Archived")})
			for bench in benches:
				bench = jingrow.get_pg("Bench", bench)
				bench.database_server = self.database_server
				bench.managed_database_service = self.managed_database_service
				bench.save()

		if self.database_server:
			database_server_public = jingrow.db.get_value("Database Server", self.database_server, "public")
			if database_server_public != self.public:
				jingrow.db.set_value("Database Server", self.database_server, "public", self.public)

		if not self.is_new() and self.has_value_changed("team"):
			self.update_subscription()
			jingrow.db.delete("Jcloud Role Permission", {"server": self.name})

		# Enable bench memory limits for public servers
		if self.public:
			self.set_bench_memory_limits = True

	def after_insert(self):
		from jcloud.jcloud.pagetype.jcloud_role.jcloud_role import (
			add_permission_for_newly_created_pg,
		)

		super().after_insert()
		add_permission_for_newly_created_pg(self)

	def update_subscription(self):
		subscription = jingrow.db.get_value(
			"Subscription",
			{
				"document_type": self.pagetype,
				"document_name": self.name,
				"plan_type": "Server Plan",
				"plan": self.plan,
				"enabled": 1,
			},
			["name", "team"],
			as_dict=True,
		)
		if subscription and subscription.team != self.team:
			jingrow.get_pg("Subscription", subscription).disable()

			if subscription := jingrow.db.get_value(
				"Subscription",
				{
					"document_type": self.pagetype,
					"document_name": self.name,
					"team": self.team,
					"plan_type": "Server Plan",
					"plan": self.plan,
				},
			):
				jingrow.db.set_value("Subscription", subscription, "enabled", 1)
			else:
				try:
					# create new subscription
					jingrow.get_pg(
						{
							"pagetype": "Subscription",
							"document_type": self.pagetype,
							"document_name": self.name,
							"team": self.team,
							"plan_type": "Server Plan",
							"plan": self.plan,
						}
					).insert()
				except Exception:
					jingrow.log_error("Server Subscription Creation Error")

		add_on_storage_subscription = jingrow.db.get_value(
			"Subscription",
			{
				"document_type": self.pagetype,
				"document_name": self.name,
				"plan_type": "Server Storage Plan",
				"enabled": 1,
			},
			["name", "team", "additional_storage"],
			as_dict=True,
		)
		if add_on_storage_subscription and add_on_storage_subscription.team != self.team:
			jingrow.get_pg("Subscription", add_on_storage_subscription).disable()

			if existing_add_on_storage_subscription := jingrow.db.get_value(
				"Subscription",
				filters={
					"document_type": self.pagetype,
					"document_name": self.name,
					"team": self.team,
					"plan_type": "Server Storage Plan",
				},
			):
				jingrow.db.set_value(
					"Subscription",
					existing_add_on_storage_subscription,
					{
						"enabled": 1,
						"additional_storage": add_on_storage_subscription.additional_storage,
					},
				)
			else:
				try:
					# create new subscription
					jingrow.get_pg(
						{
							"pagetype": "Subscription",
							"document_type": self.pagetype,
							"document_name": self.name,
							"team": self.team,
							"plan_type": "Server Storage Plan",
							"plan": add_on_storage_subscription.plan,
						}
					).insert()
				except Exception:
					jingrow.log_error("Server Storage Subscription Creation Error")

	@jingrow.whitelist()
	def add_upstream_to_proxy(self):
		agent = Agent(self.proxy_server, server_type="Proxy Server")
		agent.new_server(self.name)

	def _setup_server(self):
		agent_password = self.get_password("agent_password")
		agent_repository_url = self.get_agent_repository_url()
		certificate = self.get_certificate()
		log_server, kibana_password = self.get_log_server()
		agent_sentry_dsn = jingrow.db.get_single_value("Jcloud Settings", "agent_sentry_dsn")

		try:
			ansible = Ansible(
				playbook="self_hosted.yml" if getattr(self, "is_self_hosted", False) else "server.yml",
				server=self,
				user=self._ssh_user(),
				port=self._ssh_port(),
				variables={
					"server": self.name,
					"private_ip": self.private_ip,
					"proxy_ip": self.get_proxy_ip(),
					"workers": "2",
					"agent_password": agent_password,
					"agent_repository_url": agent_repository_url,
					"agent_sentry_dsn": agent_sentry_dsn,
					"monitoring_password": self.get_monitoring_password(),
					"log_server": log_server,
					"kibana_password": kibana_password,
					"certificate_private_key": certificate.private_key,
					"certificate_full_chain": certificate.full_chain,
					"certificate_intermediate_chain": certificate.intermediate_chain,
					"docker_depends_on_mounts": self.docker_depends_on_mounts,
					**self.get_mount_variables(),
				},
			)
			play = ansible.run()
			self.reload()
			self._set_mount_status(play)
			if play.status == "Success":
				self.status = "Active"
				self.is_server_setup = True
			else:
				self.status = "Broken"
		except Exception:
			self.status = "Broken"
			log_error("Server Setup Exception", server=self.as_dict())
		self.save()

	def get_proxy_ip(self):
		"""In case of standalone setup proxy will not required"""

		if self.is_standalone:
			return self.ip

		return jingrow.db.get_value("Proxy Server", self.proxy_server, "private_ip")

	@jingrow.whitelist()
	def setup_standalone(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_setup_standalone", queue="short", timeout=1200)

	def _setup_standalone(self):
		try:
			ansible = Ansible(
				playbook="standalone.yml",
				server=self,
				user=self._ssh_user(),
				port=self._ssh_port(),
				variables={
					"server": self.name,
					"domain": self.domain,
				},
			)
			play = ansible.run()
			self.reload()
			if play.status == "Success":
				self.is_standalone_setup = True
		except Exception:
			log_error("Standalone Server Setup Exception", server=self.as_dict())
		self.save()

	@jingrow.whitelist()
	def setup_agent_sentry(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_setup_agent_sentry")

	def _setup_agent_sentry(self):
		agent_sentry_dsn = jingrow.db.get_single_value("Jcloud Settings", "agent_sentry_dsn")
		try:
			ansible = Ansible(
				playbook="agent_sentry.yml",
				server=self,
				variables={"agent_sentry_dsn": agent_sentry_dsn},
			)
			ansible.run()
		except Exception:
			log_error("Agent Sentry Setup Exception", server=self.as_dict())

	@jingrow.whitelist()
	def whitelist_ipaddress(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_whitelist_ip", queue="short", timeout=1200)

	def _whitelist_ip(self):
		proxy_server = jingrow.get_value("Server", self.name, "proxy_server")
		proxy_server_ip = jingrow.get_pg("Proxy Server", proxy_server).ip

		try:
			ansible = Ansible(
				playbook="whitelist_ipaddress.yml",
				server=self,
				variables={"ip_address": proxy_server_ip},
			)
			play = ansible.run()
			self.reload()
			self.reload()
			if play.status == "Success":
				self.status = "Active"
			else:
				self.status = "Broken"
		except Exception:
			self.status = "Broken"
			log_error("Proxy IP Whitelist Exception", server=self.as_dict())
		self.save()

	@jingrow.whitelist()
	def agent_set_proxy_ip(self):
		jingrow.enqueue_pg(self.pagetype, self.name, "_agent_set_proxy_ip", queue="short", timeout=1200)

	def _agent_set_proxy_ip(self):
		agent_password = self.get_password("agent_password")

		try:
			ansible = Ansible(
				playbook="agent_set_proxy_ip.yml",
				server=self,
				user=self._ssh_user(),
				port=self._ssh_port(),
				variables={
					"server": self.name,
					"proxy_ip": self.get_proxy_ip(),
					"workers": "2",
					"agent_password": agent_password,
				},
			)
			ansible.run()
		except Exception:
			log_error("Agent Proxy IP Setup Exception", server=self.as_dict())
		self.save()

	@jingrow.whitelist()
	def setup_fail2ban(self):
		self.status = "Installing"
		self.save()
		jingrow.enqueue_pg(self.pagetype, self.name, "_setup_fail2ban", queue="long", timeout=1200)

	def _setup_fail2ban(self):
		try:
			ansible = Ansible(
				playbook="fail2ban.yml",
				server=self,
			)
			play = ansible.run()
			self.reload()
			if play.status == "Success":
				self.status = "Active"
			else:
				self.status = "Broken"
		except Exception:
			self.status = "Broken"
			log_error("Fail2ban Setup Exception", server=self.as_dict())
		self.save()

	@jingrow.whitelist()
	def setup_replication(self):
		self.status = "Installing"
		self.save()
		jingrow.enqueue_pg(self.pagetype, self.name, "_setup_replication", queue="long", timeout=1200)

	def _setup_replication(self):
		self._setup_secondary()
		if self.status == "Active":
			primary = jingrow.get_pg("Server", self.primary)
			primary._setup_primary(self.name)
			if primary.status == "Active":
				self.is_replication_setup = True
				self.save()

	def _setup_primary(self, secondary):
		secondary_private_ip = jingrow.db.get_value("Server", secondary, "private_ip")
		try:
			ansible = Ansible(
				playbook="primary_app.yml",
				server=self,
				variables={"secondary_private_ip": secondary_private_ip},
			)
			play = ansible.run()
			self.reload()
			if play.status == "Success":
				self.status = "Active"
			else:
				self.status = "Broken"
		except Exception:
			self.status = "Broken"
			log_error("Primary Server Setup Exception", server=self.as_dict())
		self.save()

	def _setup_secondary(self):
		try:
			ansible = Ansible(
				playbook="secondary_app.yml",
				server=self,
				variables={"primary_public_key": self.get_primary_jingrow_public_key()},
			)
			play = ansible.run()
			self.reload()

			if play.status == "Success":
				self.status = "Active"
			else:
				self.status = "Broken"
		except Exception:
			self.status = "Broken"
			log_error("Secondary Server Setup Exception", server=self.as_dict())
		self.save()

	def _install_exporters(self):
		monitoring_password = jingrow.get_pg("Cluster", self.cluster).get_password("monitoring_password")
		try:
			ansible = Ansible(
				playbook="server_exporters.yml",
				server=self,
				variables={
					"private_ip": self.private_ip,
					"monitoring_password": monitoring_password,
				},
			)
			ansible.run()
		except Exception:
			log_error("Exporters Install Exception", server=self.as_dict())

	@classmethod
	def get_all_prod(cls, **kwargs) -> list[str]:
		"""Active prod servers."""
		return jingrow.get_all("Server", {"status": "Active"}, pluck="name", **kwargs)

	@classmethod
	def get_all_primary_prod(cls) -> list[str]:
		"""Active primary prod servers."""
		return jingrow.get_all("Server", {"status": "Active", "is_primary": True}, pluck="name")

	@classmethod
	def get_all_staging(cls, **kwargs) -> list[str]:
		"""Active staging servers."""
		return jingrow.get_all("Server", {"status": "Active", "staging": True}, pluck="name", **kwargs)

	@classmethod
	def get_one_staging(cls) -> str:
		return cls.get_all_staging(limit=1)[0]

	@classmethod
	def get_prod_for_new_bench(cls, extra_filters=None) -> str | None:
		filters = {"status": "Active", "use_for_new_benches": True}
		if extra_filters:
			filters.update(extra_filters)
		servers = jingrow.get_all("Server", {**filters}, pluck="name", limit=1)
		if servers:
			return servers[0]
		return None

	def _rename_server(self):
		agent_password = self.get_password("agent_password")
		agent_repository_url = self.get_agent_repository_url()
		certificate_name = jingrow.db.get_value(
			"TLS Certificate", {"wildcard": True, "domain": self.domain}, "name"
		)
		certificate = jingrow.get_pg("TLS Certificate", certificate_name)
		monitoring_password = jingrow.get_pg("Cluster", self.cluster).get_password("monitoring_password")
		log_server = jingrow.db.get_single_value("Jcloud Settings", "log_server")
		if log_server:
			kibana_password = jingrow.get_pg("Log Server", log_server).get_password("kibana_password")
		else:
			kibana_password = None

		try:
			ansible = Ansible(
				playbook="rename.yml",
				server=self,
				user=self._ssh_user(),
				port=self._ssh_port(),
				variables={
					"server": self.name,
					"private_ip": self.private_ip,
					"proxy_ip": self.get_proxy_ip(),
					"workers": "2",
					"agent_password": agent_password,
					"agent_repository_url": agent_repository_url,
					"monitoring_password": monitoring_password,
					"log_server": log_server,
					"kibana_password": kibana_password,
					"certificate_private_key": certificate.private_key,
					"certificate_full_chain": certificate.full_chain,
					"certificate_intermediate_chain": certificate.intermediate_chain,
				},
			)
			play = ansible.run()
			self.reload()
			if play.status == "Success":
				self.status = "Active"
				self.is_server_renamed = True
			else:
				self.status = "Broken"
		except Exception:
			self.status = "Broken"
			log_error("Server Rename Exception", server=self.as_dict())
		self.save()

	@jingrow.whitelist()
	def auto_scale_workers(self, commit=True):
		if self.new_worker_allocation:
			self._auto_scale_workers_new(commit)
		else:
			self._auto_scale_workers_old()

	@cached_property
	def bench_workloads(self) -> dict["Bench", int]:
		bench_workloads = {}
		benches = jingrow.get_all(
			"Bench",
			filters={"server": self.name, "status": "Active", "auto_scale_workers": True},
			pluck="name",
		)
		for bench_name in benches:
			bench = jingrow.get_pg("Bench", bench_name)
			bench_workloads[bench] = bench.workload
		return bench_workloads

	@cached_property
	def workload(self) -> int:
		return sum(self.bench_workloads.values())

	@cached_property
	def usable_ram(self) -> float:
		return max(self.ram - 3000, self.ram * 0.75)  # in MB (leaving some for disk cache + others)

	@cached_property
	def max_gunicorn_workers(self) -> int:
		usable_ram_for_gunicorn = 0.6 * self.usable_ram  # 60% of usable ram
		return usable_ram_for_gunicorn / self.GUNICORN_MEMORY

	@cached_property
	def max_bg_workers(self) -> int:
		usable_ram_for_bg = 0.4 * self.usable_ram  # 40% of usable ram
		return usable_ram_for_bg / self.BACKGROUND_JOB_MEMORY

	def _auto_scale_workers_new(self, commit):
		for bench in self.bench_workloads:
			try:
				bench.reload()
				bench.allocate_workers(
					self.workload,
					self.max_gunicorn_workers,
					self.max_bg_workers,
					self.set_bench_memory_limits,
					self.GUNICORN_MEMORY,
					self.BACKGROUND_JOB_MEMORY,
				)
				if commit:
					jingrow.db.commit()
			except jingrow.TimestampMismatchError:
				if commit:
					jingrow.db.rollback()
				continue
			except Exception:
				log_error("Bench Auto Scale Worker Error", bench=bench, workload=self.bench_workloads[bench])
				if commit:
					jingrow.db.rollback()

	def _auto_scale_workers_old(self):  # noqa: C901
		benches = jingrow.get_all(
			"Bench",
			filters={"server": self.name, "status": "Active", "auto_scale_workers": True},
			pluck="name",
		)
		for bench_name in benches:
			bench = jingrow.get_pg("Bench", bench_name)
			workload = bench.workload

			if workload <= 10:
				background_workers, gunicorn_workers = 1, 2
			elif workload <= 20:
				background_workers, gunicorn_workers = 2, 4
			elif workload <= 30:
				background_workers, gunicorn_workers = 3, 6
			elif workload <= 50:
				background_workers, gunicorn_workers = 4, 8
			elif workload <= 100:
				background_workers, gunicorn_workers = 6, 12
			elif workload <= 250:
				background_workers, gunicorn_workers = 8, 16
			elif workload <= 500:
				background_workers, gunicorn_workers = 16, 32
			else:
				background_workers, gunicorn_workers = 24, 48

			if (bench.background_workers, bench.gunicorn_workers) != (
				background_workers,
				gunicorn_workers,
			):
				bench = jingrow.get_pg("Bench", bench.name)
				bench.background_workers, bench.gunicorn_workers = (
					background_workers,
					gunicorn_workers,
				)
				bench.save()

	@jingrow.whitelist()
	def reset_sites_usage(self):
		sites = jingrow.get_all(
			"Site",
			filters={"server": self.name, "status": "Active"},
			pluck="name",
		)
		for site_name in sites:
			site = jingrow.get_pg("Site", site_name)
			site.reset_site_usage()

	def install_earlyoom(self):
		jingrow.enqueue_pg(
			self.pagetype,
			self.name,
			"_install_earlyoom",
		)

	def _install_earlyoom(self):
		try:
			ansible = Ansible(
				playbook="server_memory_limits.yml",
				server=self,
			)
			ansible.run()
		except Exception:
			log_error("Earlyoom Install Exception", server=self.as_dict())

	@property
	def docker_depends_on_mounts(self):
		mount_points = set(mount.mount_point for mount in self.mounts)
		bench_mount_points = set(["/home/jingrow/benches"])
		return bench_mount_points.issubset(mount_points)


def scale_workers(now=False):
	servers = jingrow.get_all("Server", {"status": "Active", "is_primary": True})
	for server in servers:
		try:
			if now:
				jingrow.get_pg("Server", server.name).auto_scale_workers()
			else:
				jingrow.enqueue_pg(
					"Server",
					server.name,
					method="auto_scale_workers",
					job_id=f"auto_scale_workers:{server.name}",
					deduplicate=True,
					queue="long",
					enqueue_after_commit=True,
				)
			jingrow.db.commit()
		except Exception:
			log_error("Auto Scale Worker Error", server=server)
			jingrow.db.rollback()


def process_new_server_job_update(job):
	if job.status == "Success":
		jingrow.db.set_value("Server", job.upstream, "is_upstream_setup", True)


def cleanup_unused_files():
	servers = jingrow.get_all("Server", fields=["name"], filters={"status": "Active"})
	for server in servers:
		try:
			jingrow.get_pg("Server", server.name).cleanup_unused_files()
		except Exception:
			log_error("Server File Cleanup Error", server=server)


get_permission_query_conditions = get_permission_query_conditions_for_pagetype("Server")


def get_hostname_abbreviation(hostname):
	hostname_parts = hostname.split("-")

	abbr = hostname_parts[0]

	for part in hostname_parts[1:]:
		abbr += part[0]

	return abbr


def is_dedicated_server(server_name):
	if not isinstance(server_name, str):
		jingrow.throw("Invalid argument")
	is_public = jingrow.db.get_value("Server", server_name, "public")
	return not is_public
