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

from __future__ import annotations

import contextlib
import json
from collections import defaultdict
from contextlib import suppress
from functools import cached_property, wraps
from typing import Any

import dateutil.parser
import jingrow
import jingrow.data
import jingrow.utils
import pytz
import requests
from jingrow import _
from jingrow.core.utils import find
from jingrow.jingrowclient import JingrowClient, JingrowException
from jingrow.model.page import Page
from jingrow.model.naming import append_number_if_name_exists
from jingrow.utils import (
	add_to_date,
	cint,
	comma_and,
	cstr,
	flt,
	get_datetime,
	get_url,
	now_datetime,
	sbool,
	time_diff_in_hours,
)

from jcloud.exceptions import (
	CannotChangePlan,
	InsufficientSpaceOnServer,
	SiteAlreadyArchived,
	SiteUnderMaintenance,
	VolumeResizeLimitError,
)
from jcloud.marketplace.pagetype.marketplace_app_plan.marketplace_app_plan import (
	MarketplaceAppPlan,
)
from jcloud.utils.webhook import create_webhook_event

try:
	from jingrow.utils import convert_utc_to_user_timezone
except ImportError:
	from jingrow.utils import (
		convert_utc_to_system_timezone as convert_utc_to_user_timezone,
	)

from typing import TYPE_CHECKING

from jingrow.utils.password import get_decrypted_password
from jingrow.utils.user import is_system_user

from jcloud.agent import Agent, AgentRequestSkippedException
from jcloud.api.client import dashboard_whitelist
from jcloud.api.site import check_dns, get_updates_between_current_and_next_apps
from jcloud.overrides import get_permission_query_conditions_for_pagetype
from jcloud.jcloud.pagetype.marketplace_app.marketplace_app import (
	get_plans_for_app,
	marketplace_app_hook,
)
from jcloud.jcloud.pagetype.resource_tag.tag_helpers import TagHelpers
from jcloud.jcloud.pagetype.server.server import is_dedicated_server
from jcloud.jcloud.pagetype.site_activity.site_activity import log_site_activity
from jcloud.jcloud.pagetype.site_analytics.site_analytics import create_site_analytics
from jcloud.jcloud.pagetype.site_plan.site_plan import UNLIMITED_PLANS, get_plan_config
from jcloud.jcloud.report.mariadb_slow_queries.mariadb_slow_queries import (
	get_pagetype_name,
)
from jcloud.utils import (
	convert,
	fmt_timedelta,
	get_client_blacklisted_keys,
	get_current_team,
	guess_type,
	human_readable,
	log_error,
	unique,
	validate_subdomain,
)
from jcloud.utils.dns import _change_dns_record, create_dns_record
from jcloud.api.aliyun_sms import send_renew_sms

if TYPE_CHECKING:
	from datetime import datetime

	from jingrow.types.DF import Table

	from jcloud.jcloud.pagetype.bench.bench import Bench
	from jcloud.jcloud.pagetype.bench_app.bench_app import BenchApp
	from jcloud.jcloud.pagetype.database_server.database_server import DatabaseServer
	from jcloud.jcloud.pagetype.deploy_candidate.deploy_candidate import DeployCandidate
	from jcloud.jcloud.pagetype.release_group.release_group import ReleaseGroup
	from jcloud.jcloud.pagetype.server.server import BaseServer, Server

PAGETYPE_SERVER_TYPE_MAP = {
	"Server": "Application",
	"Database Server": "Database",
	"Proxy Server": "Proxy",
}


class Site(Page, TagHelpers):
	# 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.site_app.site_app import SiteApp
		from jcloud.jcloud.pagetype.site_config.site_config import SiteConfig

		_keys_removed_in_last_update: DF.Data | None
		_site_usages: DF.Data | None
		account_request: DF.Link | None
		additional_system_user_created: DF.Check
		admin_password: DF.Password | None
		apps: DF.Table[SiteApp]
		archive_failed: DF.Check
		auto_update_last_triggered_on: DF.Datetime | None
		backup_time: DF.Time | None
		bench: DF.Link
		cluster: DF.Link
		config: DF.Code | None
		configuration: DF.Table[SiteConfig]
		current_cpu_usage: DF.Int
		current_database_usage: DF.Int
		current_disk_usage: DF.Int
		database_access_connection_limit: DF.Int
		database_name: DF.Data | None
		domain: DF.Link | None
		jerp_consultant: DF.Link | None
		free: DF.Check
		group: DF.Link
		hide_config: DF.Check
		host_name: DF.Data | None
		hybrid_saas_pool: DF.Link | None
		is_jerp_setup: DF.Check
		is_standby: DF.Check
		label: DF.Data | None
		notify_email: DF.Data | None
		only_update_at_specified_time: DF.Check
		plan: DF.Link | None
		remote_config_file: DF.Link | None
		remote_database_file: DF.Link | None
		remote_private_file: DF.Link | None
		remote_public_file: DF.Link | None
		saas_communication_secret: DF.Data | None
		server: DF.Link
		setup_wizard_complete: DF.Check
		setup_wizard_status_check_next_retry_on: DF.Datetime | None
		setup_wizard_status_check_retries: DF.Int
		signup_time: DF.Datetime | None
		site_end_date: DF.Date | None
		skip_auto_updates: DF.Check
		skip_failing_patches: DF.Check
		skip_scheduled_backups: DF.Check
		staging: DF.Check
		standby_for: DF.Link | None
		standby_for_product: DF.Link | None
		status: DF.Literal["Pending", "Installing", "Updating", "Active", "Inactive", "Broken", "Archived", "Suspended"]
		status_before_update: DF.Data | None
		subdomain: DF.Data
		tags: DF.Table[ResourceTag]
		team: DF.Link
		timezone: DF.Data | None
		trial_end_date: DF.Date | None
		update_end_of_month: DF.Check
		update_on_day_of_month: DF.Int
		update_on_weekday: DF.Literal["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
		update_trigger_frequency: DF.Literal["Daily", "Weekly", "Monthly"]
		update_trigger_time: DF.Time | None
	# end: auto-generated types

	PAGETYPE = "Site"

	dashboard_fields = (
		"ip",
		"status",
		"group",
		"notify_email",
		"team",
		"plan",
		"setup_wizard_complete",
		"archive_failed",
		"cluster",
		"bench",
		"group",
		"database_access_connection_limit",
		"site_end_date",
		"trial_end_date",
		"tags",
		"server",
		"host_name",
		"skip_auto_updates",
		"additional_system_user_created",
		"label",
		"signup_time",
	)

	@staticmethod
	def get_list_query(query, filters=None, **list_args):
		from jcloud.jcloud.pagetype.site_update.site_update import (
			benches_with_available_update,
		)

		Site = jingrow.qb.PageType("Site")

		status = filters.get("status")
		if status == "Archived":
			sites = query.where(Site.status == status).run(as_dict=1)
		else:
			benches_with_available_update = benches_with_available_update()
			sites = query.where(Site.status != "Archived").select(Site.bench).run(as_dict=1)

			for site in sites:
				if site.bench in benches_with_available_update:
					site.status = "Update Available"

		return sites

	@staticmethod
	def on_not_found(name):
		# If name is a custom domain then redirect to the site name
		site_name = jingrow.db.get_value("Site Domain", name, "site")
		if site_name:
			jingrow.response.message = {
				"redirect": f"/dashboard/sites/{site_name}",
			}
		raise

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

		group = jingrow.db.get_value(
			"Release Group",
			self.group,
			["title", "public", "team", "central_bench", "version"],
			as_dict=1,
		)
		pg.group_title = group.title
		pg.version = group.version
		pg.group_team = group.team
		pg.group_public = group.public or group.central_bench
		pg.latest_jingrow_version = jingrow.db.get_value(
			"Jingrow Version", {"status": "Stable", "public": True}, order_by="name desc"
		)
		pg.eol_versions = jingrow.db.get_all(
			"Jingrow Version",
			filters={"status": "End of Life"},
			fields=["name"],
			order_by="name desc",
			pluck="name",
		)
		pg.owner_email = jingrow.db.get_value("Team", self.team, "user")
		pg.current_usage = self.current_usage
		pg.current_plan = get("Site Plan", self.plan) if self.plan else None
		pg.last_updated = self.last_updated
		pg.has_scheduled_updates = bool(
			jingrow.db.exists("Site Update", {"site": self.name, "status": "Scheduled"})
		)
		pg.update_information = self.get_update_information()
		pg.actions = self.get_actions()
		server = jingrow.get_value("Server", self.server, ["ip", "proxy_server", "team", "title"], as_dict=1)
		pg.cluster = jingrow.db.get_value("Cluster", self.cluster, ["title", "image"], as_dict=1)
		pg.outbound_ip = server.ip
		pg.server_team = server.team
		pg.server_title = server.title
		pg.inbound_ip = self.inbound_ip
		pg.is_dedicated_server = is_dedicated_server(self.server)

		if broken_domain_tls_certificate := jingrow.db.get_value(
			"Site Domain", {"site": self.name, "status": "Broken"}, "tls_certificate"
		):
			pg.broken_domain_error = jingrow.db.get_value(
				"TLS Certificate", broken_domain_tls_certificate, "error"
			)

		return pg

	def site_action(allowed_status: list[str]):
		def outer_wrapper(func):
			@wraps(func)
			def wrapper(inst, *args, **kwargs):
				user_type = jingrow.session.data.user_type or jingrow.get_cached_value(
					"User", jingrow.session.user, "user_type"
				)
				if user_type == "System User":
					return func(inst, *args, **kwargs)
				status = jingrow.get_value(inst.pagetype, inst.name, "status", for_update=True)
				if status not in allowed_status:
					jingrow.throw(
						jingrow._("不允许对状态为 {0} 的站点执行此操作。\n允许的状态为：{1}。").format(
							jingrow.bold(jingrow._(status)), 
							jingrow.bold(comma_and([jingrow._(s) for s in allowed_status]))
						)
					)
				return func(inst, *args, **kwargs)

			return wrapper

		return outer_wrapper

	def _get_site_name(self, subdomain: str):
		"""Get full site domain name given subdomain."""
		if not self.domain:
			self.domain = jingrow.db.get_single_value("Jcloud Settings", "domain")
		return f"{subdomain}.{self.domain}"

	def autoname(self):
		self.name = self._get_site_name(self.subdomain)

	def validate(self):
		if self.has_value_changed("subdomain"):
			self.validate_site_name()
		self.validate_bench()
		self.set_site_admin_password()
		self.validate_installed_apps()
		self.validate_host_name()
		self.validate_site_config()
		self.validate_auto_update_fields()
		self.validate_site_plan()

	def before_insert(self):
		if not self.bench and self.group:
			self.set_latest_bench()
		# initialize site.config based on plan
		self._update_configuration(self.get_plan_config(), save=False)
		if not self.notify_email and self.team != "Administrator":
			self.notify_email = jingrow.db.get_value("Team", self.team, "notify_email")
		if not self.setup_wizard_status_check_next_retry_on:
			self.setup_wizard_status_check_next_retry_on = now_datetime()

	def validate_site_name(self):
		validate_subdomain(self.subdomain)

	def set_site_admin_password(self):
		# set site.admin_password if doesn't exist
		if not self.admin_password:
			self.admin_password = jingrow.generate_hash(length=16)

	def validate_bench(self):
		if (
			self.status not in ("Broken", "Archived")
			and jingrow.db.get_value("Bench", self.bench, "status", for_update=True) == "Archived"
		):
			jingrow.throw(
				f"Bench {self.bench} is not active. Please try again if you've deployed a new bench."
			)

		bench_group = jingrow.db.get_value("Bench", self.bench, "group")
		if bench_group != self.group:
			jingrow.throw(
				f"Bench release group {bench_group} is not the same as site release group {self.group}."
			)

		bench_server = jingrow.db.get_value("Bench", self.bench, "server")
		if bench_server != self.server:
			jingrow.throw(f"Bench server {bench_server} is not the same as site server {self.server}.")

	def validate_installed_apps(self):
		# validate apps to be installed on site
		bench_apps: Table[BenchApp] = jingrow.get_pg("Bench", self.bench).apps
		for app in self.apps:
			if not find(bench_apps, lambda x: x.app == app.app):
				jingrow.throw(f"app {app.app} is not available on Bench {self.bench}.")

		if self.apps[0].app != "jingrow":
			jingrow.throw("First app to be installed on site must be jingrow.")

		site_apps = [app.app for app in self.apps]
		if len(site_apps) != len(set(site_apps)):
			jingrow.throw("Can't install same app twice.")

		# Install apps in the same order as bench
		if self.is_new():
			self.sort_apps(bench_apps)

	def sort_apps(self, bench_apps: Table[BenchApp]):
		bench_app_names = [app.app for app in bench_apps]
		self.apps.sort(key=lambda x: bench_app_names.index(x.app))
		for idx, app in enumerate(self.apps):
			app.idx = idx + 1

	def validate_host_name(self):
		# set or update site.host_name
		if self.is_new():
			self.host_name = self.name
			self._update_configuration({"host_name": f"https://{self.host_name}"}, save=False)
		elif self.has_value_changed("host_name"):
			self._validate_host_name()

	def validate_site_config(self):
		# update site._keys_removed_in_last_update value
		old_keys = json.loads(self.config)
		new_keys = [x.key for x in self.configuration]
		self._keys_removed_in_last_update = json.dumps([x for x in old_keys if x not in new_keys])

		# generate site.config from site.configuration
		self.update_config_preview()

		# create an agent request if config has been updated
		# if not self.is_new() and self.has_value_changed("config"):
		# 	Agent(self.server).update_site_config(self)

	def validate_auto_update_fields(self):
		# Validate day of month
		if not (1 <= self.update_on_day_of_month <= 31):
			jingrow.throw("Day of the month must be between 1 and 31 (included)!")
		# If site is on public bench, don't allow to disable auto updates
		is_group_public = jingrow.get_cached_value("Release Group", self.group, "public")
		if self.skip_auto_updates and is_group_public:
			jingrow.throw("Auto updates can't be disabled for sites on public benches!")

	def validate_site_plan(self):
		if hasattr(self, "subscription_plan") and self.subscription_plan:
			"""
			If `release_groups` in site plan is empty, then site can be deployed in any release group.
			Otherwise, site can only be deployed in the clusters mentioned in the release groups.
			"""
			release_groups = jingrow.db.get_all(
				"Site Plan Release Group",
				pluck="release_group",
				filters={
					"parenttype": "Site Plan",
					"parentfield": "release_groups",
					"parent": self.subscription_plan,
				},
			)
			clusters = jingrow.db.get_all("Bench", pluck="cluster", filters={"group": ("in", release_groups)})
			is_valid = len(clusters) == 0 or self.cluster in clusters
			if not is_valid:
				jingrow.throw(f"In {self.subscription_plan}, you can't deploy site in {self.cluster} cluster")

			"""
			If `allowed_apps` in site plan is empty, then site can be deployed with any apps.
			Otherwise, site can only be deployed with the apps mentioned in the site plan.
			"""
			allowed_apps = jingrow.db.get_all(
				"Site Plan Allowed App",
				pluck="app",
				filters={
					"parenttype": "Site Plan",
					"parentfield": "allowed_apps",
					"parent": self.subscription_plan,
				},
			)
			if allowed_apps:
				selected_apps = [app.app for app in self.apps]

				for app in selected_apps:
					if app not in allowed_apps:
						jingrow.throw(f"In {self.subscription_plan}, you can't deploy site with {app} app")

			is_dedicated_server_plan = jingrow.db.get_value(
				"Site Plan", self.subscription_plan, "dedicated_server_plan"
			)
			is_site_on_public_server = jingrow.db.get_value("Server", self.server, "public")

			# If site is on public server, don't allow unlimited plans
			if is_site_on_public_server and is_dedicated_server_plan:
				self.subscription_plan = jingrow.db.get_value(
					"Site Plan",
					{
						"private_benches": 1,
						"dedicated_server_plan": 0,
						"document_type": "Site",
						"price_cny": ["!=", 0],
					},
					order_by="price_cny asc",
				)

			# If site is on dedicated server, set unlimited plan
			elif not is_dedicated_server_plan and not is_site_on_public_server:
				self.subscription_plan = jingrow.db.get_value(
					"Site Plan",
					{
						"dedicated_server_plan": 1,
						"document_type": "Site",
						"support_included": 0,
					},
				)

	def capture_signup_event(self, event: str):
		team = jingrow.get_pg("Team", self.team)
		if jingrow.db.count("Site", {"team": team.name}) <= 1 and team.account_request:
			from jcloud.utils.telemetry import capture

			account_request = jingrow.get_pg("Account Request", team.account_request)
			if not (account_request.is_saas_signup() or account_request.invited_by_parent_team):
				capture(event, "fc_signup", team.user)

	def on_update(self):
		if self.status == "Active" and self.has_value_changed("host_name"):
			self.update_site_config({"host_name": f"https://{self.host_name}"})
			self._update_redirects_for_all_site_domains()
			jingrow.db.set_value("Site Domain", self.host_name, "redirect_to_primary", False)

		self.update_subscription()

		if self.has_value_changed("team"):
			jingrow.db.set_value("Site Domain", {"site": self.name}, "team", self.team)
			jingrow.db.delete("Jcloud Role Permission", {"site": self.name})

		if self.status not in [
			"Pending",
			"Archived",
			"Suspended",
		] and (self.has_value_changed("subdomain") or self.has_value_changed("domain")):
			self.rename(self._get_site_name(self.subdomain))

		# Telemetry: Send event if first site status changed to Active
		if self.status == "Active" and self.has_value_changed("status"):
			self.capture_signup_event("first_site_status_changed_to_active")

		if self.has_value_changed("status"):
			create_site_status_update_webhook_event(self.name)

	def generate_saas_communication_secret(self, create_agent_job=False, save=True):
		if not self.standby_for and not self.standby_for_product:
			return
		if not self.saas_communication_secret:
			self.saas_communication_secret = jingrow.generate_hash(length=32)
			config = {
				"fc_communication_secret": self.saas_communication_secret,
			}
			if create_agent_job:
				self.update_site_config(config)
			else:
				self._update_configuration(config=config, save=save)

	def rename_upstream(self, new_name: str):
		proxy_server = jingrow.db.get_value("Server", self.server, "proxy_server")
		agent = Agent(proxy_server, server_type="Proxy Server")
		site_domains = jingrow.get_all(
			"Site Domain", {"site": self.name, "name": ("!=", self.name)}, pluck="name"
		)
		agent.rename_upstream_site(self.server, self, new_name, site_domains)

	def set_apps(self, apps: list):
		self.apps = []
		bench_apps = jingrow.get_pg("Bench", self.bench).apps
		for app in apps:
			if not find(bench_apps, lambda x: x.app == app):
				continue
			self.append("apps", {"app": app})
		self.save()

	@jingrow.whitelist()
	def sync_apps(self):
		agent = Agent(self.server)
		apps_list = agent.get_site_apps(site=self)
		self.set_apps(apps_list)

	@jingrow.whitelist()
	def retry_rename(self):
		"""Retry rename with current subdomain"""
		if self.name != self._get_site_name(self.subdomain):
			self.rename(self._get_site_name(self.subdomain))
		else:
			jingrow.throw("Please choose a different subdomain")

	@jingrow.whitelist()
	def retry_archive(self):
		"""Retry archive with subdomain+domain name of site"""
		site_name = self.subdomain + "." + self.domain
		if jingrow.db.exists("Site", {"name": site_name, "bench": self.bench}):
			jingrow.throw(f"Another site already exists in {self.bench} with name: {site_name}")
		self.archive(site_name=site_name, reason="Retry Archive")

	def check_duplicate_site(self):
		if jingrow.db.exists(
			"Site",
			{
				"subdomain": self.subdomain,
				"domain": self.domain,
				"status": ("!=", "Archived"),
				"name": ("!=", self.name),
			},
		):
			jingrow.throw("Site with same subdomain already exists")

	def rename(self, new_name: str):
		self.check_duplicate_site()
		create_dns_record(pg=self, record_name=self._get_site_name(self.subdomain))
		agent = Agent(self.server)
		if self.standby_for_product or self.standby_for:
			# if standby site, rename site and create first user for trial signup
			create_user = self.get_user_details()
			# update the subscription config while renaming the standby site
			self.update_config_preview()
			site_config = json.loads(self.config)
			subscription_config = site_config.get("subscription", {})
			job = agent.rename_site(self, new_name, create_user, config={"subscription": subscription_config})
			self.flags.rename_site_agent_job_name = job.name
		else:
			agent.rename_site(self, new_name)
		self.rename_upstream(new_name)
		self.status = "Pending"
		self.save()

		try:
			# remove old dns record from route53 after rename
			domain = jingrow.get_pg("Root Domain", self.domain)
			proxy_server = jingrow.get_value("Server", self.server, "proxy_server")
			self.remove_dns_record(domain, proxy_server, self.name)
		except Exception:
			log_error("Removing Old Site from Route53 Failed")

	def update_config_preview(self):
		"""Regenerates site.config on each site.validate from the site.configuration child table data"""
		new_config = {}

		# Update from site.configuration
		for row in self.configuration:
			# update internal flag from master
			row.internal = jingrow.db.get_value("Site Config Key", row.key, "internal")
			key_type = row.type or row.get_type()
			row.type = key_type

			if key_type == "Number":
				key_value = int(row.value) if isinstance(row.value, (float, int)) else json.loads(row.value)
			elif key_type == "Boolean":
				key_value = (
					row.value if isinstance(row.value, bool) else bool(sbool(json.loads(cstr(row.value))))
				)
			elif key_type == "JSON":
				key_value = json.loads(cstr(row.value))
			else:
				key_value = row.value

			new_config[row.key] = key_value

		self.config = json.dumps(new_config, indent=4)

	def install_marketplace_conf(self, app: str, plan: str | None = None):
		if plan:
			MarketplaceAppPlan.create_marketplace_app_subscription(self.name, app, plan, self.team)
		marketplace_app_hook(app=app, site=self, op="install")

	def uninstall_marketplace_conf(self, app: str):
		marketplace_app_hook(app=app, site=self, op="uninstall")

		# disable marketplace plan if it exists
		marketplace_app_name = jingrow.db.get_value("Marketplace App", {"app": app})
		app_subscription = jingrow.db.exists(
			"Subscription",
			{
				"team": self.team,
				"site": self.name,
				"document_type": "Marketplace App",
				"document_name": marketplace_app_name,
			},
		)
		if marketplace_app_name and app_subscription:
			jingrow.db.set_value("Subscription", app_subscription, "enabled", 0)

	def check_marketplace_app_installable(self, plan: str | None = None):
		if not plan:
			return
		if (
			not jingrow.db.get_value("Marketplace App Plan", plan, "price_usd") <= 0
			and not jingrow.local.team().can_install_paid_apps()
		):
			jingrow.throw(
				"You cannot install a Paid app on Free Credits. Please buy credits before trying to install again."
			)

			# TODO: check if app is available and can be installed

	@dashboard_whitelist()
	@site_action(["Active"])
	def install_app(self, app: str, plan: str | None = None) -> str:
		self.check_marketplace_app_installable(plan)

		if find(self.apps, lambda x: x.app == app):
			return None

		agent = Agent(self.server)
		job = agent.install_app_site(self, app)
		log_site_activity(self.name, "Install App", app, job.name)
		self.status = "Pending"
		self.save()
		self.install_marketplace_conf(app, plan)

		return job.name

	@dashboard_whitelist()
	@site_action(["Active"])
	def uninstall_app(self, app: str) -> str:
		agent = Agent(self.server)
		job = agent.uninstall_app_site(self, app)

		log_site_activity(self.name, "Uninstall App", app, job.name)

		self.uninstall_marketplace_conf(app)
		self.status = "Pending"
		self.save()

		return job.name

	def _create_default_site_domain(self):
		"""Create Site Domain with Site name."""
		return jingrow.get_pg(
			{
				"pagetype": "Site Domain",
				"site": self.name,
				"domain": self.name,
				"status": "Active",
				"retry_count": 0,
				"dns_type": "CNAME",
			}
		).insert(ignore_if_duplicate=True)

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

		self.capture_signup_event("created_first_site")

		if hasattr(self, "subscription_plan") and self.subscription_plan:
			# create subscription
			self.create_subscription(self.subscription_plan)
			self.reload()

		if hasattr(self, "app_plans") and self.app_plans:
			for app, plan in self.app_plans.items():
				MarketplaceAppPlan.create_marketplace_app_subscription(
					self.name, app, plan["name"], self.team, True
				)

		# log activity
		log_site_activity(self.name, "Create")
		self._create_default_site_domain()
		create_dns_record(self, record_name=self._get_site_name(self.subdomain))
		self.create_agent_request()

		if hasattr(self, "share_details_consent") and self.share_details_consent:
			# create partner lead
			jingrow.get_pg(pagetype="Partner Lead", team=self.team, site=self.name).insert(
				ignore_permissions=True
			)

		if self.backup_time:
			self.backup_time = None  # because FF by default sets it to current time
			self.save()
		add_permission_for_newly_created_pg(self)

		create_site_status_update_webhook_event(self.name)

	def remove_dns_record(self, domain: Page, proxy_server: str, site: str):
		"""Remove dns record of site pointing to proxy."""
		_change_dns_record(method="DELETE", domain=domain, proxy_server=proxy_server, record_name=site)

	def is_version_14_or_higher(self) -> bool:
		group: ReleaseGroup = jingrow.get_cached_pg("Release Group", self.group)
		return group.is_version_14_or_higher()

	@property
	def space_required_on_app_server(self):
		db_size, public_size, private_size = (
			jingrow.get_pg("Remote File", file_name).size if file_name else 0
			for file_name in (
				self.remote_database_file,
				self.remote_public_file,
				self.remote_private_file,
			)
		)
		space_for_download = db_size + public_size + private_size
		space_for_extracted_files = (
			(0 if self.is_version_14_or_higher() else (8 * db_size)) + public_size + private_size
		)  # 8 times db size for extraction; estimated
		return space_for_download + space_for_extracted_files

	@property
	def space_required_on_db_server(self):
		db_size = jingrow.get_pg("Remote File", self.remote_database_file).size
		return 8 * db_size * 2  # double extracted size for binlog

	def check_and_increase_disk(self, server: "BaseServer", space_required: int):
		mountpoint = server.guess_data_disk_mountpoint()
		free_space = server.free_space(mountpoint)
		if (diff := free_space - space_required) <= 0:
			msg = f"Insufficient estimated space on {PAGETYPE_SERVER_TYPE_MAP[server.pagetype]} server to create site. Required: {human_readable(space_required)}, Available: {human_readable(free_space)} (Need {human_readable(abs(diff))})."
			if server.public:
				self.try_increasing_disk(server, mountpoint, diff, msg)
			else:
				jingrow.throw(msg, InsufficientSpaceOnServer)

	def try_increasing_disk(self, server: "BaseServer", mountpoint: str, diff: int, err_msg: str):
		try:
			server.calculated_increase_disk_size(mountpoint=mountpoint, additional=diff / 1024 / 1024 // 1024)
		except VolumeResizeLimitError:
			jingrow.throw(
				f"{err_msg} Please wait {fmt_timedelta(server.time_to_wait_before_updating_volume)} before trying again.",
				InsufficientSpaceOnServer,
			)

	def check_enough_space_on_server(self):
		app: "Server" = jingrow.get_pg("Server", self.server)
		self.check_and_increase_disk(app, self.space_required_on_app_server)

		if app.database_server:
			db: "DatabaseServer" = jingrow.get_pg("Database Server", app.database_server)
			self.check_and_increase_disk(db, self.space_required_on_db_server)

	def create_agent_request(self):
		agent = Agent(self.server)
		if self.remote_database_file:
			agent.new_site_from_backup(self, skip_failing_patches=self.skip_failing_patches)
		else:
			"""
			If the site is creating for saas / product trial purpose,
			Create a system user with password at the time of site creation.

			If `ignore_additional_system_user_creation` is set, don't create additional system user
			"""
			if (self.standby_for_product or self.standby_for) and not self.is_standby:
				user_details = self.get_user_details()
				if self.flags.get("ignore_additional_system_user_creation", False):
					user_details = None
				self.flags.new_site_agent_job_name = agent.new_site(self, create_user=user_details).name
			else:
				self.flags.new_site_agent_job_name = agent.new_site(self).name

		server = jingrow.get_all("Server", filters={"name": self.server}, fields=["proxy_server"], limit=1)[0]

		agent = Agent(server.proxy_server, server_type="Proxy Server")
		agent.new_upstream_file(server=self.server, site=self.name)

	@dashboard_whitelist()
	@site_action(["Active", "Broken"])
	def reinstall(self):
		agent = Agent(self.server)
		job = agent.reinstall_site(self)
		log_site_activity(self.name, "Reinstall", job=job.name)
		self.status = "Pending"
		self.save()
		return job.name

	@dashboard_whitelist()
	@site_action(["Active", "Broken"])
	def migrate(self, skip_failing_patches=False):
		agent = Agent(self.server)
		activate = True
		if self.status in ("Inactive", "Suspended"):
			activate = False
			self.status_before_update = self.status
		elif self.status == "Broken" and self.status_before_update in (
			"Inactive",
			"Suspended",
		):
			activate = False
		job = agent.migrate_site(self, skip_failing_patches=skip_failing_patches, activate=activate)
		log_site_activity(self.name, "Migrate", job=job.name)
		self.status = "Pending"
		self.save()

	@jingrow.whitelist()
	def last_migrate_failed(self):
		"""Returns `True` if the last site update's(`Migrate` deploy type) migrate site job step failed, `False` otherwise"""

		site_update = jingrow.get_all(
			"Site Update",
			filters={"site": self.name},
			fields=["status", "update_job", "deploy_type"],
			limit=1,
			order_by="creation desc",
		)

		if not (site_update and site_update[0].deploy_type == "Migrate"):
			return False
		site_update = site_update[0]

		if site_update.status == "Recovered":
			migrate_site_step = jingrow.get_all(
				"Agent Job Step",
				filters={
					"step_name": "Migrate Site",
					"agent_job": site_update.update_job,
				},
				fields=["status"],
				limit=1,
			)

			if migrate_site_step and migrate_site_step[0].status == "Failure":
				return True

		return False

	@jingrow.whitelist()
	def restore_tables(self):
		self.status_before_update = self.status
		agent = Agent(self.server)
		agent.restore_site_tables(self)
		self.status = "Pending"
		self.save()

	@dashboard_whitelist()
	def clear_site_cache(self):
		agent = Agent(self.server)
		job = agent.clear_site_cache(self)

		log_site_activity(self.name, "Clear Cache", job=job.name)

	@dashboard_whitelist()
	@site_action(["Active", "Broken"])
	def restore_site(self, skip_failing_patches=False):
		if not jingrow.get_pg("Remote File", self.remote_database_file).exists():
			raise Exception(f"Remote File {self.remote_database_file} is unavailable on S3")

		agent = Agent(self.server)
		job = agent.restore_site(self, skip_failing_patches=skip_failing_patches)
		log_site_activity(self.name, "Restore", job=job.name)
		self.status = "Pending"
		self.save()
		return job.name

	@dashboard_whitelist()
	@site_action(["Active", "Broken"])
	def restore_site_from_files(self, files, skip_failing_patches=False):
		self.remote_database_file = files["database"]
		self.remote_public_file = files["public"]
		self.remote_private_file = files["private"]
		self.save()
		self.reload()
		return self.restore_site(skip_failing_patches=skip_failing_patches)

	@jingrow.whitelist()
	def physical_backup(self):
		return self.backup(physical=True)

	@dashboard_whitelist()
	def backup(self, with_files=False, offsite=False, force=False, physical=False):
		if self.status == "Suspended":
			activity = jingrow.db.get_all(
				"Site Activity",
				filters={"site": self.name, "action": "Suspend Site"},
				order_by="creation desc",
				limit=1,
			)
			suspension_time = jingrow.get_pg("Site Activity", activity[0]).creation

			if (
				jingrow.db.count(
					"Site Backup",
					filters=dict(
						site=self.name,
						status="Success",
						creation=(">=", suspension_time),
					),
				)
				> 3
			):
				jingrow.throw("You cannot take more than 3 backups after site suspension")

		return jingrow.get_pg(
			{
				"pagetype": "Site Backup",
				"site": self.name,
				"with_files": with_files,
				"offsite": offsite,
				"force": force,
				"physical": physical,
			}
		).insert()

	@dashboard_whitelist()
	def get_backup_download_link(self, backup, file):
		from botocore.exceptions import ClientError

		if file not in ["database", "public", "private", "config"]:
			jingrow.throw("Invalid file type")

		try:
			remote_file = jingrow.db.get_value(
				"Site Backup",
				{"name": backup, "site": self.name},
				f"remote_{file}_file",
			)
			return jingrow.get_pg("Remote File", remote_file).download_link
		except ClientError:
			log_error(title="Offsite Backup Response Exception")

	def site_migration_scheduled(self):
		return jingrow.db.get_value(
			"Site Migration", {"site": self.name, "status": "Scheduled"}, "scheduled_time"
		)

	def site_update_scheduled(self):
		return jingrow.db.get_value(
			"Site Update", {"site": self.name, "status": "Scheduled"}, "scheduled_time"
		)

	def check_move_scheduled(self):
		if time := self.site_migration_scheduled():
			jingrow.throw(f"Site Migration is scheduled for {self.name} at {time}")
		if time := self.site_update_scheduled():
			jingrow.throw(f"Site Update is scheduled for {self.name} at {time}")

	def ready_for_move(self):
		if self.status in ["Updating", "Pending", "Installing"]:
			jingrow.throw(f"Site is in {self.status} state. Cannot Update", SiteUnderMaintenance)
		elif self.status == "Archived":
			jingrow.throw("Site is archived. Cannot Update", SiteAlreadyArchived)
		self.check_move_scheduled()

		self.status_before_update = self.status
		self.status = "Pending"
		self.save()

	@dashboard_whitelist()
	@site_action(["Active", "Inactive", "Suspended"])
	def schedule_update(
		self,
		skip_failing_patches: bool = False,
		skip_backups: bool = False,
		physical_backup: bool = False,
		scheduled_time: str | None = None,
	):
		log_site_activity(self.name, "Update")

		pg = jingrow.get_pg(
			{
				"pagetype": "Site Update",
				"site": self.name,
				"backup_type": "Physical" if physical_backup else "Logical",
				"skipped_failing_patches": skip_failing_patches,
				"skipped_backups": skip_backups,
				"status": "Scheduled" if scheduled_time else "Pending",
				"scheduled_time": scheduled_time,
			}
		).insert()
		return pg.name

	@dashboard_whitelist()
	def edit_scheduled_update(
		self,
		name,
		skip_failing_patches: bool = False,
		skip_backups: bool = False,
		scheduled_time: str | None = None,
	):
		pg = jingrow.get_pg("Site Update", name)
		pg.skipped_failing_patches = skip_failing_patches
		pg.skipped_backups = skip_backups
		pg.scheduled_time = scheduled_time
		pg.save()
		return pg.name

	@dashboard_whitelist()
	def cancel_scheduled_update(self, site_update: str):
		if status := jingrow.db.get_value("Site Update", site_update, "status") != "Scheduled":
			jingrow.throw(f"Cannot cancel a Site Update with status {status}")

		# TODO: Set status to cancelled instead of deleting the pg
		jingrow.delete_pg("Site Update", site_update)

	@jingrow.whitelist()
	def move_to_group(self, group, skip_failing_patches=False, skip_backups=False):
		log_site_activity(self.name, "Update")

		return jingrow.get_pg(
			{
				"pagetype": "Site Update",
				"site": self.name,
				"destination_group": group,
				"skipped_failing_patches": skip_failing_patches,
				"skipped_backups": skip_backups,
				"ignore_past_failures": True,
			}
		).insert()

	@jingrow.whitelist()
	def move_to_bench(self, bench, deactivate=True, skip_failing_patches=False):
		jingrow.only_for("System Manager")
		self.ready_for_move()

		if bench == self.bench:
			jingrow.throw("Site is already on the selected bench.")

		agent = Agent(self.server)
		job = agent.move_site_to_bench(self, bench, deactivate, skip_failing_patches)
		log_site_activity(self.name, "Update", job=job.name)

		return job

	def reset_previous_status(self, fix_broken=False):
		if self.status == "Archived":
			return
		self.status = self.status_before_update
		self.status_before_update = None
		if not self.status or (self.status == "Broken" and fix_broken):
			status_map = {402: "Suspended", 503: "Inactive"}
			try:
				response = requests.get(f"https://{self.name}")
				self.status = status_map.get(response.status_code, "Active")
			except Exception:
				log_error("Site Status Fetch Error", site=self.name)
		self.save()

	@jingrow.whitelist()
	@site_action(["Active"])
	def update_without_backup(self):
		log_site_activity(self.name, "Update")

		jingrow.get_pg(
			{
				"pagetype": "Site Update",
				"site": self.name,
				"skipped_backups": 1,
			}
		).insert()

	@dashboard_whitelist()
	def add_domain(self, domain):
		domain = domain.lower().strip(".")
		response = check_dns(self.name, domain)
		if response["matched"]:
			if jingrow.db.exists("Site Domain", {"domain": domain}):
				jingrow.throw(f"The domain {jingrow.bold(domain)} is already used by a site")

			log_site_activity(self.name, "Add Domain")
			jingrow.get_pg(
				{
					"pagetype": "Site Domain",
					"status": "Pending",
					"site": self.name,
					"domain": domain,
					"dns_type": "CNAME",
					"dns_response": json.dumps(response, indent=4, default=str),
				}
			).insert()

	def add_domain_for_product_site(self, domain):
		domain = domain.lower().strip(".")
		log_site_activity(self.name, "Add Domain")
		create_dns_record(pg=self, record_name=domain)
		jingrow.get_pg(
			{
				"pagetype": "Site Domain",
				"status": "Pending",
				"site": self.name,
				"domain": domain,
				"dns_type": "CNAME",
			}
		).insert()

	@jingrow.whitelist()
	def create_dns_record(self):
		domains = jingrow.db.get_list(
			"Site Domain", filters={"site": self.name}, fields=["domain"], pluck="domain"
		)
		for domain in domains:
			if bool(jingrow.db.exists("Root Domain", domain.split(".", 1)[1], "name")):
				create_dns_record(pg=self, record_name=domain)

	@jingrow.whitelist()
	def update_dns_record(self, value):
		domain = jingrow.get_pg("Root Domain", self.domain)
		record_name = self._get_site_name(self.subdomain)
		_change_dns_record("UPSERT", domain, value, record_name)

	def get_config_value_for_key(self, key: str) -> Any:
		"""
		Get site config value configuration child table for given key.

		:returns: None if key not in config.
		"""
		key_obj = find(self.configuration, lambda x: x.key == key)
		if key_obj:
			return json.loads(key_obj.get("value"))
		return None

	def add_domain_to_config(self, domain: str):
		domains = self.get_config_value_for_key("domains") or []
		domains.append(domain)
		self._update_configuration({"domains": domains})
		agent = Agent(self.server)
		agent.add_domain(self, domain)

	def remove_domain_from_config(self, domain):
		domains = self.get_config_value_for_key("domains") or []
		if domain not in domains:
			return
		domains.remove(domain)
		self._update_configuration({"domains": domains})
		agent = Agent(self.server)
		agent.remove_domain(self, domain)

	@dashboard_whitelist()
	def remove_domain(self, domain):
		if domain == self.name:
			jingrow.throw("Cannot delete default site_domain")
		site_domain = jingrow.get_all("Site Domain", filters={"site": self.name, "domain": domain})[0]
		site_domain = jingrow.delete_pg("Site Domain", site_domain.name)

	def retry_add_domain(self, domain):
		if check_dns(self.name, domain)["matched"]:
			site_domain = jingrow.get_all(
				"Site Domain",
				filters={
					"site": self.name,
					"domain": domain,
					"status": ("!=", "Active"),
					"retry_count": ("<=", 5),
				},
			)[0]
			site_domain = jingrow.get_pg("Site Domain", site_domain.name)
			site_domain.retry()

	def _check_if_domain_belongs_to_site(self, domain: str):
		if not jingrow.db.exists({"pagetype": "Site Domain", "site": self.name, "domain": domain}):
			jingrow.throw(
				msg=f"Site Domain {domain} for site {self.name} does not exist",
				exc=jingrow.exceptions.LinkValidationError,
			)

	def _check_if_domain_is_active(self, domain: str):
		status = jingrow.get_value("Site Domain", domain, "status")
		if status != "Active":
			jingrow.throw(msg="Only active domains can be primary", exc=jingrow.LinkValidationError)

	def _validate_host_name(self):
		"""Perform checks for primary domain."""
		self._check_if_domain_belongs_to_site(self.host_name)
		self._check_if_domain_is_active(self.host_name)

	@dashboard_whitelist()
	def set_host_name(self, domain: str):
		"""Set host_name/primary domain of site."""
		self.host_name = domain
		self.save()

	def _get_redirected_domains(self) -> list[str]:
		"""Get list of redirected site domains for site."""
		return jingrow.get_all(
			"Site Domain",
			filters={"site": self.name, "redirect_to_primary": True},
			pluck="name",
		)

	def _update_redirects_for_all_site_domains(self):
		domains = self._get_redirected_domains()
		if domains:
			return self.set_redirects_in_proxy(domains)
		return None

	def _remove_redirects_for_all_site_domains(self):
		domains = self._get_redirected_domains()
		if domains:
			self.unset_redirects_in_proxy(domains)

	def set_redirects_in_proxy(self, domains: list[str]):
		target = self.host_name
		proxy_server = jingrow.db.get_value("Server", self.server, "proxy_server")
		agent = Agent(proxy_server, server_type="Proxy Server")
		return agent.setup_redirects(self.name, domains, target)

	def unset_redirects_in_proxy(self, domains: list[str]):
		proxy_server = jingrow.db.get_value("Server", self.server, "proxy_server")
		agent = Agent(proxy_server, server_type="Proxy Server")
		agent.remove_redirects(self.name, domains)

	@dashboard_whitelist()
	def set_redirect(self, domain: str):
		"""Enable redirect to primary for domain."""
		self._check_if_domain_belongs_to_site(domain)
		site_domain = jingrow.get_pg("Site Domain", domain)
		site_domain.setup_redirect()

	@dashboard_whitelist()
	def unset_redirect(self, domain: str):
		"""Disable redirect to primary for domain."""
		self._check_if_domain_belongs_to_site(domain)
		site_domain = jingrow.get_pg("Site Domain", domain)
		site_domain.remove_redirect()

	@dashboard_whitelist()
	@site_action(["Active", "Broken", "Suspended"])
	def archive(self, site_name=None, reason=None, force=False, skip_reload=False):
		agent = Agent(self.server)
		self.status = "Pending"
		self.save()
		job = agent.archive_site(self, site_name, force)
		log_site_activity(self.name, "Archive", reason, job.name)

		server = jingrow.get_all("Server", filters={"name": self.server}, fields=["proxy_server"], limit=1)[0]

		agent = Agent(server.proxy_server, server_type="Proxy Server")
		agent.remove_upstream_file(
			server=self.server,
			site=self.name,
			site_name=site_name,
			skip_reload=skip_reload,
		)

		self.db_set("host_name", None)

		self.delete_offsite_backups()
		jingrow.db.set_value(
			"Site Backup",
			{"site": self.name, "offsite": False},
			"files_availability",
			"Unavailable",
		)
		self.disable_subscription()
		self.disable_marketplace_subscriptions()

		self.archive_site_database_users()

	@jingrow.whitelist()
	def cleanup_after_archive(self):
		site_cleanup_after_archive(self.name)

	def delete_offsite_backups(self):
		from jcloud.jcloud.pagetype.remote_file.remote_file import (
			delete_remote_backup_objects,
		)

		log_site_activity(self.name, "Drop Offsite Backups")

		sites_remote_files = []
		site_backups = jingrow.get_all(
			"Site Backup",
			filters={
				"site": self.name,
				"offsite": True,
				"files_availability": "Available",
			},
			pluck="name",
			order_by="creation desc",
		)[1:]  # Keep latest backup
		for backup_files in jingrow.get_all(
			"Site Backup",
			filters={"name": ("in", site_backups)},
			fields=[
				"remote_database_file",
				"remote_public_file",
				"remote_private_file",
			],
			as_list=True,
			order_by="creation desc",
			ignore_ifnull=True,
		):
			sites_remote_files += backup_files

		if not sites_remote_files:
			return None

		jingrow.db.set_value(
			"Site Backup",
			{"name": ("in", site_backups), "offsite": True},
			"files_availability",
			"Unavailable",
		)

		return delete_remote_backup_objects(sites_remote_files)

	@dashboard_whitelist()
	def send_change_team_request(self, team_mail_id: str, reason: str):
		"""Send email to team to accept site transfer request"""

		if self.team != get_current_team():
			jingrow.throw(
				"You should belong to the team owning the site to initiate a site ownership transfer."
			)

		if not jingrow.db.exists("Team", {"user": team_mail_id, "enabled": 1}):
			jingrow.throw("No Active Team record found.")

		old_team = jingrow.db.get_value("Team", self.team, "user")

		if old_team == team_mail_id:
			jingrow.throw(f"Site is already owned by the team {team_mail_id}")

		key = jingrow.generate_hash("Site Transfer Link", 20)
		jingrow.get_pg(
			{
				"pagetype": "Team Change",
				"document_type": "Site",
				"document_name": self.name,
				"to_team": jingrow.db.get_value("Team", {"user": team_mail_id, "enabled": 1}),
				"from_team": self.team,
				"reason": reason,
				"key": key,
			}
		).insert()

		link = get_url(f"/api/action/jcloud.api.site.confirm_site_transfer?key={key}")

		if jingrow.conf.developer_mode:
			print(f"\nSite transfer link for {team_mail_id}\n{link}\n")

		jingrow.sendmail(
			recipients=team_mail_id,
			subject="Transfer Site Ownership Confirmation",
			template="transfer_team_confirmation",
			args={
				"name": self.host_name or self.name,
				"type": "site",
				"old_team": old_team,
				"new_team": team_mail_id,
				"transfer_url": link,
			},
		)

	@dashboard_whitelist()
	@site_action(["Active", "Broken"])
	def login_as_admin(self, reason=None):
		sid = self.login(reason=reason)
		# Redirect to setup-wizard if setup is not complete
		route = "/setup-wizard" if not self.setup_wizard_complete else "/app"
		return f"https://{self.host_name or self.name}{route}?sid={sid}"

	@dashboard_whitelist()
	@site_action(["Active"])
	def login_as_team(self, reason=None):
		if self.additional_system_user_created:
			team_user = jingrow.db.get_value("Team", self.team, "user")
			sid = self.get_login_sid(user=team_user)
			# Redirect to setup-wizard if setup is not complete
			if not self.setup_wizard_complete:
				redirect_route = "/setup-wizard"
			elif self.standby_for_product:
				redirect_route = (
					jingrow.db.get_value("Product Trial", self.standby_for_product, "redirect_to_after_login")
					or "/app"
				)
			else:
				redirect_route = "/app"
			return f"https://{self.host_name or self.name}{redirect_route}?sid={sid}"

		jingrow.throw("No additional system user created for this site")
		return None

	@site_action(["Active"])
	def login_as_user(self, user_email, reason=None):
		try:
			sid = self.get_login_sid(user=user_email)
			# Redirect to setup-wizard if setup is not complete
			if not self.setup_wizard_complete:
				redirect_route = "/setup-wizard"
			elif self.standby_for_product:
				redirect_route = (
					jingrow.db.get_value("Product Trial", self.standby_for_product, "redirect_to_after_login")
					or "/app"
				)
			else:
				redirect_route = "/app"
			return f"https://{self.host_name or self.name}{redirect_route}?sid={sid}"
		except Exception as e:
			jingrow.throw(str(e))

	@jingrow.whitelist()
	def login(self, reason=None):
		log_site_activity(self.name, "Login as Administrator", reason=reason)
		return self.get_login_sid()

	def create_user_with_team_info(self):
		team_user = jingrow.db.get_value("Team", self.team, "user")
		user = jingrow.get_pg("User", team_user)
		return self.create_user(user.email, user.first_name or "", user.last_name or "")

	def create_user(self, email, first_name, last_name, password=None):
		if self.additional_system_user_created:
			return None
		agent = Agent(self.server)
		return agent.create_user(self, email, first_name, last_name, password)

	@jingrow.whitelist()
	def show_admin_password(self):
		jingrow.msgprint(self.get_password("admin_password"), title="Password", indicator="green")

	def get_connection_as_admin(self):
		password = get_decrypted_password("Site", self.name, "admin_password")
		return JingrowClient(f"https://{self.name}", "Administrator", password)

	def get_sid_from_agent(self, user: str) -> str | None:
		try:
			agent = Agent(self.server)
			return agent.get_site_sid(self, user)
		except requests.HTTPError as e:
			if "validate_ip_address" in str(e):
				jingrow.throw(
					f"Login with {user}'s credentials is IP restricted. Please remove the same and try again.",
					jingrow.ValidationError,
				)
			elif f"User {user} does not exist" in str(e):
				jingrow.throw(f"User {user} does not exist in the site", jingrow.ValidationError)
			elif jingrow.db.exists(
				"Incident",
				{
					"server": self.server,
					"status": ("not in", ["Resolved", "Auto-Resolved", "Jcloud-Resolved"]),
				},
			):
				jingrow.throw(
					"Server appears to be unresponsive. Please try again in some time.",
					jingrow.ValidationError,
				)
			else:
				raise e
		except AgentRequestSkippedException:
			jingrow.throw(
				"Server is unresponsive. Please try again in some time.",
				jingrow.ValidationError,
			)

	def get_login_sid(self, user: str = "Administrator"):
		sid = None
		if user == "Administrator":
			password = get_decrypted_password("Site", self.name, "admin_password")
			response = requests.post(
				f"https://{self.name}/api/action/login",
				data={"usr": user, "pwd": password},
			)
			sid = response.cookies.get("sid")
		if not sid or sid == "Guest":
			sid = self.get_sid_from_agent(user)
		if not sid or sid == "Guest":
			jingrow.throw(f"Could not login as {user}", jingrow.ValidationError)
		return sid

	def fetch_info(self):
		agent = Agent(self.server)
		return agent.get_site_info(self)

	def fetch_analytics(self):
		agent = Agent(self.server)
		if agent.should_skip_requests():
			return None
		return agent.get_site_analytics(self)

	def get_disk_usages(self):
		try:
			last_usage = jingrow.get_last_pg("Site Usage", {"site": self.name})
		except jingrow.DoesNotExistError:
			return defaultdict(lambda: None)

		return {
			"database": last_usage.database,
			"database_free": last_usage.database_free,
			"backups": last_usage.backups,
			"public": last_usage.public,
			"private": last_usage.private,
			"creation": last_usage.creation,
		}

	def _sync_config_info(self, fetched_config: dict) -> bool:
		"""Update site pg config with the fetched_config values.

		:fetched_config: Generally data passed is the config part of the agent info response
		:returns: True if value has changed
		"""
		config = {
			key: fetched_config[key] for key in fetched_config if key not in get_client_blacklisted_keys()
		}
		new_config = {**json.loads(self.config or "{}"), **config}
		current_config = json.dumps(new_config, indent=4)

		if self.config != current_config:
			self._update_configuration(new_config, save=False)
			return True
		return False

	def _sync_usage_info(self, fetched_usage: dict):
		"""Generate a Site Usage pg for the site using the fetched_usage data.

		:fetched_usage: Requires backups, database, public, private keys with Numeric values
		"""

		if isinstance(fetched_usage, list):
			for usage in fetched_usage:
				self._insert_site_usage(usage)
		else:
			self._insert_site_usage(fetched_usage)

	def _insert_site_usage(self, usage: dict):
		current_usages = self.get_disk_usages()
		site_usage_data = {
			"site": self.name,
			"backups": usage["backups"],
			"database": usage["database"],
			"database_free": usage.get("database_free", 0),
			"database_free_tables": json.dumps(usage.get("database_free_tables", []), indent=1),
			"public": usage["public"],
			"private": usage["private"],
		}

		same_as_last_usage = (
			current_usages["backups"] == site_usage_data["backups"]
			and current_usages["database"] == site_usage_data["database"]
			and current_usages["public"] == site_usage_data["public"]
			and current_usages["private"] == site_usage_data["private"]
			and current_usages["database_free"] == site_usage_data["private"]
		)

		if same_as_last_usage:
			return

		equivalent_site_time = None
		if usage.get("timestamp"):
			equivalent_site_time = convert_utc_to_user_timezone(
				dateutil.parser.parse(usage["timestamp"])
			).replace(tzinfo=None)
			if jingrow.db.exists("Site Usage", {"site": self.name, "creation": equivalent_site_time}):
				return
			if current_usages["creation"] and equivalent_site_time < current_usages["creation"]:
				return

		site_usage = jingrow.get_pg({"pagetype": "Site Usage", **site_usage_data}).insert()

		if equivalent_site_time:
			site_usage.db_set("creation", equivalent_site_time)

	def _sync_timezone_info(self, timezone: str) -> bool:
		"""Update site pg timezone with the passed value of timezone.

		:timezone: Timezone passed in part of the agent info response
		:returns: True if value has changed
		"""
		# Validate timezone string
		# Empty string is fine, since we default to IST
		if timezone:
			try:
				pytz.timezone(timezone)
			except pytz.exceptions.UnknownTimeZoneError:
				return False

		if self.timezone != timezone:
			self.timezone = timezone
			return True
		return False

	def _sync_database_name(self, config):
		database_name = config.get("db_name")
		if self.database_name != database_name:
			self.database_name = database_name
			return True
		return False

	@jingrow.whitelist()
	def sync_info(self, data=None):
		"""Updates Site Usage, site.config and timezone details for site."""
		if not data:
			data = self.fetch_info()

		if not data:
			return

		fetched_usage = data["usage"]
		fetched_config = data["config"]
		fetched_timezone = data["timezone"]

		self._sync_usage_info(fetched_usage)
		to_save = self._sync_config_info(fetched_config)
		to_save |= self._sync_timezone_info(fetched_timezone)
		to_save |= self._sync_database_name(fetched_config)

		if to_save:
			self.save()

	def sync_analytics(self, analytics=None):
		if not analytics:
			analytics = self.fetch_analytics()
		if analytics:
			create_site_analytics(self.name, analytics)

	def create_sync_user_webhook(self):
		"""
		Create 3 webhook records in the site to sync the user with jcloud
		- One for user record creation
		- One for user record update
		- One for user record deletion
		"""
		conn = self.get_connection_as_admin()
		pagetype_data = {
			"pagetype": "Webhook",
			"webhook_pagetype": "User",
			"enabled": 1,
			"request_url": "https://jingrow.com/api/action/jcloud.api.site_login.sync_product_site_user",
			"request_method": "POST",
			"request_structure": "JSON",
			"webhook_json": """{ "user_info": { "email": "{{pg.email}}", "enabled": "{{pg.enabled}}" } }""",
			"webhook_headers": [
				{"key": "x-site", "value": self.name},
				{"key": "Content-Type", "value": "application/json"},
				{"key": "x-site-token", "value": self.saas_communication_secret},
			],
		}

		webhook_data = [
			{
				"name": "Sync User records with Jingrow on create",
				"webhook_docevent": "after_insert",
			},
			{
				"name": "Sync User records with Jingrow on update",
				"webhook_docevent": "on_update",
				"condition": """pg.has_value_changed("enabled")""",
			},
			{
				"name": "Sync User records with Jingrow on delete",
				"webhook_docevent": "on_trash",
			},
		]

		for webhook in webhook_data:
			try:
				conn.insert({**pagetype_data, **webhook})
			except JingrowException as ex:
				if "jingrow.exceptions.DuplicateEntryError" not in str(ex):
					raise ex

	def sync_users_to_product_site(self, analytics=None):
		from jcloud.jcloud.pagetype.site_user.site_user import create_user_for_product_site

		if self.is_standby:
			return
		if not analytics:
			analytics = self.fetch_analytics()
		if analytics:
			create_user_for_product_site(self.name, analytics)

	@dashboard_whitelist()
	def is_setup_wizard_complete(self):
		if self.setup_wizard_complete:
			return True

		sid = self.get_login_sid()
		conn = JingrowClient(f"https://{self.name}?sid={sid}")

		try:
			value = conn.get_value("System Settings", "setup_complete", "System Settings")
		except json.JSONDecodeError:
			# the proxy might be down or network failure
			# that's why the response is blank and get_value try to parse the json
			# and raise json.JSONDecodeError
			return False
		except Exception:
			if self.ping().status_code == requests.codes.ok:
				# Site is up but setup status fetch failed
				log_error("Fetching Setup Status Failed", pg=self)
			return False

		setup_complete = cint(value["setup_complete"])
		if not setup_complete:
			return False

		self.reload()
		self.setup_wizard_complete = 1

		self.team = (
			jingrow.db.get_value(
				"Team",
				{"user": jingrow.db.get_value("Account Request", self.account_request, "email")},
				"name",
			)
			if self.team == "Administrator"
			else self.team
		)

		self.save()

		# Telemetry: Send event if first site status changed to Active
		if self.setup_wizard_complete:
			self.capture_signup_event("first_site_setup_wizard_completed")

		return setup_complete

	def fetch_setup_wizard_complete_status(self):
		with suppress(Exception):
			# max retries = 18, backoff time = 10s, with exponential backoff it will try for 30 days
			if self.setup_wizard_status_check_retries >= 18:
				return
			is_completed = self.is_setup_wizard_complete()
			if not is_completed:
				self.setup_wizard_status_check_retries += 1
				exponential_backoff_duration = 10 * (2**self.setup_wizard_status_check_retries)
				self.setup_wizard_status_check_next_retry_on = add_to_date(
					now_datetime(), seconds=exponential_backoff_duration
				)
				self.save()

	@jingrow.whitelist()
	def set_status_based_on_ping(self):
		if self.status in ("Active", "Archived", "Inactive", "Suspended"):
			return
		try:
			response = self.ping()
		except Exception:
			return
		else:
			if response.status_code == requests.codes.ok:
				self.status = "Active"
				self.save()

	def ping(self):
		return requests.get(f"https://{self.name}/api/action/ping")

	def _set_configuration(self, config: list[dict]):
		"""Similar to _update_configuration but will replace full configuration at once
		This is necessary because when you update site config from the UI, you can update the key,
		update the value, remove the key. All of this can be handled by setting the full configuration at once.

		Args:
		config (list): List of dicts with key, value, and type
		"""
		blacklisted_config = [x for x in self.configuration if x.key in get_client_blacklisted_keys()]
		self.configuration = []

		# Maintain keys that aren't accessible to Dashboard user
		for i, _config in enumerate(blacklisted_config):
			_config.idx = i + 1
			self.configuration.append(_config)

		for d in config:
			d = jingrow._dict(d)
			if isinstance(d.value, (dict, list)):
				value = json.dumps(d.value)
			else:
				value = d.value
			# Value is mandatory, skip None and empty strings
			if value is None or cstr(value).strip() == "":
				continue
			self.append("configuration", {"key": d.key, "value": value, "type": d.type})
		self.save()

	def _update_configuration(self, config, save=True):
		"""Updates site.configuration, runs site.save which updates site.config

		Args:
		config (dict): Python dict for any suitable jingrow.conf
		"""
		existing_keys = {x.key: i for i, x in enumerate(self.configuration)}
		for key, value in config.items():
			_type = jingrow.get_value("Site Config Key", {"key": key}, "type") or guess_type(value)
			converted_value = convert(value)
			if converted_value is None or cstr(converted_value).strip() == "":
				continue
			if key in existing_keys:
				self.configuration[existing_keys[key]].value = converted_value
				self.configuration[existing_keys[key]].type = _type
			else:
				self.append(
					"configuration",
					{"key": key, "value": converted_value, "type": _type},
				)

		if save:
			self.save()

	@dashboard_whitelist()
	@site_action(["Active"])
	def update_config(self, config=None):
		"""Updates site.configuration, meant for dashboard and API users"""
		if config is None:
			return
		# config = {"key1": value1, "key2": value2}
		config = jingrow.parse_json(config)

		sanitized_config = {}
		for key, value in config.items():
			if key in get_client_blacklisted_keys():
				jingrow.throw(_(f"The key <b>{key}</b> is blacklisted or internal and cannot be updated"))

			_type = self._site_config_key_type(key, value)

			if _type == "Number":
				value = flt(value)
			elif _type == "Boolean":
				value = bool(sbool(value))
			elif _type == "JSON":
				value = jingrow.parse_json(value)
			elif _type == "Password" and value == "*******":
				value = jingrow.get_value("Site Config", {"key": key, "parent": self.name}, "value")
			sanitized_config[key] = value

		self.update_site_config(sanitized_config)

	def _site_config_key_type(self, key, value):
		if jingrow.db.exists("Site Config Key", key):
			return jingrow.db.get_value("Site Config Key", key, "type")

		if isinstance(value, (dict, list)):
			return "JSON"
		if isinstance(value, bool):
			return "Boolean"
		if isinstance(value, (int, float)):
			return "Number"
		return "String"

	@dashboard_whitelist()
	@site_action(["Active"])
	def delete_config(self, key):
		"""Deletes a key from site configuration, meant for dashboard and API users"""
		if key in get_client_blacklisted_keys():
			return None

		updated_config = []
		for row in self.configuration:
			if row.key != key and not row.internal:
				updated_config.append({"key": row.key, "value": row.value, "type": row.type})

		return self.update_site_config(updated_config)

	def delete_multiple_config(self, keys: list[str]):
		# relies on self._keys_removed_in_last_update in self.validate
		# used by https://jingrow.com/app/marketplace-app/email_delivery_service
		config_list: list[dict] = []
		for key in self.configuration:
			config = {}
			if key.key in keys:
				continue
			config["key"] = key.key
			config["value"] = key.value
			config["type"] = key.type
			config_list.append(config)
		self.update_site_config(config_list)

	@jingrow.whitelist()
	def update_site_config(self, config=None):
		"""Updates site.configuration, site.config and runs site.save which initiates an Agent Request
		This checks for the blacklisted config keys via Jingrow Validations, but not for internal usages.
		Don't expose this directly to an external API. Pass through `jcloud.utils.sanitize_config` or use
		`jcloud.api.site.update_config` instead.

		Args:
		config (dict): Python dict for any suitable jingrow.conf
		"""
		if config is None:
			config = {}
		if isinstance(config, list):
			self._set_configuration(config)
		else:
			self._update_configuration(config)
		return Agent(self.server).update_site_config(self)

	def update_site(self):
		log_site_activity(self.name, "Update")

	def create_subscription(self, plan):
		# create a site plan change log
		self._create_initial_site_plan_change(plan)

	def update_subscription(self):
		if self.status in ["Archived", "Broken", "Suspended"]:
			self.disable_subscription()
		else:
			self.enable_subscription()

		if self.has_value_changed("team"):
			subscription = self.subscription
			if subscription:
				subscription.team = self.team
				subscription.save(ignore_permissions=True)

	def enable_subscription(self):
		subscription = self.subscription
		if subscription:
			subscription.enable()

	def disable_subscription(self):
		subscription = self.subscription
		if subscription:
			jingrow.db.set_value("Subscription", subscription.name, "enabled", False)

	def disable_marketplace_subscriptions(self):
		app_subscriptions = jingrow.get_all(
			"Marketplace App Subscription",
			filters={"site": self.name, "status": "Active"},
			pluck="name",
		)

		for subscription in app_subscriptions:
			subscription_pg = jingrow.get_pg("Marketplace App Subscription", subscription)
			subscription_pg.disable()

		subscriptions = jingrow.get_all("Subscription", {"site": self.name, "enabled": 1}, pluck="name")
		for subscription in subscriptions:
			subscription_pg = jingrow.get_pg("Subscription", subscription)
			subscription_pg.disable()

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

		if ignore_card_setup:
			# ignore card setup for prepaid app payments
			return

		if bool(jingrow.db.get_value("Cluster", self.cluster, "hybrid")):
			# skip validation if site is on hybrid server
			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", CannotChangePlan)

		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",
				CannotChangePlan,
			)

	# TODO: rename to change_plan and remove the need for ignore_card_setup param
	@dashboard_whitelist()
	def set_plan(self, plan):
		from jcloud.api.site import validate_plan

		validate_plan(self.server, plan)
		self.change_plan(plan)

	def change_plan(self, plan, ignore_card_setup=False):
		self.can_change_plan(ignore_card_setup)
		plan_config = self.get_plan_config(plan)

		self._update_configuration(plan_config)
		ret = jingrow.get_pg(
			{
				"pagetype": "Site Plan Change",
				"site": self.name,
				"from_plan": self.plan,
				"to_plan": plan,
			}
		).insert()

		self.reload()
		if self.status == "Suspended":
			self.unsuspend_if_applicable()
		else:
			# trigger agent job only once
			self.update_site_config(plan_config)

		if self.trial_end_date:
			self.reload()
			self.trial_end_date = ""
			self.save()

		jingrow.enqueue_pg(
			self.pagetype,
			self.name,
			"revoke_database_access_on_plan_change",
			enqueue_after_commit=True,
		)
		return ret

	def archive_site_database_users(self):
		db_users = jingrow.get_all(
			"Site Database User",
			filters={
				"site": self.name,
				"status": ("!=", "Archived"),
			},
			pluck="name",
		)

		for db_user in db_users:
			jingrow.get_pg("Site Database User", db_user).archive(
				raise_error=False, skip_remove_db_user_step=True
			)

	def revoke_database_access_on_plan_change(self):
		# If the new plan doesn't have database access, disable it
		if jingrow.db.get_value("Site Plan", self.plan, "database_access"):
			return

		self.archive_site_database_users()

	def unsuspend_if_applicable(self):
		try:
			usage = jingrow.get_last_pg("Site Usage", {"site": self.name})
		except jingrow.DoesNotExistError:
			# If no pg is found, it means the site was created a few moments before
			# team was suspended, potentially due to failure in payment. Don't unsuspend
			# site in that case. team.unsuspend_sites should handle that, then.
			return

		plan_name = self.plan
		# get plan from subscription
		if not plan_name:
			subscription = self.subscription
			if not subscription:
				return
			plan_name = subscription.plan

		plan = jingrow.get_pg("Site Plan", plan_name)

		disk_usage = usage.public + usage.private
		if usage.database < plan.max_database_usage and disk_usage < plan.max_storage_usage:
			self.current_database_usage = (usage.database / plan.max_database_usage) * 100
			self.current_disk_usage = ((usage.public + usage.private) / plan.max_storage_usage) * 100
			self.unsuspend(reason="Plan Upgraded")

	@dashboard_whitelist()
	@site_action(["Active", "Broken"])
	def deactivate(self):
		plan = jingrow.db.get_value("Site Plan", self.plan, ["is_jingrow_plan", "is_trial_plan"], as_dict=True)
		if self.plan and plan.is_trial_plan:
			jingrow.throw(_("Cannot deactivate site on a trial plan"))

		if self.plan and plan.is_jingrow_plan:
			jingrow.throw(_("Cannot deactivate site on a Jingrow plan"))

		log_site_activity(self.name, "Deactivate Site")
		self.status = "Inactive"
		self.update_site_config({"maintenance_mode": 1})
		self.update_site_status_on_proxy("deactivated")

	@dashboard_whitelist()
	@site_action(["Inactive", "Broken"])
	def activate(self):
		log_site_activity(self.name, "Activate Site")
		self.status = "Active"
		self.update_site_config({"maintenance_mode": 0})
		self.update_site_status_on_proxy("activated")
		self.reactivate_app_subscriptions()

	@jingrow.whitelist()
	def suspend(self, reason=None, skip_reload=False):
		log_site_activity(self.name, "Suspend Site", reason)
		self.status = "Suspended"
		self.update_site_config({"maintenance_mode": 1})
		self.update_site_status_on_proxy("suspended", skip_reload=skip_reload)
		self.deactivate_app_subscriptions()

	def deactivate_app_subscriptions(self):
		jingrow.db.set_value(
			"Marketplace App Subscription",
			{"status": "Active", "site": self.name},
			{"status": "Inactive"},
		)

	def reactivate_app_subscriptions(self):
		jingrow.db.set_value(
			"Marketplace App Subscription",
			{"status": "Inactive", "site": self.name},
			{"status": "Active"},
		)

	@jingrow.whitelist()
	@site_action(["Suspended"])
	def unsuspend(self, reason=None, skip_reload=False):
		log_site_activity(self.name, "Unsuspend Site", reason)
		self.status = "Active"
		self.update_site_config({"maintenance_mode": 0})
		self.update_site_status_on_proxy("activated", skip_reload=skip_reload)
		self.reactivate_app_subscriptions()

	@jingrow.whitelist()
	def reset_site_usage(self):
		agent = Agent(self.server)
		agent.reset_site_usage(self)

	def update_site_status_on_proxy(self, status, skip_reload=False):
		proxy_server = jingrow.db.get_value("Server", self.server, "proxy_server")
		agent = Agent(proxy_server, server_type="Proxy Server")
		agent.update_site_status(self.server, self.name, status, skip_reload)

	def get_user_details(self):
		if jingrow.db.get_value("Team", self.team, "user") == "Administrator" and self.account_request:
			ar = jingrow.get_pg("Account Request", self.account_request)
			user_email = ar.email
			user_first_name = ar.first_name
			user_last_name = ar.last_name
		else:
			user_email = jingrow.db.get_value("Team", self.team, "user")
			user = jingrow.db.get_value(
				"User", {"email": user_email}, ["first_name", "last_name"], as_dict=True
			)
			user_first_name = user.first_name if (user and user.first_name) else ""
			user_last_name = user.last_name if (user and user.last_name) else ""
		payload = {
			"email": user_email,
			"first_name": user_first_name or "",
			"last_name": user_last_name or "",
		}
		"""
		If the site is created for product trial,
		we might have collected the password from end-user for his site
		"""
		if self.account_request and self.standby_for_product and not self.is_standby:
			with contextlib.suppress(jingrow.DoesNotExistError):
				# fetch the product trial request
				product_trial_request = jingrow.get_pg(
					"Product Trial Request",
					{
						"account_request": self.account_request,
						"product_trial": self.standby_for_product,
						"site": self.name,
					},
				)
				setup_wizard_completion_mode = jingrow.get_value(
					"Product Trial", product_trial_request.product_trial, "setup_wizard_completion_mode"
				)
				if setup_wizard_completion_mode == "manual":
					password = product_trial_request.get_user_login_password_from_signup_details()
					if password:
						payload["password"] = password

		return payload

	def setup_jerp(self):
		account_request = jingrow.get_pg("Account Request", self.account_request)
		agent = Agent(self.server)
		user = {
			"email": account_request.email,
			"first_name": account_request.first_name,
			"last_name": account_request.last_name,
		}
		config = {
			"setup_config": {
				"country": account_request.country,
				"timezone": account_request.timezone,
				"domain": account_request.domain,
				"currency": account_request.currency,
				"language": account_request.language,
				"company": account_request.company,
			}
		}
		agent.setup_jerp(self, user, config)

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

	def can_charge_for_subscription(self, subscription=None):
		today = jingrow.utils.getdate()
		return (
			self.status not in ["Archived", "Suspended"]
			and self.team
			and self.team != "Administrator"
			and not self.free
			and (today > get_datetime(self.trial_end_date).date() if self.trial_end_date else True)
		)

	def get_plan_name(self, plan=None):
		if not plan:
			plan = self.subscription_plan if hasattr(self, "subscription_plan") else self.plan
		if plan and not isinstance(plan, str):
			jingrow.throw("Site.subscription_plan must be a string")
		return plan

	def get_plan_config(self, plan=None):
		plan = self.get_plan_name(plan)
		config = get_plan_config(plan)
		if plan in UNLIMITED_PLANS:
			# PERF: do not enable usage tracking on unlimited sites.
			config.pop("rate_limit", None)
		return config

	def set_latest_bench(self):
		from pypika.terms import PseudoColumn

		if not (self.domain and self.cluster and self.group):
			jingrow.throw("domain, cluster and group are required to create site")

		proxy_servers_names = jingrow.db.get_all(
			"Proxy Server Domain", {"domain": self.domain}, pluck="parent"
		)
		proxy_servers = jingrow.db.get_all(
			"Proxy Server",
			{"status": "Active", "name": ("in", proxy_servers_names)},
			pluck="name",
		)

		"""
		For restricted plans, just choose any bench from the release groups and clusters combination
		For others, don't allow to deploy on those specific release group benches, choose anything except that
		"""

		release_group_names = []
		if self.get_plan_name():
			release_group_names = jingrow.db.get_all(
				"Site Plan Release Group",
				pluck="release_group",
				filters={
					"parenttype": "Site Plan",
					"parentfield": "release_groups",
					"parent": self.get_plan_name(),
				},
			)

		Bench = jingrow.qb.PageType("Bench")
		Server = jingrow.qb.PageType("Server")

		bench_query = (
			jingrow.qb.from_(Bench)
			.select(
				Bench.name,
				Bench.server,
				Bench.group,
				PseudoColumn(f"`tabBench`.`cluster` = '{self.cluster}' `in_primary_cluster`"),
			)
			.left_join(Server)
			.on(Bench.server == Server.name)
			.where(Server.proxy_server.isin(proxy_servers))
			.where(Bench.status == "Active")
			.orderby(PseudoColumn("in_primary_cluster"), order=jingrow.qb.desc)
			.orderby(Server.use_for_new_sites, order=jingrow.qb.desc)
			.orderby(Bench.creation, order=jingrow.qb.desc)
			.limit(1)
		)
		if release_group_names:
			bench_query = bench_query.where(Bench.group.isin(release_group_names))
		else:
			restricted_release_group_names = jingrow.db.get_all(
				"Site Plan Release Group",
				pluck="release_group",
				filters={"parenttype": "Site Plan", "parentfield": "release_groups"},
			)
			if self.group in restricted_release_group_names:
				jingrow.throw(f"Site can't be deployed on this release group {self.group} due to restrictions")
			bench_query = bench_query.where(Bench.group == self.group)
		if self.server:
			bench_query = bench_query.where(Server.name == self.server)

		result = bench_query.run(as_dict=True)
		if len(result) == 0:
			jingrow.throw("No bench available to deploy this site")
			return

		self.bench = result[0].name
		self.server = result[0].server
		if release_group_names:
			self.group = result[0].group

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

	def check_db_access_enabling(self):
		if jingrow.db.get_value(
			"Agent Job",
			filters={
				"site": self.name,
				"job_type": "Add User to ProxySQL",
				"status": ["in", ["Running", "Pending"]],
			},
			for_update=True,
		):
			jingrow.throw("Database Access is already being enabled on this site. Please check after a while.")

	def get_auto_update_info(self):
		fields = [
			"auto_updates_scheduled",
			"auto_update_last_triggered_on",
			"update_trigger_frequency",
			"update_trigger_time",
			"update_on_weekday",
			"update_end_of_month",
			"update_on_day_of_month",
		]
		return {field: self.get(field) for field in fields}

	def get_update_information(self):
		from jcloud.jcloud.pagetype.site_update.site_update import (
			benches_with_available_update,
		)

		out = jingrow._dict()
		out.update_available = self.bench in benches_with_available_update(site=self.name)
		if not out.update_available:
			return out

		bench: "Bench" = jingrow.get_pg("Bench", self.bench)
		source = bench.candidate
		destinations = jingrow.get_all(
			"Deploy Candidate Difference",
			filters={"source": source},
			limit=1,
			pluck="destination",
		)
		if not destinations:
			out.update_available = False
			return out

		destination = destinations[0]

		destination_candidate: "DeployCandidate" = jingrow.get_pg("Deploy Candidate", destination)

		current_apps = bench.apps
		next_apps = destination_candidate.apps
		out.apps = get_updates_between_current_and_next_apps(current_apps, next_apps)

		out.installed_apps = self.apps
		out.update_available = any([app["update_available"] for app in out.apps])
		return out

	def fetch_running_optimize_tables_job(self):
		return jingrow.db.exists(
			"Agent Job",
			{
				"site": self.name,
				"job_type": "Optimize Tables",
				"status": ["in", ["Undelivered", "Running", "Pending"]],
			},
		)

	@dashboard_whitelist()
	def optimize_tables(self, ignore_checks: bool = False):
		if not ignore_checks:
			# check for running `Optimize Tables` agent job
			if job := self.fetch_running_optimize_tables_job():
				return {
					"success": True,
					"message": "Optimize Tables job is already running on this site.",
					"job_name": job,
				}
			# check if `Optimize Tables` has run in last 1 hour
			recent_agent_job_name = jingrow.db.exists(
				"Agent Job",
				{
					"site": self.name,
					"job_type": "Optimize Tables",
					"status": ["not in", ["Failure", "Delivery Failure"]],
					"creation": [">", jingrow.utils.add_to_date(jingrow.utils.now_datetime(), hours=-1)],
				},
			)
			if recent_agent_job_name:
				return {
					"success": False,
					"message": "Optimize Tables job has already run in the last 1 hour. Try later.",
					"job_name": None,
				}

		agent = Agent(self.server)
		job_name = agent.optimize_tables(self).name
		return {
			"success": True,
			"message": "Optimize Tables has been triggered on this site.",
			"job_name": job_name,
		}

	@dashboard_whitelist()
	def get_database_performance_report(self):
		from jcloud.jcloud.report.mariadb_slow_queries.mariadb_slow_queries import get_data as get_slow_queries

		agent = Agent(self.server)
		# fetch slow queries of last 7 days
		slow_queries = get_slow_queries(
			jingrow._dict(
				{
					"database": self.database_name,
					"start_datetime": jingrow.utils.add_to_date(None, days=-7),
					"stop_datetime": jingrow.utils.now_datetime(),
					"search_pattern": ".*",
					"max_lines": 2000,
					"normalize_queries": True,
				}
			)
		)
		# convert all the float to int
		for query in slow_queries:
			for key, value in query.items():
				if isinstance(value, float):
					query[key] = int(value)

		# Sort by duration
		slow_queries.sort(key=lambda x: x["duration"], reverse=True)

		is_performance_schema_enabled = False
		if database_server := jingrow.db.get_value("Server", self.server, "database_server"):
			is_performance_schema_enabled = jingrow.db.get_value(
				"Database Server",
				database_server,
				"is_performance_schema_enabled",
			)
		result = None
		if is_performance_schema_enabled:
			with suppress(Exception):
				# for larger table or if database has any locks, fetching perf report will be failed
				result = agent.get_summarized_performance_report_of_database(self)
				# remove `parent` & `creation` indexes from unused_indexes
				result["unused_indexes"] = [
					index
					for index in result.get("unused_indexes", [])
					if index["index_name"] not in ["parent", "creation"]
				]

		if not result:
			result = {}
			result["unused_indexes"] = []
			result["redundant_indexes"] = []
			result["top_10_time_consuming_queries"] = []
			result["top_10_queries_with_full_table_scan"] = []

		# sort the slow queries by `rows_examined`
		result["slow_queries"] = sorted(slow_queries, key=lambda x: x["rows_examined"], reverse=True)
		result["is_performance_schema_enabled"] = is_performance_schema_enabled
		return result

	@property
	def server_logs(self):
		return Agent(self.server).get(f"benches/{self.bench}/sites/{self.name}/logs")

	def get_server_log(self, log):
		return Agent(self.server).get(f"benches/{self.bench}/sites/{self.name}/logs/{log}")

	def get_server_log_for_log_browser(self, log):
		return Agent(self.server).get(f"benches/{self.bench}/sites/{self.name}/logs_v2/{log}")

	@property
	def has_paid(self) -> bool:
		"""Has the site been paid for by customer."""
		invoice_items = jingrow.get_all(
			"Invoice Item",
			{
				"document_type": self.pagetype,
				"document_name": self.name,
				"Amount": (">", 0),
			},
			pluck="parent",
		)
		today = jingrow.utils.getdate()
		today_last_month = jingrow.utils.add_to_date(today, months=-1)
		last_month_last_date = jingrow.utils.get_last_day(today_last_month)
		return jingrow.db.exists(
			"Invoice",
			{
				"status": "Paid",
				"name": ("in", invoice_items or ["NULL"]),
				"period_end": (">=", last_month_last_date),
				# this month's or last month's invoice has been paid for
			},
		)

	@property
	def inbound_ip(self):
		server = jingrow.db.get_value(
			"Server",
			self.server,
			["ip", "is_standalone", "proxy_server", "team"],
			as_dict=True,
		)
		if server.is_standalone:
			ip = server.ip
		else:
			ip = jingrow.db.get_value("Proxy Server", server.proxy_server, "ip")
		return ip

	@property
	def current_usage(self):
		from jcloud.api.analytics import get_current_cpu_usage

		result = jingrow.db.get_all(
			"Site Usage",
			fields=["database", "public", "private"],
			filters={"site": self.name},
			order_by="creation desc",
			limit=1,
		)
		usage = result[0] if result else {}

		# number of hours until cpu usage resets
		now = jingrow.utils.now_datetime()
		today_end = now.replace(hour=23, minute=59, second=59)
		hours_left_today = flt(time_diff_in_hours(today_end, now), 2)

		return {
			"cpu": flt(get_current_cpu_usage(self.name) / (3.6 * (10**9)), 5),
			"storage": usage.get("public", 0) + usage.get("private", 0),
			"database": usage.get("database", 0),
			"hours_until_cpu_usage_resets": hours_left_today,
		}

	@property
	def last_updated(self):
		result = jingrow.db.get_all(
			"Site Activity",
			filters={"site": self.name, "action": "Update"},
			order_by="creation desc",
			limit=1,
			pluck="creation",
		)
		return result[0] if result else None

	@classmethod
	def get_sites_with_backup_time(cls) -> list[dict]:
		sites = jingrow.qb.PageType(cls.PAGETYPE)
		return (
			jingrow.qb.from_(sites)
			.select(sites.name, sites.backup_time)
			.where(sites.backup_time.isnotnull())
			.where(sites.status == "Active")
			.where(sites.skip_scheduled_backups == 0)
			.run(as_dict=True)
		)

	@classmethod
	def get_sites_for_backup(cls, interval: int):
		sites = cls.get_sites_without_backup_in_interval(interval)
		servers_with_backups = jingrow.get_all(
			"Server",
			{"status": "Active", "skip_scheduled_backups": False},
			pluck="name",
		)
		return jingrow.get_all(
			"Site",
			{
				"name": ("in", sites),
				"skip_scheduled_backups": False,
				"backup_time": ("is", "not set"),
				"server": ("in", servers_with_backups),
			},
			["name", "timezone", "server"],
			order_by="server",
			ignore_ifnull=True,
		)

	@classmethod
	def get_sites_without_backup_in_interval(cls, interval: int) -> list[str]:
		"""Return active sites that haven't had backup taken in interval hours."""
		interval_hrs_ago = jingrow.utils.add_to_date(None, hours=-interval)
		all_sites = set(
			jingrow.get_all(
				"Site",
				{
					"status": "Active",
					"creation": ("<=", interval_hrs_ago),
					"is_standby": False,
					"plan": ("not like", "%Trial"),
				},
				pluck="name",
			)
		)
		return list(
			all_sites
			- set(cls.get_sites_with_backup_in_interval(interval_hrs_ago))
			- set(cls.get_sites_with_pending_backups(interval_hrs_ago))
		)
		# TODO: query using creation time of account request for actual new sites <03-09-21, Balamurali M> #

	@classmethod
	def get_sites_with_pending_backups(cls, interval_hrs_ago: datetime) -> list[str]:
		return jingrow.get_all(
			"Site Backup",
			{
				"status": ("in", ["Running", "Pending"]),
				"creation": (">=", interval_hrs_ago),
			},
			pluck="site",
		)

	@classmethod
	def get_sites_with_backup_in_interval(cls, interval_hrs_ago) -> list[str]:
		return jingrow.get_all(
			"Site Backup",
			{
				"creation": (">", interval_hrs_ago),
				"status": ("!=", "Failure"),
				"owner": "Administrator",
			},
			pluck="site",
			ignore_ifnull=True,
		)

	@classmethod
	def exists(cls, subdomain, domain) -> bool:
		"""Check if subdomain is available"""
		banned_domains = jingrow.get_all("Blocked Domain", {"block_for_all": 1}, pluck="name")
		if banned_domains and subdomain in banned_domains:
			return True
		return bool(
			jingrow.db.exists("Blocked Domain", {"name": subdomain, "root_domain": domain})
			or jingrow.db.exists(
				"Site",
				{
					"subdomain": subdomain,
					"domain": domain,
				},
			)
		)

	@jingrow.whitelist()
	def run_after_migrate_steps(self):
		agent = Agent(self.server)
		agent.run_after_migrate_steps(self)

	@jingrow.whitelist()
	def get_actions(self):
		is_group_public = jingrow.get_cached_value("Release Group", self.group, "public")

		actions = [
			{
				"action": "激活站点",
				"description": "激活站点使其可以在互联网上被访问",
				"button_label": "激活",
				"condition": self.status in ["Inactive", "Broken"],
				"pg_method": "activate",
			},
			{
				"action": "管理数据库用户",
				"description": "管理您站点数据库的用户和权限",
				"button_label": "管理",
				"pg_method": "dummy",
				"condition": not self.hybrid_site,
			},
			{
				"action": "计划备份",
				"description": "为此站点计划一个备份",
				"button_label": "计划",
				"pg_method": "schedule_backup",
			},
			{
				"action": "转移站点",
				"description": "将此站点的所有权转移给另一个团队",
				"button_label": "转移",
				"pg_method": "send_change_team_request",
			},
			{
				"action": "版本升级",
				"description": "将您的站点升级到主要版本",
				"button_label": "升级",
				"pg_method": "upgrade",
				"condition": self.status == "Active",
			},
			{
				"action": "更改区域",
				"description": "将您的站点迁移到不同的区域",
				"button_label": "更改",
				"pg_method": "change_region",
				"condition": self.status == "Active",
			},
			{
				"action": "更改工作台组",
				"description": "将您的站点迁移到不同的工作台组",
				"button_label": "更改",
				"pg_method": "change_bench",
				"condition": self.status == "Active",
			},
			{
				"action": "更改服务器",
				"description": "将您的站点迁移到不同的服务器",
				"button_label": "更改",
				"pg_method": "change_server",
				"condition": self.status == "Active" and not is_group_public,
			},
			{
				"action": "清除缓存",
				"description": "清除站点上的缓存",
				"button_label": "清除",
				"pg_method": "clear_site_cache",
			},
			{
				"action": "停用站点",
				"description": "停用的站点在互联网上无法访问",
				"button_label": "停用",
				"condition": self.status == "Active",
				"pg_method": "deactivate",
			},
			{
				"action": "更新数据库",
				"description": "在您的站点上运行更新数据库命令",
				"button_label": "更新",
				"pg_method": "migrate",
				"group": "危险操作",
			},
			{
				"action": "使用文件恢复",
				"description": "使用数据库、公共和私有文件进行恢复",
				"button_label": "恢复",
				"pg_method": "restore_site_from_files",
				"group": "危险操作",
			},
			{
				"action": "从指定站点恢复",
				"description": "从指定站点的备份文件恢复数据库、公共和私有文件",
				"button_label": "恢复",
				"pg_method": "restore_site_from_files",
				"group": "危险操作",
			},
			{
				"action": "重置站点",
				"description": "将您的站点数据库重置为干净状态",
				"button_label": "重置",
				"pg_method": "reinstall",
				"group": "危险操作",
			},
			{
				"action": "删除站点",
				"description": "当您删除站点时，所有站点数据将永久删除",
				"button_label": "删除",
				"pg_method": "archive",
				"group": "危险操作",
			},
		]

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

	@property
	def hybrid_site(self) -> bool:
		return bool(jingrow.get_cached_value("Server", self.server, "is_self_hosted"))

	@property
	def pending_for_long(self) -> bool:
		if self.status != "Pending":
			return False
		return (jingrow.utils.now_datetime() - self.modified).total_seconds() > 60 * 60 * 4  # 4 hours

	@jingrow.whitelist()
	def fetch_bench_from_agent(self):
		agent = Agent(self.server)
		benches_with_this_site = []
		for bench in agent.get("server")["benches"].values():
			if self.name in bench["sites"]:
				benches_with_this_site.append(bench["name"])
		if len(benches_with_this_site) == 1:
			jingrow.db.set_value("Site", self.name, "bench", benches_with_this_site[0])

	@cached_property
	def is_on_dedicated_plan(self):
		return bool(jingrow.db.get_value("Site Plan", self.plan, "dedicated_server_plan"))

	@jingrow.whitelist()
	def forcefully_remove_site(self, bench):
		"""Bypass all agent/jcloud callbacks and just remove this site from the target bench/server"""
		from jcloud.utils import get_mariadb_root_password

		jingrow.only_for("System Manager")

		if bench == self.bench:
			jingrow.throw("Use <b>Archive Site</b> action to remove site from current bench")

		# Mimic archive_site method in the agent.py
		server = jingrow.db.get_value("Bench", bench, ["server"])
		data = {
			"mariadb_root_password": get_mariadb_root_password(self),
			"force": True,
		}

		response = {"server": server, "bench": bench}
		agent = Agent(server)
		result = agent.request("POST", f"benches/{bench}/sites/{self.name}/archive", data, raises=False)
		if "job" in result:
			job = result["job"]
			response["job"] = job
		else:
			response["error"] = result["error"]
		self.add_comment(
			text=f"{jingrow.session.user} attempted to forcefully remove site from {bench}.<br><pre>{json.dumps(response, indent=1)}</pre>"
		)
		return response

	@dashboard_whitelist()
	def fetch_database_table_schema(self, reload=False):
		"""
		Store dump in redis cache
		"""
		key_for_schema = f"database_table_schema__data:{self.name}"
		key_for_schema_status = (
			f"database_table_schema__status:{self.name}"  # 1 - loading, 2 - done, None - not available
		)

		if reload:
			jingrow.cache().delete_value(key_for_schema)
			jingrow.cache().delete_value(key_for_schema_status)

		status = jingrow.utils.cint(jingrow.cache().get_value(key_for_schema_status))
		if status:
			if status == 1:
				return {
					"loading": True,
					"data": [],
				}
			if status == 2:
				return {
					"loading": False,
					"data": json.loads(jingrow.cache().get_value(key_for_schema)),
				}

		# Check if any agent job is created within 5 minutes and in pending/running condition
		# Checks to prevent duplicate agent job creation due to race condition
		if not jingrow.db.exists(
			"Agent Job",
			{
				"job_type": "Fetch Database Table Schema",
				"site": self.name,
				"status": ["in", ["Undelivered", "Pending", "Running"]],
				"creation": (">", jingrow.utils.add_to_date(None, minutes=-5)),
			},
		):
			# create the agent job and put it in loading state
			jingrow.cache().set_value(key_for_schema_status, 1, expires_in_sec=600)
			Agent(self.server).fetch_database_table_schema(
				self, include_index_info=True, include_table_size=True
			)
		return {
			"loading": True,
			"data": [],
		}

	@dashboard_whitelist()
	def fetch_database_processes(self):
		agent = Agent(self.server)
		if agent.should_skip_requests():
			return None
		return agent.fetch_database_processes(self)

	@dashboard_whitelist()
	def kill_database_process(self, id):
		agent = Agent(self.server)
		if agent.should_skip_requests():
			return None
		processes = agent.fetch_database_processes(self)
		if not processes:
			return None
		isFoundPid = True
		for process in processes:
			if str(process["id"]) == str(id):
				isFoundPid = True
				break
		if not isFoundPid:
			return None
		return agent.kill_database_process(self, id)

	@dashboard_whitelist()
	def run_sql_query_in_database(self, query: str, commit: bool):
		if not query:
			return {"success": False, "output": "SQL Query cannot be empty"}
		pg = jingrow.get_pg(
			{
				"pagetype": "SQL Playground Log",
				"site": self.name,
				"team": self.team,
				"query": query,
				"committed": commit,
			}
		)
		response = Agent(self.server).run_sql_query_in_database(self, query, commit)
		pg.is_successful = response.get("success", False)
		pg.insert(ignore_permissions=True)
		return response

	@dashboard_whitelist()
	def suggest_database_indexes(self):
		from jcloud.jcloud.report.mariadb_slow_queries.mariadb_slow_queries import get_data as get_slow_queries

		existing_agent_job_name = jingrow.db.exists(
			"Agent Job",
			{
				"site": self.name,
				"status": ("not in", ("Failure", "Delivery Failure")),
				"job_type": "Analyze Slow Queries",
				"creation": (
					">",
					jingrow.utils.add_to_date(None, minutes=-30),
				),
				"retry_count": 0,
			},
		)

		if existing_agent_job_name:
			existing_agent_job = jingrow.get_pg("Agent Job", existing_agent_job_name)
			if existing_agent_job.status == "Success":
				return {
					"loading": False,
					"data": json.loads(existing_agent_job.data).get("result", []),
				}
			return {
				"loading": True,
				"data": [],
			}

		# fetch slow queries of last 7 days
		slow_queries = get_slow_queries(
			jingrow._dict(
				{
					"database": self.database_name,
					"start_datetime": jingrow.utils.add_to_date(None, days=-7),
					"stop_datetime": jingrow.utils.now_datetime(),
					"search_pattern": ".*",
					"max_lines": 1000,
					"normalize_queries": True,
				}
			)
		)
		slow_queries = [{"example": x["example"], "normalized": x["query"]} for x in slow_queries]
		if len(slow_queries) == 0:
			return {
				"loading": False,
				"data": [],
			}
		agent = Agent(self.server)
		agent.analyze_slow_queries(self, slow_queries)

		return {
			"loading": True,
			"data": [],
		}

	@dashboard_whitelist()
	def add_database_index(self, table, column):
		record = jingrow.db.exists(
			"Agent Job",
			{
				"site": self.name,
				"status": ["in", ["Undelivered", "Running", "Pending"]],
				"job_type": "Add Database Index",
			},
		)
		if record:
			return {
				"success": False,
				"message": "There is already a job running for adding database index. Please wait until finished.",
				"job_name": record,
			}
		pagetype = get_pagetype_name(table)
		agent = Agent(self.server)
		job = agent.add_database_index(self, pagetype=pagetype, columns=[column])
		return {
			"success": True,
			"message": "Database index will be added on site.",
			"job_name": job.name,
		}


def site_cleanup_after_archive(site):
	delete_site_domains(site)
	delete_site_subdomain(site)
	release_name(site)


def delete_site_subdomain(site):
	site_pg = jingrow.get_pg("Site", site)
	domain = jingrow.get_pg("Root Domain", site_pg.domain)
	is_standalone = jingrow.get_value("Server", site_pg.server, "is_standalone")
	if is_standalone:
		proxy_server = site_pg.server
	else:
		proxy_server = jingrow.get_value("Server", site_pg.server, "proxy_server")
	site_pg.remove_dns_record(domain, proxy_server, site)


def delete_site_domains(site):
	domains = jingrow.get_all("Site Domain", {"site": site})
	jingrow.db.set_value("Site", site, "host_name", None)
	for domain in domains:
		jingrow.delete_pg("Site Domain", domain.name)


def release_name(name):
	if ".archived" in name:
		return
	new_name = f"{name}.archived"
	new_name = append_number_if_name_exists("Site", new_name, separator=".")
	jingrow.rename_pg("Site", name, new_name)


def process_fetch_database_table_schema_job_update(job):
	key_for_schema = f"database_table_schema__data:{job.site}"
	key_for_schema_status = (
		f"database_table_schema__status:{job.site}"  # 1 - loading, 2 - done, None - not available
	)

	if job.status in ["Failure", "Delivery Failure"]:
		jingrow.cache().delete_value(key_for_schema)
		jingrow.cache().delete_value(key_for_schema_status)
		return

	if job.status == "Success":
		"""
		Support old agent versions
		Remove this once all agents are updated
		"""
		data = json.loads(job.data)
		is_old_agent = False

		if len(data) > 0 and isinstance(data[next(iter(data.keys()))], list):
			is_old_agent = True

		if is_old_agent:
			data_copy = data.copy()
			data = {}
			for key, value in data_copy.items():
				data[key] = {
					"columns": value,
					"size": {
						"data_length": 0,
						"index_length": 0,
						"total_size": 0,
					},  # old agent api doesn't have size info
				}
				for column in data[key]["columns"]:
					column["index_info"] = {
						"index_usage": {x: 0 for x in column["indexes"]},  # just fill some dummy value
						"indexes": column["indexes"],
						"is_indexed": len(column["indexes"]) > 0,
					}

		jingrow.cache().set_value(key_for_schema, json.dumps(data), expires_in_sec=6000)
		jingrow.cache().set_value(key_for_schema_status, 2, expires_in_sec=6000)


def process_new_site_job_update(job):  # noqa: C901
	site_status = jingrow.get_value("Site", job.site, "status", for_update=True)

	other_job_types = {
		"Add Site to Upstream": ("New Site", "New Site from Backup"),
		"New Site": ("Add Site to Upstream",),
		"New Site from Backup": ("Add Site to Upstream",),
	}[job.job_type]

	first = job.status
	second = jingrow.get_value(
		"Agent Job",
		{"job_type": ("in", other_job_types), "site": job.site},
		"status",
		for_update=True,
	)

	backup_tests = jingrow.get_all(
		"Backup Restoration Test",
		dict(test_site=job.site, status="Running"),
		pluck="name",
	)

	if "Success" == first == second:
		updated_status = "Active"
		marketplace_app_hook(site=Site("Site", job.site), op="install")
	elif "Failure" in (first, second) or "Delivery Failure" in (first, second):
		updated_status = "Broken"
	elif "Running" in (first, second):
		updated_status = "Installing"
	else:
		updated_status = "Pending"

	status_map = {
		"Active": "Success",
		"Broken": "Failure",
		"Installing": "Running",
		"Pending": "Running",
	}

	if updated_status != site_status:
		if backup_tests:
			jingrow.db.set_value(
				"Backup Restoration Test",
				backup_tests[0],
				"status",
				status_map[updated_status],
			)
			jingrow.db.commit()

		jingrow.db.set_value("Site", job.site, "status", updated_status)
		create_site_status_update_webhook_event(job.site)

	if job.status == "Success":
		request_data = json.loads(job.request_data)
		if "create_user" in request_data:
			jingrow.db.set_value("Site", job.site, "additional_system_user_created", True)
			jingrow.db.commit()

	# Update in product trial request
	if job.job_type in ("New Site", "Add Site to Upstream") and updated_status in (
		"Active",
		"Broken",
	):
		update_product_trial_request_status_based_on_site_status(job.site, updated_status == "Active")

	# check if new bench related to a site group deploy
	site_group_deploy = jingrow.db.get_value(
		"Site Group Deploy",
		{
			"site": job.site,
			"status": "Creating Site",
		},
	)
	if site_group_deploy:
		jingrow.get_pg("Site Group Deploy", site_group_deploy).update_site_group_deploy_on_process_job(job)


def update_product_trial_request_status_based_on_site_status(site, is_site_active):
	records = jingrow.get_list("Product Trial Request", filters={"site": site}, fields=["name"])
	if not records:
		return
	product_trial_request = jingrow.get_pg("Product Trial Request", records[0].name, for_update=True)
	if is_site_active:
		mode = jingrow.get_value(
			"Product Trial", product_trial_request.product_trial, "setup_wizard_completion_mode"
		)
		if mode != "auto":
			product_trial_request.status = "Site Created"
			product_trial_request.site_creation_completed_on = now_datetime()
			product_trial_request.save(ignore_permissions=True)
		else:
			product_trial_request.complete_setup_wizard()
	else:
		product_trial_request.status = "Error"
		product_trial_request.save(ignore_permissions=True)


def process_complete_setup_wizard_job_update(job):
	records = jingrow.get_list("Product Trial Request", filters={"site": job.site}, fields=["name"])
	if not records:
		return
	product_trial_request = jingrow.get_pg("Product Trial Request", records[0].name, for_update=True)
	if job.status == "Success":
		jingrow.db.set_value("Site", job.site, "additional_system_user_created", True)
		if jingrow.get_all("Site Domain", filters={"site": job.site, "status": ["!=", "Active"]}):
			product_trial_request.status = "Adding Domain"
		else:
			product_trial_request.status = "Site Created"
			product_trial_request.site_creation_completed_on = now_datetime()
		product_trial_request.save(ignore_permissions=True)
	elif job.status in ("Failure", "Delivery Failure"):
		product_trial_request.status = "Error"
		product_trial_request.save(ignore_permissions=True)


def process_add_domain_job_update(job):
	records = jingrow.get_list("Product Trial Request", filters={"site": job.site}, fields=["name"])
	if not records:
		return

	product_trial_request = jingrow.get_pg("Product Trial Request", records[0].name, for_update=True)
	if job.status == "Success":
		if jingrow.get_all(
			"Agent Job",
			filters={"site": job.site, "job_type": "Complete Setup Wizard", "status": ["!=", "Success"]},
		):
			product_trial_request.status = "Completing Setup Wizard"
		else:
			product_trial_request.status = "Site Created"
			product_trial_request.site_creation_completed_on = now_datetime()

		product_trial_request.save(ignore_permissions=True)

		site_domain = json.loads(job.request_data).get("domain")
		site = jingrow.get_pg("Site", job.site)
		auto_generated_domain = site.host_name
		site.host_name = site_domain
		site.save()
		site.set_redirect(auto_generated_domain)

	elif job.status in ("Failure", "Delivery Failure"):
		product_trial_request.status = "Error"
		product_trial_request.save(ignore_permissions=True)


def get_remove_step_status(job):
	remove_step_name = {
		"Archive Site": "Archive Site",
		"Remove Site from Upstream": "Remove Site File from Upstream Directory",
	}[job.job_type]

	return jingrow.db.get_value(
		"Agent Job Step",
		{"step_name": remove_step_name, "agent_job": job.name},
		"status",
		for_update=True,
	)


def process_archive_site_job_update(job):
	site_status = jingrow.get_value("Site", job.site, "status", for_update=True)

	other_job_type = {
		"Remove Site from Upstream": "Archive Site",
		"Archive Site": "Remove Site from Upstream",
	}[job.job_type]

	try:
		other_job = jingrow.get_last_pg(
			"Agent Job",
			filters={"job_type": other_job_type, "site": job.site},
			for_update=True,
		)
	except jingrow.DoesNotExistError:
		# Site is already renamed, the other job beat us to it
		# Our work is done
		return

	first = get_remove_step_status(job)
	second = get_remove_step_status(other_job)

	if (
		("Success" == first == second)
		or ("Skipped" == first == second)
		or sorted(("Success", "Skipped")) == sorted((first, second))
	):
		updated_status = "Archived"
	elif "Failure" in (first, second):
		updated_status = "Broken"
	elif "Delivery Failure" == first == second:
		updated_status = "Active"
	elif "Delivery Failure" in (first, second):
		updated_status = "Broken"
	else:
		updated_status = "Pending"

	if updated_status != site_status:
		jingrow.db.set_value(
			"Site",
			job.site,
			{"status": updated_status, "archive_failed": updated_status != "Archived"},
		)
		if updated_status == "Archived":
			site_cleanup_after_archive(job.site)


def process_install_app_site_job_update(job):
	updated_status = {
		"Pending": "Pending",
		"Running": "Installing",
		"Success": "Active",
		"Failure": "Active",
		"Delivery Failure": "Active",
	}[job.status]

	site_status = jingrow.get_value("Site", job.site, "status")
	if updated_status != site_status:
		if job.status == "Success":
			site = jingrow.get_pg("Site", job.site)
			app = json.loads(job.request_data).get("name")
			app_pg = find(site.apps, lambda x: x.app == app)
			if not app_pg:
				site.append("apps", {"app": app})
				site.save()
		jingrow.db.set_value("Site", job.site, "status", updated_status)
		create_site_status_update_webhook_event(job.site)


def process_uninstall_app_site_job_update(job):
	updated_status = {
		"Pending": "Pending",
		"Running": "Installing",
		"Success": "Active",
		"Failure": "Active",
		"Delivery Failure": "Active",
	}[job.status]

	site_status = jingrow.get_value("Site", job.site, "status")
	if updated_status != site_status:
		if job.status == "Success":
			site = jingrow.get_pg("Site", job.site)
			app = job.request_path.rsplit("/", 1)[-1]
			app_pg = find(site.apps, lambda x: x.app == app)
			if app_pg:
				site.remove(app_pg)
				site.save()
		jingrow.db.set_value("Site", job.site, "status", updated_status)
		create_site_status_update_webhook_event(job.site)


def process_marketplace_hooks_for_backup_restore(apps_from_backup: set[str], site: Site):
	site_apps = set([app.app for app in site.apps])
	apps_to_install = apps_from_backup - site_apps
	apps_to_uninstall = site_apps - apps_from_backup
	for app in apps_to_install:
		if (
			jingrow.get_cached_value("Marketplace App", app, "subscription_type") == "Free"
		):  # like china_compliance; no need to check subscription
			marketplace_app_hook(app=app, site=site, op="install")
	for app in apps_to_uninstall:
		if (
			jingrow.get_cached_value("Marketplace App", app, "subscription_type") == "Free"
		):  # like china_compliance; no need to check subscription
			marketplace_app_hook(app=app, site=site, op="uninstall")


def process_restore_job_update(job, force=False):
	"""
	force: force updates apps table sync
	"""
	updated_status = {
		"Pending": "Pending",
		"Running": "Installing",
		"Success": "Active",
		"Failure": "Broken",
		"Delivery Failure": "Active",
	}[job.status]

	site_status = jingrow.get_value("Site", job.site, "status")
	if force or updated_status != site_status:
		if job.status == "Success":
			apps_from_backup: list[str] = [line.split()[0] for line in job.output.splitlines() if line]
			site = Site("Site", job.site)
			process_marketplace_hooks_for_backup_restore(set(apps_from_backup), site)
			site.set_apps(apps_from_backup)
		jingrow.db.set_value("Site", job.site, "status", updated_status)
		create_site_status_update_webhook_event(job.site)


def process_reinstall_site_job_update(job):
	updated_status = {
		"Pending": "Pending",
		"Running": "Installing",
		"Success": "Active",
		"Failure": "Broken",
		"Delivery Failure": "Active",
	}[job.status]

	site_status = jingrow.get_value("Site", job.site, "status")
	if updated_status != site_status:
		jingrow.db.set_value("Site", job.site, "status", updated_status)
		create_site_status_update_webhook_event(job.site)
	if job.status == "Success":
		jingrow.db.set_value("Site", job.site, "setup_wizard_complete", 0)


def process_migrate_site_job_update(job):
	updated_status = {
		"Pending": "Pending",
		"Running": "Updating",
		"Success": "Active",
		"Failure": "Broken",
		"Delivery Failure": "Active",
	}[job.status]

	if updated_status == "Active":
		site: Site = jingrow.get_pg("Site", job.site)
		if site.status_before_update:
			site.reset_previous_status(fix_broken=True)
			return
	site_status = jingrow.get_value("Site", job.site, "status")
	if updated_status != site_status:
		jingrow.db.set_value("Site", job.site, "status", updated_status)
		create_site_status_update_webhook_event(job.site)


def get_rename_step_status(job):
	rename_step_name = {
		"Rename Site": "Rename Site",
		"Rename Site on Upstream": "Rename Site File in Upstream Directory",
	}[job.job_type]

	return jingrow.db.get_value(
		"Agent Job Step",
		{"step_name": rename_step_name, "agent_job": job.name},
		"status",
		for_update=True,
	)


def process_rename_site_job_update(job):  # noqa: C901
	site_status = jingrow.get_value("Site", job.site, "status", for_update=True)

	other_job_type = {
		"Rename Site": "Rename Site on Upstream",
		"Rename Site on Upstream": "Rename Site",
	}[job.job_type]

	if job.job_type == "Rename Site" and job.status == "Success":
		request_data = json.loads(job.request_data)
		if "create_user" in request_data:
			jingrow.db.set_value("Site", job.site, "additional_system_user_created", True)

	try:
		other_job = jingrow.get_last_pg(
			"Agent Job",
			filters={"job_type": other_job_type, "site": job.site},
			for_update=True,
		)
	except jingrow.DoesNotExistError:
		# Site is already renamed, he other job beat us to it
		# Our work is done
		return

	first = get_rename_step_status(job)
	second = get_rename_step_status(other_job)

	if "Success" == first == second:
		update_records_for_rename(job)
		# update job obj with new name
		job.reload()
		updated_status = "Active"
		from jcloud.jcloud.pagetype.site.pool import create as create_pooled_sites

		create_pooled_sites()

	elif "Failure" in (first, second):
		updated_status = "Broken"
	elif "Delivery Failure" == first == second:
		updated_status = "Active"
	elif "Delivery Failure" in (first, second):
		updated_status = "Broken"
	elif "Running" in (first, second):
		updated_status = "Updating"
	else:
		updated_status = "Pending"

	if updated_status != site_status:
		jingrow.db.set_value("Site", job.site, "status", updated_status)
		create_site_status_update_webhook_event(job.site)


def process_move_site_to_bench_job_update(job):
	updated_status = {
		"Pending": "Pending",
		"Running": "Updating",
		"Failure": "Broken",
	}.get(job.status)
	if job.status in ("Success", "Failure"):
		dest_bench = json.loads(job.request_data).get("target")
		dest_group = jingrow.db.get_value("Bench", dest_bench, "group")

		move_site_step_status = jingrow.db.get_value(
			"Agent Job Step",
			{"step_name": "Move Site", "agent_job": job.name},
			"status",
		)
		if move_site_step_status == "Success":
			jingrow.db.set_value("Site", job.site, "bench", dest_bench)
			jingrow.db.set_value("Site", job.site, "group", dest_group)
	if updated_status:
		jingrow.db.set_value("Site", job.site, "status", updated_status)
		create_site_status_update_webhook_event(job.site)
		return
	if job.status == "Success":
		site = jingrow.get_pg("Site", job.site)
		site.reset_previous_status(fix_broken=True)


def update_records_for_rename(job):
	"""Update jcloud records for successful site rename."""
	data = json.loads(job.request_data)
	new_name = data["new_name"]
	if new_name == job.site:  # idempotency
		return

	site = jingrow.get_pg("Site", job.site, for_update=True)
	if site.host_name == job.site:
		# Host name already updated in f server, no need to create another job
		site._update_configuration({"host_name": f"https://{new_name}"})
		site.db_set("host_name", new_name)

	jingrow.rename_pg("Site", job.site, new_name)
	jingrow.rename_pg("Site Domain", job.site, new_name)


def process_restore_tables_job_update(job):
	updated_status = {
		"Pending": "Pending",
		"Running": "Updating",
		"Success": "Active",
		"Failure": "Broken",
	}[job.status]

	site_status = jingrow.get_value("Site", job.site, "status")
	if updated_status != site_status:
		if updated_status == "Active":
			jingrow.get_pg("Site", job.site).reset_previous_status(fix_broken=True)
		else:
			jingrow.db.set_value("Site", job.site, "status", updated_status)
			create_site_status_update_webhook_event(job.site)


def process_create_user_job_update(job):
	if job.status == "Success":
		jingrow.db.set_value("Site", job.site, "additional_system_user_created", True)
		update_product_trial_request_status_based_on_site_status(job.site, True)
	elif job.status in ("Failure", "Delivery Failure"):
		update_product_trial_request_status_based_on_site_status(job.site, False)


get_permission_query_conditions = get_permission_query_conditions_for_pagetype("Site")


def prepare_site(site: str, subdomain: str | None = None) -> dict:
	# prepare site details
	pg = jingrow.get_pg("Site", site)
	site_name = subdomain if subdomain else "brt-" + pg.subdomain
	app_plans = [app.app for app in pg.apps]
	backups = jingrow.get_all(
		"Site Backup",
		dict(status="Success", site=site, files_availability="Available", offsite=1),
		pluck="name",
	)
	if not backups:
		jingrow.throw("Backup Files not found.")
	backup = jingrow.get_pg("Site Backup", backups[0])

	files = {
		"config": backup.remote_config_file,
		"database": backup.remote_database_file,
		"public": backup.remote_public_file,
		"private": backup.remote_private_file,
	}
	return {
		"domain": jingrow.db.get_single_value("Jcloud Settings", "domain"),
		"plan": pg.plan,
		"name": site_name,
		"group": pg.group,
		"selected_app_plans": {},
		"apps": app_plans,
		"files": files,
	}


@jingrow.whitelist()
def options_for_new(group: str | None = None, selected_values=None) -> dict:
	domain = jingrow.db.get_single_value("Jcloud Settings", "domain")
	selected_values = jingrow.parse_json(selected_values) if selected_values else jingrow._dict()

	versions = []
	bench = None
	apps = []
	clusters = []

	versions_filters = {"public": True}
	if not group:
		versions_filters.update({"status": ("!=", "End of Life")})

	versions = jingrow.db.get_all(
		"Jingrow Version",
		["name", "default", "status", "number"],
		versions_filters,
		order_by="number desc",
	)
	for v in versions:
		v.label = v.name
		v.value = v.name

	if selected_values.version:
		bench = _get_bench_for_new(selected_values.version)
		apps = _get_apps_of_bench(selected_values.version, bench) if bench else []
		cluster_names = unique(
			jingrow.db.get_all(
				"Bench",
				filters={"candidate": jingrow.db.get_value("Bench", bench, "candidate")},
				pluck="cluster",
			)
		)
		clusters = jingrow.db.get_all(
			"Cluster",
			filters={"name": ("in", cluster_names), "public": True},
			fields=["name", "title", "image", "beta"],
		)
		for cluster in clusters:
			cluster.label = cluster.title
			cluster.value = cluster.name

	return {
		"domain": domain,
		"bench": bench,
		"versions": versions,
		"apps": apps,
		"clusters": clusters,
	}


def _get_bench_for_new(version):
	restricted_release_group_names = jingrow.db.get_all(
		"Site Plan Release Group",
		pluck="release_group",
		filters={"parenttype": "Site Plan", "parentfield": "release_groups"},
	)
	release_group = jingrow.db.get_value(
		"Release Group",
		fieldname=["name", "`default`", "title"],
		filters={
			"enabled": 1,
			"public": 1,
			"version": version,
			"name": ("not in", restricted_release_group_names),
		},
		order_by="creation desc",
		as_dict=1,
	)
	if not release_group:
		return None

	return jingrow.db.get_value(
		"Bench",
		filters={"status": "Active", "group": release_group.name},
		order_by="creation desc",
	)


def _get_apps_of_bench(version, bench):
	team = jingrow.local.team().name
	bench_apps = jingrow.db.get_all("Bench App", {"parent": bench}, pluck="source")
	app_sources = jingrow.get_all(
		"App Source",
		[
			"name",
			"app",
			"repository_url",
			"repository",
			"repository_owner",
			"branch",
			"team",
			"public",
			"app_title",
			"jingrow",
		],
		filters={"name": ("in", bench_apps), "jingrow": 0},
		or_filters={"public": True, "team": team},
	)
	for app in app_sources:
		app.label = app.app_title
		app.value = app.app
	apps = sorted(app_sources, key=lambda x: bench_apps.index(x.name))
	marketplace_apps = jingrow.db.get_all(
		"Marketplace App",
		fields=["title", "image", "description", "app", "route"],
		filters={"app": ("in", [app.app for app in apps])},
	)
	for app in apps:
		marketplace_details = find(marketplace_apps, lambda x: x.app == app.app)
		if marketplace_details:
			app.update(marketplace_details)
			app.plans = get_plans_for_app(app.app, version)
	return apps


def sync_sites_setup_wizard_complete_status():
	team_name = jingrow.get_value("Team", {"user": "Administrator"}, "name")
	sites = jingrow.get_all(
		"Site",
		filters={
			"status": "Active",
			"setup_wizard_complete": 0,
			"setup_wizard_status_check_retries": ("<", 18),
			"setup_wizard_status_check_next_retry_on": ("<=", jingrow.utils.now()),
			"team": ("!=", team_name),
		},
		pluck="name",
		order_by="RAND()",
		limit=100,
	)
	for site in sites:
		jingrow.enqueue(
			"jcloud.jcloud.pagetype.site.site.fetch_setup_wizard_complete_status_if_site_exists",
			site=site,
			queue="sync",
			job_id=f"fetch_setup_wizard_complete_status:{site}",
			deduplicate=True,
		)


def fetch_setup_wizard_complete_status_if_site_exists(site):
	if not jingrow.db.exists("Site", site):
		return
	with suppress(jingrow.DoesNotExistError):
		jingrow.get_pg("Site", site).fetch_setup_wizard_complete_status()


def create_site_status_update_webhook_event(site: str):
	record = jingrow.get_pg("Site", site)
	if record.team == "Administrator":
		return
	create_webhook_event("Site Status Update", record, record.team)

@jingrow.whitelist()
def send_renew_notification():
    """
    发送站点续费通知给用户:
    - 到期前60天
    - 到期前30天
    - 到期前7天
    - 到期当天
    通知方式包括邮件和站内信
    """
    today = jingrow.utils.today()
    
    # 计算关键日期
    sixty_days_later = jingrow.utils.add_days(today, 60)   # 60天后
    thirty_days_later = jingrow.utils.add_days(today, 30)  # 30天后
    seven_days_later = jingrow.utils.add_days(today, 7)    # 7天后
    
    # 查找今天需要发送通知的站点
    sites = jingrow.get_all(
        "Site",
        filters={
            "status": ["in", ["Active", "Suspended"]],
            "site_end_date": ["in", [sixty_days_later, thirty_days_later, seven_days_later, today]]
        },
        fields=["name", "host_name", "team", "site_end_date", "notify_email"]
    )
    
    for site in sites:
        # 获取团队信息（不包括phone字段，因为Team页面类型中没有这个字段）
        team_info = jingrow.db.get_value("Team", site.team, ["user", "notify_email", "billing_email"], as_dict=True)
        if not team_info:
            continue
            
        site_name = site.host_name or site.name
        
        # 优先使用站点的notify_email，如果没有则使用团队的notify_email，再没有则使用团队的billing_email
        email_recipient = site.notify_email or team_info.notify_email or team_info.billing_email
        if not email_recipient:
            email_recipient = jingrow.db.get_value("User", team_info.user, "email")
        
        # 从User文档中获取手机号码用于发送短信
        phone_number = jingrow.db.get_value("User", team_info.user, "mobile_no")
        
        if not email_recipient and not phone_number:
            continue
        
        # 初始化变量
        subject = ""
        message = ""
        days_remaining = 0
        
        # 根据到期日确定通知类型
        if str(site.site_end_date) == str(sixty_days_later):
            subject = f"续费通知 {site_name}"
            message = f"您的站点 {site_name} 将在60天后【{site.site_end_date}】到期，请及时续费以确保服务不中断。"
            days_remaining = 60
        elif str(site.site_end_date) == str(thirty_days_later):
            subject = f"续费通知 {site_name}"
            message = f"您的站点 {site_name} 将在30天后【{site.site_end_date}】到期，请及时续费以确保服务不中断。"
            days_remaining = 30
        elif str(site.site_end_date) == str(seven_days_later):
            subject = f"续费通知 {site_name}"
            message = f"您的站点 {site_name} 将在7天后【{site.site_end_date}】到期，请尽快续费以确保服务不中断。"
            days_remaining = 7
        elif str(site.site_end_date) == str(today):
            subject = f"续费通知 {site_name}"
            message = f"您的站点 {site_name} 今天({site.site_end_date})到期，如需继续使用，请尽快完成续费。"
            days_remaining = 0
        
        if not subject:
            continue
            
        try:
            # 发送邮件通知
            if email_recipient:
                jingrow.sendmail(
                    recipients=email_recipient,
                    subject=subject,
                    message=message,
                    reference_pagetype="Site",
                    reference_name=site.name
                )
            
            # 发送短信通知
            if phone_number:
                try:
                    # 格式化日期为字符串
                    formatted_end_date = str(site.site_end_date)
                    
                    # 发送短信
                    send_renew_sms(
                        phone_number,
                        days_remaining,
                        formatted_end_date
                    )
                    
                except Exception as sms_error:
                    jingrow.log_error(f"站点 {site.name} 发送续费短信通知失败: {str(sms_error)}", "Renewal SMS Notification Error")
            
            # 直接创建站内通知，不检查重复
            notification = jingrow.get_pg({
                "pagetype": "Jcloud Notification",
                "team": site.team,
                "title": subject,
                "type": "Site Update",
                "document_type": "Site",
                "document_name": site.name,
                "message": message,
                "reference_pagetype": "Site",
                "reference_name": site.name,
                "read": 0,
                "is_actionable": True
            }).insert()
            
            # 触发实时事件
            jingrow.publish_realtime(
                "jcloud_notification", 
                pagetype="Jcloud Notification", 
                message={"team": site.team}
            )
        except Exception as e:
            jingrow.log_error(f"站点 {site.name} 发送续费通知失败: {str(e)}", "Renewal Notification Error")

@jingrow.whitelist()
def deactivate_expired_sites():
    """
    自动将已到期的站点（site_end_date<=今天，且状态为Active或Suspended）设为Inactive。
    每天定时任务调用。
    """
    today = jingrow.utils.today()
    sites = jingrow.get_all(
        "Site",
        filters={
            "status": ["in", ["Active", "Suspended"]],
            "site_end_date": ["<", today]
        },
        fields=["name", "status"]
    )
    for site in sites:
        try:
            site_pg = jingrow.get_pg("Site", site.name)
            site_pg.deactivate()
        except Exception as e:
            jingrow.log_error("Auto Inactivate Site Error", f"站点 {site.name} 到期自动停用失败: {str(e)}")
