# Copyright (c) 2020, Frappe and contributors
# For license information, please see license.txt
from __future__ import annotations

import _io  # type: ignore
import json
import os
import re
from contextlib import suppress
from datetime import date
from typing import TYPE_CHECKING, Any, Literal

import jingrow
import jingrow.utils
import requests
from frappe.utils.password import get_decrypted_password
from requests.exceptions import HTTPError

from press.utils import (
	get_mariadb_root_password,
	log_error,
	sanitize_config,
	servers_using_alternative_port_for_communication,
)

if TYPE_CHECKING:
	from io import BufferedReader

	from press.press.doctype.agent_job.agent_job import AgentJob
	from press.press.doctype.app_patch.app_patch import AgentPatchConfig, AppPatch
	from press.press.doctype.bench.bench import Bench
	from press.press.doctype.database_server.database_server import DatabaseServer
	from press.press.doctype.physical_backup_restoration.physical_backup_restoration import (
		PhysicalBackupRestoration,
	)
	from press.press.doctype.site.site import Site
	from press.press.doctype.site_backup.site_backup import SiteBackup


APPS_LIST_REGEX = re.compile(r"\[.*\]")


class Agent:
	if TYPE_CHECKING:
		from typing import Optional

		from requests import Response

		response: Response | None

	def __init__(self, server, server_type="Server"):
		self.server_type = server_type
		self.server = server
		self.port = 443 if self.server not in servers_using_alternative_port_for_communication() else 8443

	def new_bench(self, bench: "Bench"):
		settings = frappe.db.get_value(
			"Press Settings",
			None,
			["docker_registry_url", "docker_registry_username", "docker_registry_password"],
			as_dict=True,
		)
		cluster = frappe.db.get_value(self.server_type, self.server, "cluster")
		registry_url = frappe.db.get_value("Cluster", cluster, "repository") or settings.docker_registry_url

		data = {
			"name": bench.name,
			"bench_config": json.loads(bench.bench_config),
			"common_site_config": json.loads(bench.config),
			"registry": {
				"url": registry_url,
				"username": settings.docker_registry_username,
				"password": settings.docker_registry_password,
			},
		}

		if bench.mounts:
			data["mounts"] = [
				{
					"source": m.source,
					"destination": m.destination,
					"is_absolute_path": m.is_absolute_path,
				}
				for m in bench.mounts
			]

		return self.create_agent_job("New Bench", "benches", data, bench=bench.name)

	def archive_bench(self, bench):
		return self.create_agent_job("Archive Bench", f"benches/{bench.name}/archive", bench=bench.name)

	def restart_bench(self, bench, web_only=False):
		return self.create_agent_job(
			"Bench Restart",
			f"benches/{bench.name}/restart",
			data={"web_only": web_only},
			bench=bench.name,
		)

	def rebuild_bench(self, bench):
		return self.create_agent_job(
			"Rebuild Bench Assets",
			f"benches/{bench.name}/rebuild",
			bench=bench.name,
		)

	def update_bench_config(self, bench):
		data = {
			"bench_config": json.loads(bench.bench_config),
			"common_site_config": json.loads(bench.config),
		}
		return self.create_agent_job(
			"Update Bench Configuration", f"benches/{bench.name}/config", data, bench=bench.name
		)

	def _get_managed_db_config(self, site):
		managed_database_service = frappe.get_cached_value("Bench", site.bench, "managed_database_service")

		if not managed_database_service:
			return {}

		return frappe.get_cached_value(
			"Managed Database Service",
			managed_database_service,
			["database_host", "database_root_user", "port"],
			as_dict=True,
		)

	def new_site(self, site, create_user: dict | None = None):
		apps = [app.app for app in site.apps]

		data = {
			"config": json.loads(site.config),
			"apps": apps,
			"name": site.name,
			"mariadb_root_password": get_mariadb_root_password(site),
			"admin_password": site.get_password("admin_password"),
			"managed_database_config": self._get_managed_db_config(site),
		}

		if create_user:
			data["create_user"] = create_user

		return self.create_agent_job(
			"New Site", f"benches/{site.bench}/sites", data, bench=site.bench, site=site.name
		)

	def reinstall_site(self, site):
		data = {
			"mariadb_root_password": get_mariadb_root_password(site),
			"admin_password": site.get_password("admin_password"),
			"managed_database_config": self._get_managed_db_config(site),
		}

		return self.create_agent_job(
			"Reinstall Site",
			f"benches/{site.bench}/sites/{site.name}/reinstall",
			data,
			bench=site.bench,
			site=site.name,
		)

	def restore_site(self, site: "Site", skip_failing_patches=False):
		site.check_space_on_server_for_restore()
		apps = [app.app for app in site.apps]
		public_link, private_link, database_link = None, None, None
		if site.remote_database_file:
			database_link = frappe.get_pg("Remote File", site.remote_database_file).download_link
		if site.remote_public_file:
			public_link = frappe.get_pg("Remote File", site.remote_public_file).download_link
		if site.remote_private_file:
			private_link = frappe.get_pg("Remote File", site.remote_private_file).download_link

		data = {
			"apps": apps,
			"mariadb_root_password": get_mariadb_root_password(site),
			"admin_password": site.get_password("admin_password"),
			"database": database_link,
			"public": public_link,
			"private": private_link,
			"skip_failing_patches": skip_failing_patches,
			"managed_database_config": self._get_managed_db_config(site),
		}

		return self.create_agent_job(
			"Restore Site",
			f"benches/{site.bench}/sites/{site.name}/restore",
			data,
			bench=site.bench,
			site=site.name,
		)

	def rename_site(self, site, new_name: str, create_user: dict | None = None, config: dict | None = None):
		data: dict[str, Any] = {"new_name": new_name}
		if create_user:
			data["create_user"] = create_user
		if config:
			data["config"] = config
		return self.create_agent_job(
			"Rename Site",
			f"benches/{site.bench}/sites/{site.name}/rename",
			data,
			bench=site.bench,
			site=site.name,
		)

	def create_user(self, site, email, first_name, last_name, password=None):
		data = {
			"email": email,
			"first_name": first_name,
			"last_name": last_name,
			"password": password,
		}
		return self.create_agent_job(
			"Create User",
			f"benches/{site.bench}/sites/{site.name}/create-user",
			data,
			bench=site.bench,
			site=site.name,
		)

	def complete_setup_wizard(self, site, data):
		return self.create_agent_job(
			"Complete Setup Wizard",
			f"benches/{site.bench}/sites/{site.name}/complete-setup-wizard",
			data,
			bench=site.bench,
			site=site.name,
		)

	def optimize_tables(self, site, tables):
		return self.create_agent_job(
			"Optimize Tables",
			f"benches/{site.bench}/sites/{site.name}/optimize",
			data={"tables": tables},
			bench=site.bench,
			site=site.name,
		)

	def rename_upstream_site(self, server: str, site, new_name: str, domains: list[str]):
		_server = frappe.get_pg("Server", server)
		ip = _server.ip if _server.is_self_hosted else _server.private_ip
		data = {"new_name": new_name, "domains": domains}
		return self.create_agent_job(
			"Rename Site on Upstream",
			f"proxy/upstreams/{ip}/sites/{site.name}/rename",
			data,
			site=site.name,
		)

	def new_site_from_backup(self, site: "Site", skip_failing_patches=False):
		site.check_space_on_server_for_restore()
		apps = [app.app for app in site.apps]

		def sanitized_site_config(site):
			sanitized_config = {}
			if site.remote_config_file:
				from press.press.doctype.site_activity.site_activity import log_site_activity

				site_config = frappe.get_pg("Remote File", site.remote_config_file)
				new_config = site_config.get_content()
				new_config["maintenance_mode"] = 0  # Don't allow deactivated sites to be created
				sanitized_config = sanitize_config(new_config)
				existing_config = json.loads(site.config)
				existing_config.update(sanitized_config)
				site._update_configuration(existing_config)
				log_site_activity(site.name, "Update Configuration")

			return json.dumps(sanitized_config)

		public_link, private_link = None, None

		if site.remote_public_file:
			public_link = frappe.get_pg("Remote File", site.remote_public_file).download_link
		if site.remote_private_file:
			private_link = frappe.get_pg("Remote File", site.remote_private_file).download_link

		assert site.config is not None, "Site config is required to restore site from backup"

		data = {
			"config": json.loads(site.config),
			"apps": apps,
			"name": site.name,
			"mariadb_root_password": get_mariadb_root_password(site),
			"admin_password": site.get_password("admin_password"),
			"site_config": sanitized_site_config(site),
			"database": frappe.get_pg("Remote File", site.remote_database_file).download_link,
			"public": public_link,
			"private": private_link,
			"skip_failing_patches": skip_failing_patches,
			"managed_database_config": self._get_managed_db_config(site),
		}

		return self.create_agent_job(
			"New Site from Backup",
			f"benches/{site.bench}/sites/restore",
			data,
			bench=site.bench,
			site=site.name,
		)

	def install_app_site(self, site, app):
		data = {"name": app}
		return self.create_agent_job(
			"Install App on Site",
			f"benches/{site.bench}/sites/{site.name}/apps",
			data,
			bench=site.bench,
			site=site.name,
		)

	def uninstall_app_site(self, site, app):
		return self.create_agent_job(
			"Uninstall App from Site",
			f"benches/{site.bench}/sites/{site.name}/apps/{app}",
			method="DELETE",
			bench=site.bench,
			site=site.name,
		)

	def setup_erpnext(self, site, user, config):
		data = {"user": user, "config": config}
		return self.create_agent_job(
			"Setup ERPNext",
			f"benches/{site.bench}/sites/{site.name}/erpnext",
			data,
			bench=site.bench,
			site=site.name,
		)

	def migrate_site(self, site, skip_failing_patches=False, activate=True):
		data = {"skip_failing_patches": skip_failing_patches, "activate": activate}
		return self.create_agent_job(
			"Migrate Site",
			f"benches/{site.bench}/sites/{site.name}/migrate",
			bench=site.bench,
			site=site.name,
			data=data,
		)

	def clear_site_cache(self, site):
		return self.create_agent_job(
			"Clear Cache",
			f"benches/{site.bench}/sites/{site.name}/cache",
			method="DELETE",
			bench=site.bench,
			site=site.name,
		)

	def activate_site(self, site, reference_doctype=None, reference_name=None):
		return self.create_agent_job(
			"Activate Site",
			f"benches/{site.bench}/sites/{site.name}/activate",
			bench=site.bench,
			site=site.name,
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def deactivate_site(self, site, reference_doctype=None, reference_name=None):
		return self.create_agent_job(
			"Deactivate Site",
			f"benches/{site.bench}/sites/{site.name}/deactivate",
			bench=site.bench,
			site=site.name,
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def update_site(
		self,
		site,
		target,
		deploy_type,
		skip_failing_patches=False,
		skip_backups=False,
		before_migrate_scripts=None,
		skip_search_index=True,
	):
		activate = site.status_before_update in ("Active", "Broken")
		data = {
			"target": target,
			"activate": activate,
			"skip_failing_patches": skip_failing_patches,
			"skip_backups": skip_backups,
			"before_migrate_scripts": before_migrate_scripts,
			"skip_search_index": skip_search_index,
		}
		return self.create_agent_job(
			f"Update Site {deploy_type}",
			f"benches/{site.bench}/sites/{site.name}/update/{deploy_type.lower()}",
			data,
			bench=site.bench,
			site=site.name,
		)

	def restore_site_tables(self, site):
		activate = site.status_before_update == "Active"
		data = {"activate": activate}
		return self.create_agent_job(
			"Restore Site Tables",
			f"benches/{site.bench}/sites/{site.name}/update/migrate/restore",
			data,
			bench=site.bench,
			site=site.name,
		)

	def update_site_recover_move(
		self,
		site,
		target,
		deploy_type,
		activate,
		rollback_scripts=None,
		restore_touched_tables=True,
	):
		data = {
			"target": target,
			"activate": activate,
			"rollback_scripts": rollback_scripts,
			"restore_touched_tables": restore_touched_tables,
		}
		return self.create_agent_job(
			f"Recover Failed Site {deploy_type}",
			f"benches/{site.bench}/sites/{site.name}/update/{deploy_type.lower()}/recover",
			data,
			bench=site.bench,
			site=site.name,
		)

	def update_site_recover(self, site):
		return self.create_agent_job(
			"Recover Failed Site Update",
			f"benches/{site.bench}/sites/{site.name}/update/recover",
			bench=site.bench,
			site=site.name,
		)

	def update_site_config(self, site):
		data = {
			"config": json.loads(site.config),
			"remove": json.loads(site._keys_removed_in_last_update),
		}
		return self.create_agent_job(
			"Update Site Configuration",
			f"benches/{site.bench}/sites/{site.name}/config",
			data,
			bench=site.bench,
			site=site.name,
		)

	def reset_site_usage(self, site):
		return self.create_agent_job(
			"Reset Site Usage",
			f"benches/{site.bench}/sites/{site.name}/usage",
			method="DELETE",
			bench=site.bench,
			site=site.name,
		)

	def archive_site(self, site, site_name=None, force=False):
		site_name = site_name or site.name
		database_server = frappe.db.get_value("Bench", site.bench, "database_server")
		data = {
			"mariadb_root_password": get_decrypted_password(
				"Database Server", database_server, "mariadb_root_password"
			),
			"force": force,
		}

		return self.create_agent_job(
			"Archive Site",
			f"benches/{site.bench}/sites/{site_name}/archive",
			data,
			bench=site.bench,
			site=site.name,
		)

	def physical_backup_database(self, site: Site, site_backup: SiteBackup):
		"""
		For physical database backup, the flow :
		- Create the agent job
		- Agent job will lock the specific database + flush the changes to disk
		- Take a database dump
		- Use `fsync` to ensure the changes are written to disk
		- Agent will send back a request to FC for taking the snapshot
			- By calling `snapshot_create_callback` url
		- Then, unlock the database
		"""
		press_public_base_url = frappe.utils.get_url()
		data = {
			"databases": [site_backup.database_name],
			"mariadb_root_password": get_mariadb_root_password(site),
			"private_ip": frappe.get_value(
				"Database Server", frappe.db.get_value("Server", site.server, "database_server"), "private_ip"
			),
			"site_backup": {
				"name": site_backup.name,
				"snapshot_request_key": site_backup.snapshot_request_key,
				"snapshot_trigger_url": f"{press_public_base_url}/api/method/press.api.site_backup.create_snapshot",
			},
		}
		return self.create_agent_job(
			"Physical Backup Database",
			"/database/physical-backup",
			data=data,
			bench=site.bench,
			site=site.name,
		)

	def physical_restore_database(self, site, backup_restoration: PhysicalBackupRestoration):
		data = {
			"backup_db": backup_restoration.source_database,
			"target_db": backup_restoration.destination_database,
			"target_db_root_password": get_mariadb_root_password(site),
			"private_ip": frappe.get_value(
				"Database Server", frappe.db.get_value("Server", site.server, "database_server"), "private_ip"
			),
			"backup_db_base_directory": os.path.join(backup_restoration.mount_point, "var/lib/mysql"),
			"restore_specific_tables": backup_restoration.restore_specific_tables,
			"tables_to_restore": json.loads(backup_restoration.tables_to_restore),
		}
		return self.create_agent_job(
			"Physical Restore Database",
			"/database/physical-restore",
			data=data,
			bench=site.bench,
			site=site.name,
			reference_name=backup_restoration.name,
			reference_doctype=backup_restoration.doctype,
		)

	def backup_site(self, site, site_backup: SiteBackup):
		from press.press.doctype.site_backup.site_backup import get_backup_bucket

		data = {"with_files": site_backup.with_files}

		if site_backup.offsite:
			settings = frappe.get_single("Press Settings")
			backups_path = os.path.join(site.name, str(date.today()))
			backup_bucket = get_backup_bucket(site.cluster, region=True)
			bucket_name = backup_bucket.get("name") if isinstance(backup_bucket, dict) else backup_bucket
			if settings.aws_s3_bucket or bucket_name:
				auth = {
					"ACCESS_KEY": settings.offsite_backups_access_key_id,
					"SECRET_KEY": settings.get_password("offsite_backups_secret_access_key"),
					"REGION": backup_bucket.get("region") if isinstance(backup_bucket, dict) else "",
				}
				data.update({"offsite": {"bucket": bucket_name, "auth": auth, "path": backups_path}})
			else:
				log_error("Offsite Backups aren't set yet")

			data.update(
				{
					"keep_files_locally_after_offsite_backup": bool(
						frappe.get_value("Server", site.server, "keep_files_on_server_in_offsite_backup")
					)
				}
			)

		return self.create_agent_job(
			"Backup Site",
			f"benches/{site.bench}/sites/{site.name}/backup",
			data=data,
			bench=site.bench,
			site=site.name,
		)

	def add_domain(self, site, domain):
		data = {
			"domain": domain,
		}
		return self.create_agent_job(
			"Add Domain",
			f"benches/{site.bench}/sites/{site.name}/domains",
			data,
			bench=site.bench,
			site=site.name,
		)

	def remove_domain(self, site, domain):
		return self.create_agent_job(
			"Remove Domain",
			f"benches/{site.bench}/sites/{site.name}/domains/{domain}",
			method="DELETE",
			site=site.name,
			bench=site.bench,
		)

	def new_host(self, domain):
		certificate = frappe.get_pg("TLS Certificate", domain.tls_certificate)
		data = {
			"name": domain.domain,
			"target": domain.site,
			"certificate": {
				"privkey.pem": certificate.private_key,
				"fullchain.pem": certificate.full_chain,
				"chain.pem": certificate.intermediate_chain,
			},
		}
		return self.create_agent_job(
			"Add Host to Proxy", "proxy/hosts", data, host=domain.domain, site=domain.site
		)

	def setup_wildcard_hosts(self, wildcards):
		return self.create_agent_job("Add Wildcard Hosts to Proxy", "proxy/wildcards", wildcards)

	def setup_redirects(self, site: str, domains: list[str], target: str):
		data = {"domains": domains, "target": target}
		return self.create_agent_job("Setup Redirects on Hosts", "proxy/hosts/redirects", data, site=site)

	def remove_redirects(self, site: str, domains: list[str]):
		data = {"domains": domains}
		return self.create_agent_job(
			"Remove Redirects on Hosts",
			"proxy/hosts/redirects",
			data,
			method="DELETE",
			site=site,
		)

	def remove_host(self, domain):
		return self.create_agent_job(
			"Remove Host from Proxy",
			f"proxy/hosts/{domain.domain}",
			{},
			method="DELETE",
			site=domain.site,
		)

	def new_server(self, server):
		_server = frappe.get_pg("Server", server)
		ip = _server.ip if _server.is_self_hosted else _server.private_ip
		data = {"name": ip}
		return self.create_agent_job("Add Upstream to Proxy", "proxy/upstreams", data, upstream=server)

	def update_upstream_private_ip(self, server):
		ip, private_ip = frappe.db.get_value("Server", server, ["ip", "private_ip"])
		data = {"name": private_ip}
		return self.create_agent_job("Rename Upstream", f"proxy/upstreams/{ip}/rename", data, upstream=server)

	def proxy_add_auto_scale_site_to_upstream(
		self, primary_upstream: str, secondary_upstreams: list[dict[str, int]]
	) -> "AgentJob":
		return self.create_agent_job(
			"Add Auto Scale Site to Upstream",
			f"proxy/upstreams/{primary_upstream}/auto-scale-site",
			data={"secondary_upstreams": secondary_upstreams},
		)

	def proxy_remove_auto_scale_site_to_upstream(self, primary_upstream: str) -> "AgentJob":
		return self.create_agent_job(
			"Remove Auto Scale Site from Upstream",
			f"proxy/upstreams/{primary_upstream}/remove-auto-scale-site",
		)

	def new_upstream_file(self, server, site=None, code_server=None):
		_server = frappe.get_pg("Server", server)
		ip = _server.ip if _server.is_self_hosted else _server.private_ip
		data = {"name": site if site else code_server}
		doctype = "Site" if site else "Code Server"
		return self.create_agent_job(
			f"Add {doctype} to Upstream",
			f"proxy/upstreams/{ip}/sites",
			data,
			site=site,
			code_server=code_server,
			upstream=server,
		)

	def add_domain_to_upstream(self, server, site=None, domain=None):
		_server = frappe.get_pg("Server", server)
		ip = _server.ip if _server.is_self_hosted else _server.private_ip
		data = {"domain": domain}
		return self.create_agent_job(
			"Add Domain to Upstream",
			f"proxy/upstreams/{ip}/domains",
			data,
			site=site,
			upstream=server,
		)

	def remove_upstream_file(self, server, site=None, site_name=None, code_server=None):
		_server = frappe.get_pg("Server", server)
		ip = _server.ip if _server.is_self_hosted else _server.private_ip
		doctype = "Site" if site else "Code Server"
		file_name = site_name or site if (site or site_name) else code_server
		extra_domains = frappe.get_all(
			"Site Domain",
			{"site": site, "tls_certificate": ("is", "not set"), "status": "Active", "domain": ("!=", site)},
			pluck="domain",
		)
		data = {"extra_domains": extra_domains}
		return self.create_agent_job(
			f"Remove {doctype} from Upstream",
			f"proxy/upstreams/{ip}/sites/{file_name}",
			method="DELETE",
			site=site,
			code_server=code_server,
			upstream=server,
			data=data,
		)

	def setup_code_server(self, bench, name, password):
		data = {"name": name, "password": password}
		return self.create_agent_job(
			"Setup Code Server", f"benches/{bench}/codeserver", data, code_server=name
		)

	def start_code_server(self, bench, name, password):
		data = {"password": password}
		return self.create_agent_job(
			"Start Code Server",
			f"benches/{bench}/codeserver/start",
			data,
			code_server=name,
		)

	def stop_code_server(self, bench, name):
		return self.create_agent_job(
			"Stop Code Server",
			f"benches/{bench}/codeserver/stop",
			code_server=name,
		)

	def archive_code_server(self, bench, name):
		return self.create_agent_job(
			"Archive Code Server",
			f"benches/{bench}/codeserver/archive",
			method="POST",
			code_server=name,
		)

	def add_ssh_user(self, bench):
		private_ip = frappe.db.get_value("Server", bench.server, "private_ip")
		candidate = frappe.get_pg("Deploy Candidate", bench.candidate)
		data = {
			"name": bench.name,
			"principal": bench.group,
			"ssh": {"ip": private_ip, "port": 22000 + bench.port_offset},
			"certificate": candidate.get_certificate(),
		}
		return self.create_agent_job(
			"Add User to Proxy", "ssh/users", data, bench=bench.name, upstream=bench.server
		)

	def remove_ssh_user(self, bench):
		return self.create_agent_job(
			"Remove User from Proxy",
			f"ssh/users/{bench.name}",
			method="DELETE",
			bench=bench.name,
			upstream=bench.server,
		)

	def add_proxysql_user(
		self,
		site,
		database: str,
		username: str,
		password: str,
		max_connections: int,
		database_server,
		reference_doctype=None,
		reference_name=None,
	):
		data = {
			"username": username,
			"password": password,
			"database": database,
			"max_connections": max_connections,
			"backend": {"ip": database_server.private_ip, "id": database_server.server_id},
		}
		return self.create_agent_job(
			"Add User to ProxySQL",
			"proxysql/users",
			data,
			site=site.name,
			reference_name=reference_name,
			reference_doctype=reference_doctype,
		)

	def add_proxysql_backend(self, database_server):
		data = {
			"backend": {"ip": database_server.private_ip, "id": database_server.server_id},
		}
		return self.create_agent_job("Add Backend to ProxySQL", "proxysql/backends", data)

	def remove_proxysql_user(self, site, username, reference_doctype=None, reference_name=None):
		return self.create_agent_job(
			"Remove User from ProxySQL",
			f"proxysql/users/{username}",
			method="DELETE",
			site=site.name,
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def create_database_access_credentials(self, site, mode):
		database_server = frappe.db.get_value("Bench", site.bench, "database_server")
		data = {
			"mode": mode,
			"mariadb_root_password": get_decrypted_password(
				"Database Server", database_server, "mariadb_root_password"
			),
		}
		return self.post(f"benches/{site.bench}/sites/{site.name}/credentials", data=data)

	def revoke_database_access_credentials(self, site):
		database_server = frappe.db.get_value("Bench", site.bench, "database_server")
		data = {
			"user": site.database_access_user,
			"mariadb_root_password": get_decrypted_password(
				"Database Server", database_server, "mariadb_root_password"
			),
		}
		return self.post(f"benches/{site.bench}/sites/{site.name}/credentials/revoke", data=data)

	def create_database_user(self, site, username, password, reference_name):
		database_server = frappe.db.get_value("Bench", site.bench, "database_server")
		data = {
			"username": username,
			"password": password,
			"mariadb_root_password": get_decrypted_password(
				"Database Server", database_server, "mariadb_root_password"
			),
		}
		return self.create_agent_job(
			"Create Database User",
			f"benches/{site.bench}/sites/{site.name}/database/users",
			data,
			site=site.name,
			reference_doctype="Site Database User",
			reference_name=reference_name,
		)

	def remove_database_user(self, site, username, reference_name):
		database_server = frappe.db.get_value("Bench", site.bench, "database_server")
		data = {
			"mariadb_root_password": get_decrypted_password(
				"Database Server", database_server, "mariadb_root_password"
			)
		}
		return self.create_agent_job(
			"Remove Database User",
			f"benches/{site.bench}/sites/{site.name}/database/users/{username}",
			method="DELETE",
			data=data,
			site=site.name,
			reference_doctype="Site Database User",
			reference_name=reference_name,
		)

	def modify_database_user_permissions(self, site, username, mode, permissions: dict, reference_name):
		database_server = frappe.db.get_value("Bench", site.bench, "database_server")
		data = {
			"mode": mode,
			"permissions": permissions,
			"mariadb_root_password": get_decrypted_password(
				"Database Server", database_server, "mariadb_root_password"
			),
		}
		return self.create_agent_job(
			"Modify Database User Permissions",
			f"benches/{site.bench}/sites/{site.name}/database/users/{username}/permissions",
			method="POST",
			data=data,
			site=site.name,
			reference_doctype="Site Database User",
			reference_name=reference_name,
		)

	def update_site_status(self, server: str, site: str, status, skip_reload=False):
		extra_domains = frappe.get_all(
			"Site Domain",
			{"site": site, "tls_certificate": ("is", "not set"), "status": "Active", "domain": ("!=", site)},
			pluck="domain",
		)
		data = {"status": status, "extra_domains": extra_domains, "skip_reload": skip_reload}
		_server = frappe.get_pg("Server", server)
		ip = _server.ip if _server.is_self_hosted else _server.private_ip
		return self.create_agent_job(
			"Update Site Status",
			f"proxy/upstreams/{ip}/sites/{site}/status",
			data=data,
			site=site,
			upstream=server,
		)

	def reload_nginx(self):
		return self.create_agent_job("Reload NGINX Job", "proxy/reload")

	def cleanup_unused_files(self, force: bool = False):
		return self.create_agent_job("Cleanup Unused Files", "server/cleanup", {"force": force})

	def get(self, path, raises=True):
		return self.request("GET", path, raises=raises)

	def post(self, path, data=None, raises=True):
		return self.request("POST", path, data, raises=raises)

	def delete(self, path, data=None, raises=True):
		return self.request("DELETE", path, data, raises=raises)

	def _make_req(self, method, path, data, files, agent_job_id):
		password = get_decrypted_password(self.server_type, self.server, "agent_password")
		headers = {"Authorization": f"bearer {password}", "X-Agent-Job-Id": agent_job_id}
		url = f"https://{self.server}:{self.port}/agent/{path}"
		intermediate_ca = frappe.db.get_value("Press Settings", "Press Settings", "backbone_intermediate_ca")
		if frappe.conf.developer_mode and intermediate_ca:
			root_ca = frappe.db.get_value("Certificate Authority", intermediate_ca, "parent_authority")
			verify = frappe.get_pg("Certificate Authority", root_ca).certificate_file
		else:
			verify = True
		if files:
			file_objects = {
				key: value
				if isinstance(value, _io.BufferedReader)
				else frappe.get_pg("File", {"file_url": url}).get_content()
				for key, value in files.items()
			}
			file_objects["json"] = json.dumps(data).encode()
			return requests.request(method, url, headers=headers, files=file_objects, verify=verify)
		return requests.request(method, url, headers=headers, json=data, verify=verify, timeout=(10, 30))

	def request(self, method, path, data=None, files=None, agent_job=None, raises=True):
		self.raise_if_past_requests_have_failed()
		response = json_response = None
		try:
			agent_job_id = agent_job.name if agent_job else None
			response = self._make_req(method, path, data, files, agent_job_id)
			json_response = response.json()
			if raises and response.status_code >= 400:
				output = "\n\n".join([json_response.get("output", ""), json_response.get("traceback", "")])
				if output == "\n\n":
					output = json.dumps(json_response, indent=2, sort_keys=True)
				raise HTTPError(
					f"{response.status_code} {response.reason}\n\n{output}",
					response=response,
				)
			return json_response
		except (HTTPError, TypeError, ValueError):
			self.handle_request_failure(agent_job, response)
			log_error(
				title="Agent Request Result Exception",
				result=json_response or getattr(response, "text", None),
			)
		except requests.JSONDecodeError as exc:
			if response and response.status_code >= 500:
				self.log_request_failure(exc)
				self.handle_exception(agent_job, exc)
				log_error(
					title="Agent Request Exception",
				)
		except Exception as exc:
			self.log_request_failure(exc)
			self.handle_exception(agent_job, exc)
			log_error(
				title="Agent Request Exception",
			)

	def raise_if_past_requests_have_failed(self):
		failures = frappe.db.get_value("Agent Request Failure", {"server": self.server}, "failure_count")
		if failures:
			raise AgentRequestSkippedException(f"Previous {failures} requests have failed. Try again later.")

	def log_request_failure(self, exc):
		filters = {
			"server": self.server,
		}
		failure = frappe.db.get_value(
			"Agent Request Failure", filters, ["name", "failure_count"], as_dict=True
		)
		if failure:
			frappe.db.set_value(
				"Agent Request Failure", failure.name, "failure_count", failure.failure_count + 1
			)
		else:
			fields = filters
			fields.update(
				{
					"server_type": self.server_type,
					"traceback": frappe.get_traceback(with_context=True),
					"error": repr(exc),
					"failure_count": 1,
				}
			)
			is_primary = frappe.db.get_value(self.server_type, self.server, "is_primary")
			if self.server_type == "Server" and not is_primary:
				# Don't create agent request failures for secondary servers
				# Since we try to connect to them frequently after IP changes
				return

			frappe.new_pg("Agent Request Failure", **fields).insert(ignore_permissions=True)

	def raw_request(self, method, path, data=None, raises=True, timeout=None):
		url = f"https://{self.server}:{self.port}/agent/{path}"
		password = get_decrypted_password(self.server_type, self.server, "agent_password")
		headers = {"Authorization": f"bearer {password}"}
		timeout = timeout or (10, 30)
		response = requests.request(method, url, headers=headers, json=data, timeout=timeout)
		json_response = response.json()
		if raises:
			response.raise_for_status()
		return json_response

	def should_skip_requests(self):
		if self.server_type in ("Server", "Database Server", "Proxy Server") and frappe.db.get_value(
			self.server_type, self.server, "halt_agent_jobs"
		):
			return True

		return bool(frappe.db.count("Agent Request Failure", {"server": self.server}))

	def handle_request_failure(self, agent_job, result: Response | None):
		if not agent_job:
			raise

		status_code = getattr(result, "status_code", "Unknown")
		with suppress(TypeError, ValueError):
			reason = json.dumps(result.json(), indent=4, sort_keys=True) if result else None

		message = f"""
Status Code: {status_code}\n
Response: {reason or getattr(result, "text", "Unknown")}
"""
		self.log_failure_reason(agent_job, message)
		agent_job.flags.status_code = status_code

	def handle_exception(self, agent_job, exception):
		self.log_failure_reason(agent_job, exception)

	def log_failure_reason(self, agent_job=None, message=None):
		if not agent_job:
			raise

		agent_job.traceback = message
		agent_job.output = message

	def create_agent_job(
		self,
		job_type,
		path,
		data: dict | None = None,
		files=None,
		method="POST",
		bench=None,
		site=None,
		code_server=None,
		upstream=None,
		host=None,
		reference_doctype=None,
		reference_name=None,
	):
		"""
		Check if job already exists in Undelivered, Pending, Running state
		don't add new job until its gets completed
		"""

		disable_agent_job_deduplication = frappe.db.get_single_value(
			"Press Settings", "disable_agent_job_deduplication", cache=True
		)

		if not disable_agent_job_deduplication:
			existing_job = self.get_similar_in_execution_job(
				job_type, path, bench, site, code_server, upstream, host, method
			)

			if existing_job:
				return existing_job

		job: "AgentJob" = frappe.get_pg(
			{
				"doctype": "Agent Job",
				"server_type": self.server_type,
				"server": self.server,
				"bench": bench,
				"host": host,
				"site": site,
				"code_server": code_server,
				"upstream": upstream,
				"status": "Undelivered",
				"request_method": method,
				"request_path": path,
				"request_data": json.dumps(data or {}, indent=4, sort_keys=True),
				"request_files": json.dumps(files or {}, indent=4, sort_keys=True),
				"job_type": job_type,
				"reference_doctype": reference_doctype,
				"reference_name": reference_name,
			}
		).insert()
		return job

	def get_similar_in_execution_job(
		self,
		job_type,
		path,
		bench=None,
		site=None,
		code_server=None,
		upstream=None,
		host=None,
		method="POST",
	):
		"""Deduplicate jobs in execution state"""

		filters = {
			"server_type": self.server_type,
			"server": self.server,
			"job_type": job_type,
			"status": ("not in", ("Success", "Failure", "Delivery Failure")),
			"request_method": method,
			"request_path": path,
		}

		if bench:
			filters["bench"] = bench

		if site:
			filters["site"] = site if not isinstance(site, list) else ("IN", site)

		if code_server:
			filters["code_server"] = code_server

		if upstream:
			filters["upstream"] = upstream

		if host:
			filters["host"] = host

		job = frappe.db.get_value("Agent Job", filters, "name", debug=1)

		return frappe.get_pg("Agent Job", job) if job else False

	def update_monitor_rules(self, rules, routes):
		data = {"rules": rules, "routes": routes}
		return self.post("monitor/rules", data=data)

	def get_job_status(self, id):
		return self.get(f"jobs/{id}")

	def cancel_job(self, id):
		return self.post(f"jobs/{id}/cancel")

	def get_site_sid(self, site, user=None):
		if user:
			data = {"user": user}
			result = self.post(f"benches/{site.bench}/sites/{site.name}/sid", data=data)
		else:
			result = self.get(f"benches/{site.bench}/sites/{site.name}/sid")
		return result and result.get("sid")

	def get_site_info(self, site):
		result = self.get(f"benches/{site.bench}/sites/{site.name}/info")
		if result:
			return result["data"]
		return None

	def get_sites_info(self, bench, since):
		return self.post(f"benches/{bench.name}/info", data={"since": since})

	def get_site_analytics(self, site):
		result = self.get(f"benches/{site.bench}/sites/{site.name}/analytics")
		if result:
			return result["data"]
		return None

	def get_sites_analytics(self, bench):
		return self.get(f"benches/{bench.name}/analytics")

	def describe_database_table(self, site, doctype, columns):
		data = {"doctype": doctype, "columns": list(columns)}
		return self.post(
			f"benches/{site.bench}/sites/{site.name}/describe-database-table",
			data=data,
		)["data"]

	def add_database_index(self, site, doctype, columns):
		data = {"doctype": doctype, "columns": list(columns)}
		return self.create_agent_job(
			"Add Database Index",
			f"benches/{site.bench}/sites/{site.name}/add-database-index",
			data,
			site=site.name,
		)

	def get_jobs_status(self, ids):
		status = self.get(f"jobs/{','.join(map(str, ids))}")
		if len(ids) == 1:
			return [status]
		return status

	def get_jobs_id(self, agent_job_ids):
		return self.get(f"agent-jobs/{agent_job_ids}")

	def get_version(self):
		return self.raw_request("GET", "version", raises=True, timeout=(2, 10))

	def update(self):
		url = frappe.get_pg(self.server_type, self.server).get_agent_repository_url()
		branch = frappe.get_pg(self.server_type, self.server).get_agent_repository_branch()
		return self.post("update", data={"url": url, "branch": branch})

	def ping(self):
		return self.raw_request("GET", "ping", raises=True, timeout=(2, 5))["message"]

	def fetch_monitor_data(self, bench):
		return self.post(f"benches/{bench}/monitor")["data"]

	def fetch_site_status(self, site):
		return self.get(f"benches/{site.bench}/sites/{site.name}/status")["data"]

	def fetch_bench_status(self, bench):
		return self.get(f"benches/{bench}/status")

	def get_snapshot(self, bench: str):
		return self.get(f"process-snapshot/{bench}")

	def run_after_migrate_steps(self, site):
		data = {
			"admin_password": site.get_password("admin_password"),
		}
		return self.create_agent_job(
			"Run After Migrate Steps",
			f"benches/{site.bench}/sites/{site.name}/run_after_migrate_steps",
			bench=site.bench,
			site=site.name,
			data=data,
		)

	def move_site_to_bench(
		self,
		site,
		target,
		deactivate=True,
		skip_failing_patches=False,
	):
		"""
		Move site to bench without backup
		"""
		activate = site.status not in ("Inactive", "Suspended")
		data = {
			"target": target,
			"deactivate": deactivate,
			"activate": activate,
			"skip_failing_patches": skip_failing_patches,
		}
		return self.create_agent_job(
			"Move Site to Bench",
			f"benches/{site.bench}/sites/{site.name}/move_to_bench",
			data,
			bench=site.bench,
			site=site.name,
		)

	def force_update_bench_limits(self, bench: str, data: dict):
		return self.create_agent_job(
			"Force Update Bench Limits", f"benches/{bench}/limits", bench=bench, data=data
		)

	def patch_app(self, app_patch: "AppPatch", data: "AgentPatchConfig"):
		bench = app_patch.bench
		app = app_patch.app
		return self.create_agent_job(
			"Patch App",
			f"benches/{bench}/patch/{app}",
			bench=bench,
			data=data,
			reference_doctype="App Patch",
			reference_name=app_patch.name,
		)

	def upload_build_context_for_docker_build(
		self,
		file: "BufferedReader",
		dc_name: str,
	) -> str | None:
		if res := self.request("POST", f"builder/upload/{dc_name}", files={"build_context_file": file}):
			return res.get("filename")

		return None

	def run_build(self, data: dict):
		reference_name = data.get("deploy_candidate_build")
		return self.create_agent_job(
			"Run Remote Builder",
			"builder/build",
			data=data,
			reference_doctype="Deploy Candidate Build",
			reference_name=reference_name,
		)

	def call_supervisorctl(self, bench: str, action: str, programs: list[str]):
		return self.create_agent_job(
			"Call Bench Supervisorctl",
			f"/benches/{bench}/supervisorctl",
			data={"command": action, "programs": programs},
		)

	def run_command_in_docker_cache(
		self,
		command: str = "ls -A",
		cache_target: str = "/home/frappe/.cache",
		remove_image: bool = True,
	):
		data = dict(
			command=command,
			cache_target=cache_target,
			remove_image=remove_image,
		)
		return self.request(
			"POST",
			"docker_cache_utils/run_command_in_docker_cache",
			data=data,
		)

	def get_cached_apps(self):
		return self.request(
			"POST",
			"docker_cache_utils/get_cached_apps",
			data={},
		)

	def get_site_apps(self, site):
		raw_apps_list = self.get(
			f"benches/{site.bench}/sites/{site.name}/apps",
		)

		try:
			raw_apps = raw_apps_list["data"]
			apps = APPS_LIST_REGEX.findall(raw_apps)[0]
			apps: list[str] = json.loads(apps)
		except (json.JSONDecodeError, IndexError):
			apps: list[str] = [line.split()[0] for line in raw_apps_list["data"].splitlines() if line]

		return apps

	def fetch_database_table_schema(
		self, site, include_table_size: bool = False, include_index_info: bool = False
	):
		return self.create_agent_job(
			"Fetch Database Table Schema",
			f"benches/{site.bench}/sites/{site.name}/database/schema",
			bench=site.bench,
			site=site.name,
			data={
				"include_table_size": include_table_size,
				"include_index_info": include_index_info,
			},
			reference_doctype="Site",
			reference_name=site.name,
		)

	def run_sql_query_in_database(self, site, query, commit):
		return self.post(
			f"benches/{site.bench}/sites/{site.name}/database/query/execute",
			data={"query": query, "commit": commit, "as_dict": False},
		)

	def get_summarized_performance_report_of_database(self, site):
		return self.post(
			f"benches/{site.bench}/sites/{site.name}/database/performance-report",
			data={"mariadb_root_password": get_mariadb_root_password(site)},
		)

	def analyze_slow_queries(self, site, normalized_queries: list[dict]):
		"""
		normalized_queries format:
		[
			{
				"example": "",
				"normalized" : "",
			}
		]
		"""
		return self.create_agent_job(
			"Analyze Slow Queries",
			f"benches/{site.bench}/sites/{site.name}/database/analyze-slow-queries",
			data={
				"queries": normalized_queries,
				"mariadb_root_password": get_mariadb_root_password(site),
			},
			site=site.name,
		)

	def fetch_database_processes(self, site):
		return self.post(
			f"benches/{site.bench}/sites/{site.name}/database/processes",
			data={
				"mariadb_root_password": get_mariadb_root_password(site),
			},
		)

	def kill_database_process(self, site, id):
		return self.post(
			f"benches/{site.bench}/sites/{site.name}/database/kill-process/{id}",
			data={
				"mariadb_root_password": get_mariadb_root_password(site),
			},
		)

	def update_database_schema_sizes(self):
		if self.server_type != "Database Server":
			return NotImplementedError("This method is only supported for Database Server")

		return self.create_agent_job(
			"Update Database Schema Sizes",
			"database/update-schema-sizes",
			data={
				"private_ip": frappe.get_value("Database Server", self.server, "private_ip"),
				"mariadb_root_password": get_decrypted_password(
					"Database Server", self.server, "mariadb_root_password"
				),
			},
			reference_doctype=self.server_type,
			reference_name=self.server,
		)

	def fetch_database_variables(self):
		if self.server_type != "Database Server":
			return NotImplementedError("Only Database Server supports this method")
		return self.post(
			"database/variables",
			data={
				"private_ip": frappe.get_value("Database Server", self.server, "private_ip"),
				"mariadb_root_password": get_decrypted_password(
					"Database Server", self.server, "mariadb_root_password"
				),
			},
		)

	def fetch_binlog_list(self):
		return self.get("database/binlogs/list")

	def pull_docker_images(self, image_tags: list[str], reference_doctype=None, reference_name=None):
		settings = frappe.db.get_value(
			"Press Settings",
			None,
			["docker_registry_url", "docker_registry_username", "docker_registry_password"],
			as_dict=True,
		)
		return self.create_agent_job(
			"Pull Docker Images",
			"/server/pull-images",
			data={
				"image_tags": image_tags,
				"registry": {
					"url": settings.docker_registry_url,
					"username": settings.docker_registry_username,
					"password": settings.docker_registry_password,
				},
			},
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def push_docker_images(
		self, images: list[str], reference_doctype: str | None = None, reference_name: str | None = None
	) -> AgentJob:
		settings = frappe.db.get_value(
			"Press Settings",
			None,
			["docker_registry_url", "docker_registry_username", "docker_registry_password"],
			as_dict=True,
		)
		return self.create_agent_job(
			"Push Images to Registry",
			"/server/push-images",
			data={
				"images": images,
				"registry_settings": {
					"url": settings.docker_registry_url,
					"username": settings.docker_registry_username,
					"password": settings.docker_registry_password,
				},
			},
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def upload_binlogs_to_s3(self, binlogs: list[str]):
		from press.press.doctype.site_backup.site_backup import get_backup_bucket

		if self.server_type != "Database Server":
			return NotImplementedError("Only Database Server supports this method")

		settings = frappe.get_single("Press Settings")
		backup_bucket = get_backup_bucket(
			frappe.get_value("Database Server", self.server, "cluster"), region=True
		)
		bucket_name = backup_bucket.get("name") if isinstance(backup_bucket, dict) else backup_bucket
		if not (settings.aws_s3_bucket or bucket_name):
			return ValueError("Offsite Backups aren't set yet")

		auth = {
			"ACCESS_KEY": settings.offsite_backups_access_key_id,
			"SECRET_KEY": settings.get_password("offsite_backups_secret_access_key"),
			"REGION": backup_bucket.get("region") if isinstance(backup_bucket, dict) else "",
		}

		return self.create_agent_job(
			"Upload Binlogs To S3",
			"/database/binlogs/upload",
			data={"binlogs": binlogs, "offsite": {"bucket": bucket_name, "auth": auth, "path": self.server}},
		)

	def add_binlogs_to_indexer(self, binlogs):
		return self.create_agent_job(
			"Add Binlogs To Indexer",
			"/database/binlogs/indexer/add",
			data={"binlogs": binlogs},
		)

	def remove_binlogs_from_indexer(self, binlogs):
		return self.create_agent_job(
			"Remove Binlogs From Indexer", "/database/binlogs/indexer/remove", data={"binlogs": binlogs}
		)

	def get_binlogs_timeline(
		self,
		start: int,
		end: int,
		database: str,
		table: str | None = None,
		type: str | None = None,
		event_size_comparator: Literal["gt", "lt"] | None = None,
		event_size: int | None = None,
	):
		return self.post(
			"/database/binlogs/indexer/timeline",
			data={
				"start_timestamp": start,
				"end_timestamp": end,
				"database": database,
				"table": table,
				"type": type,
				"event_size_comparator": event_size_comparator,
				"event_size": event_size,
			},
		)

	def search_binlogs(
		self,
		start: int,
		end: int,
		database: str,
		type: str | None = None,
		table: str | None = None,
		search_str: str | None = None,
		event_size_comparator: Literal["gt", "lt"] | None = None,
		event_size: int | None = None,
	):
		return self.post(
			"/database/binlogs/indexer/search",
			data={
				"start_timestamp": start,
				"end_timestamp": end,
				"database": database,
				"type": type,
				"table": table,
				"search_str": search_str,
				"event_size_comparator": event_size_comparator,
				"event_size": event_size,
			},
		)

	def purge_binlog(self, database_server: DatabaseServer, to_binlog: str):
		return self.post(
			"/database/binlogs/purge",
			data={
				"private_ip": database_server.private_ip,
				"mariadb_root_password": database_server.get_password("mariadb_root_password"),
				"to_binlog": to_binlog,
			},
		)

	def purge_binlogs_by_size_limit(self, database_server: DatabaseServer, max_binlog_gb: int):
		return self.create_agent_job(
			"Purge Binlogs By Size Limit",
			"/database/binlogs/purge_by_size_limit",
			data={
				"private_ip": database_server.private_ip,
				"mariadb_root_password": database_server.get_password("mariadb_root_password"),
				"max_binlog_gb": max_binlog_gb,
			},
		)

	def get_binlog_queries(self, row_ids: dict[str, list[int]], database: str):
		return self.post(
			"/database/binlogs/indexer/query",
			data={
				"row_ids": row_ids,
				"database": database,
			},
		)

	def ping_database(self, database_server: DatabaseServer):
		return self.post(
			"/database/ping",
			data={
				"private_ip": database_server.private_ip,
				"mariadb_root_password": database_server.get_password("mariadb_root_password"),
			},
		)

	def get_replication_status(self, database_server: DatabaseServer):
		return self.post(
			"/database/replication/status",
			data={
				"private_ip": database_server.private_ip,
				"mariadb_root_password": database_server.get_password("mariadb_root_password"),
			},
		)

	def reset_replication(self, database_server: DatabaseServer):
		return self.post(
			"/database/replication/reset",
			data={
				"private_ip": database_server.private_ip,
				"mariadb_root_password": database_server.get_password("mariadb_root_password"),
			},
		)

	def configure_replication(
		self,
		database_server: DatabaseServer,
		master_database_server: DatabaseServer,
		gtid_slave_pos: str | None = None,
	):
		return self.post(
			"/database/replication/config",
			data={
				"private_ip": database_server.private_ip,
				"mariadb_root_password": database_server.get_password("mariadb_root_password"),
				"master_private_ip": master_database_server.private_ip,
				"master_mariadb_root_password": master_database_server.get_password("mariadb_root_password"),
				"gtid_slave_pos": gtid_slave_pos,
			},
		)

	def start_replication(self, database_server: DatabaseServer):
		return self.post(
			"/database/replication/start",
			data={
				"private_ip": database_server.private_ip,
				"mariadb_root_password": database_server.get_password("mariadb_root_password"),
			},
		)

	def stop_replication(self, database_server: DatabaseServer):
		return self.post(
			"/database/replication/stop",
			data={
				"private_ip": database_server.private_ip,
				"mariadb_root_password": database_server.get_password("mariadb_root_password"),
			},
		)

	# Snapshot Recovery Related Methods
	def search_sites_in_snapshot(self, sites: list[str], reference_doctype=None, reference_name=None):
		return self.create_agent_job(
			"Search Sites In Snapshot",
			"/snapshot_recovery/search_sites",
			data={"sites": sites},
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def backup_site_database_from_snapshot(
		self,
		cluster: str,
		site: str,
		database_name: str,
		database_server: str,
		reference_doctype=None,
		reference_name=None,
	):
		from press.press.doctype.site_backup.site_backup import get_backup_bucket

		database_server_pg: DatabaseServer = frappe.get_pg("Database Server", database_server)  # type: ignore
		data = {
			"site": site,
			"database_name": database_name,
			"database_ip": frappe.get_value(
				"Virtual Machine", database_server_pg.virtual_machine, "private_ip_address"
			),
			"mariadb_root_password": database_server_pg.get_password("mariadb_root_password"),
		}

		# offsite config
		settings = frappe.get_single("Press Settings")
		backups_path = os.path.join(site, str(date.today()))
		backup_bucket = get_backup_bucket(cluster, region=True)
		bucket_name = backup_bucket.get("name") if isinstance(backup_bucket, dict) else backup_bucket
		if settings.aws_s3_bucket or bucket_name:
			auth = {
				"ACCESS_KEY": settings.offsite_backups_access_key_id,
				"SECRET_KEY": settings.get_password("offsite_backups_secret_access_key"),
				"REGION": backup_bucket.get("region") if isinstance(backup_bucket, dict) else "",
			}
			data.update({"offsite": {"bucket": bucket_name, "auth": auth, "path": backups_path}})
		else:
			frappe.throw("Offsite Backups aren't setup yet")

		return self.create_agent_job(
			"Backup Database From Snapshot",
			"/snapshot_recovery/backup_database",
			data=data,
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def backup_site_files_from_snapshot(
		self,
		cluster: str,
		site: str,
		bench: str,
		reference_doctype=None,
		reference_name=None,
	):
		from press.press.doctype.site_backup.site_backup import get_backup_bucket

		data: dict[str, Any] = {
			"site": site,
			"bench": bench,
		}

		# offsite config
		settings = frappe.get_single("Press Settings")
		backups_path = os.path.join(site, str(date.today()))
		backup_bucket = get_backup_bucket(cluster, region=True)
		bucket_name = backup_bucket.get("name") if isinstance(backup_bucket, dict) else backup_bucket
		if settings.aws_s3_bucket or bucket_name:
			auth = {
				"ACCESS_KEY": settings.offsite_backups_access_key_id,
				"SECRET_KEY": settings.get_password("offsite_backups_secret_access_key"),
				"REGION": backup_bucket.get("region") if isinstance(backup_bucket, dict) else "",
			}
			data.update({"offsite": {"bucket": bucket_name, "auth": auth, "path": backups_path}})
		else:
			frappe.throw("Offsite Backups aren't setup yet")

		return self.create_agent_job(
			"Backup Files From Snapshot",
			"/snapshot_recovery/backup_files",
			data=data,
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def update_database_host_in_all_benches(
		self, db_host: str, reference_doctype: str | None = None, reference_name: str | None = None
	):
		return self.create_agent_job(
			"Update Database Host",
			"/benches/database_host",
			data={
				"db_host": db_host,
			},
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def change_bench_directory(
		self,
		secondary_server_private_ip: str,
		is_primary: bool,
		directory: str,
		restart_benches: bool,
		reference_name: str | None = None,
		redis_connection_string_ip: str | None = None,
		reference_doctype: str | None = None,
		registry_settings: dict | None = None,
	) -> AgentJob:
		return self.create_agent_job(
			"Change Bench Directory",
			"/server/change-bench-directory",
			data={
				"restart_benches": restart_benches,
				"redis_connection_string_ip": redis_connection_string_ip,
				"is_primary": is_primary,
				"directory": directory,
				"secondary_server_private_ip": secondary_server_private_ip,
				"registry_settings": registry_settings,
			},
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def add_servers_to_acl(
		self,
		secondary_server_private_ip: str,
		reference_doctype: str | None = None,
		reference_name: str | None = None,
	) -> AgentJob:
		return self.create_agent_job(
			"Add Servers to ACL",
			"/nfs/add-to-acl",
			data={
				"secondary_server_private_ip": secondary_server_private_ip,
			},
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def remove_servers_from_acl(
		self,
		secondary_server_private_ip: str,
		reference_doctype: str | None = None,
		reference_name: str | None = None,
	) -> AgentJob:
		return self.create_agent_job(
			"Remove Servers from ACL",
			"/nfs/remove-from-acl",
			data={
				"secondary_server_private_ip": secondary_server_private_ip,
			},
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def stop_bench_workers(
		self, reference_doctype: str | None = None, reference_name: str | None = None
	) -> AgentJob:
		return self.create_agent_job(
			"Stop Bench Workers",
			"/server/stop-bench-workers",
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def start_bench_workers(
		self, reference_doctype: str | None = None, reference_name: str | None = None
	) -> AgentJob:
		return self.create_agent_job(
			"Start Bench Workers",
			"/server/start-bench-workers",
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def remove_redis_localhost_bind(
		self, reference_doctype: str | None = None, reference_name: str | None = None
	) -> AgentJob:
		return self.create_agent_job(
			"Remove Redis Localhost Bind",
			"/server/remove-localhost-redis-bind",
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)

	def force_remove_all_benches(
		self, reference_doctype: str | None = None, reference_name: str | None = None
	) -> AgentJob:
		return self.create_agent_job(
			"Force Remove All Benches",
			"/server/force-remove-all-benches",
			reference_doctype=reference_doctype,
			reference_name=reference_name,
		)


class AgentCallbackException(Exception):
	pass


class AgentRequestSkippedException(Exception):
	pass
