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

import re
from collections import OrderedDict
from typing import TYPE_CHECKING

import jingrow
from frappe.core.utils import find, find_all
from frappe.model.naming import append_number_if_name_exists
from frappe.utils import flt, sbool

from press.api.github import branches
from press.api.site import protected
from press.press.doctype.agent_job.agent_job import job_detail
from press.press.doctype.app_patch.app_patch import create_app_patch
from press.press.doctype.bench_update.bench_update import get_bench_update
from press.press.doctype.cluster.cluster import Cluster
from press.press.doctype.deploy_candidate_build.deploy_candidate_build import (
	fail_and_redeploy as fail_and_redeploy_build,
)
from press.press.doctype.deploy_candidate_build.deploy_candidate_build import fail_remote_job
from press.press.doctype.deploy_candidate_build.deploy_candidate_build import redeploy as redeploy_candidate
from press.press.doctype.marketplace_app.marketplace_app import (
	get_total_installs_by_app,
)
from press.press.doctype.release_group.release_group import (
	ReleaseGroup,
	new_release_group,
)
from press.press.doctype.team.team import get_child_team_members
from press.utils import (
	get_app_tag,
	get_client_blacklisted_keys,
	get_current_team,
	unique,
)

if TYPE_CHECKING:
	from press.press.doctype.app_source.app_source import AppSource
	from press.press.doctype.bench.bench import Bench
	from press.press.doctype.deploy_candidate.deploy_candidate import DeployCandidate
	from press.press.doctype.deploy_candidate_build.deploy_candidate_build import DeployCandidateBuild


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

	if exists(bench["title"]):
		frappe.throw("A bench exists with the same name")

	if bench["server"] and not (
		frappe.session.data.user_type == "System User"
		or frappe.db.get_value("Server", bench["server"], "team") == team.name
	):
		frappe.throw("You can only create benches on your servers")

	apps = [{"app": app["name"], "source": app["source"]} for app in bench["apps"]]
	group = new_release_group(
		bench["title"],
		bench["version"],
		apps,
		team.name,
		bench["cluster"],
		bench["saas_app"] if frappe.db.exists("Saas App", bench["saas_app"]) else "",
		bench["server"],
	)
	return group.name


@jingrow.whitelist()
@protected("Release Group")
def get(name):
	group = frappe.get_pg("Release Group", name)
	return {
		"name": group.name,
		"title": group.title,
		"team": group.team,
		"version": group.version,
		"status": get_group_status(name),
		"last_updated": group.modified,
		"creation": group.creation,
		"saas_app": group.saas_app or "",
		"public": group.public,
		"no_sites": frappe.db.count("Site", {"group": group.name, "status": "Active"}),
		"bench_tags": [{"name": x.tag, "tag": x.tag_name} for x in group.tags],
		"tags": frappe.get_all(
			"Press Tag", {"team": group.team, "doctype_name": "Release Group"}, ["name", "tag"]
		),
	}


def get_group_status(name):
	active_benches = frappe.get_all(
		"Bench", {"group": name, "status": "Active"}, limit=1, order_by="creation desc"
	)

	return "Active" if active_benches else "Awaiting Deploy"


@jingrow.whitelist()
def all(server=None, bench_filter=None):
	if bench_filter is None:
		bench_filter = {"status": "", "tag": ""}

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

	group = frappe.qb.DocType("Release Group")
	site = frappe.qb.DocType("Site")
	query = (
		frappe.qb.from_(group)
		.left_join(site)
		.on((site.group == group.name) & (site.status != "Archived"))
		.where((group.enabled == 1) & (group.public == 0))
		.where((group.team).isin(teams))
		.groupby(group.name)
		.select(
			frappe.query_builder.functions.Count(site.name).as_("number_of_sites"),
			group.name,
			group.title,
			group.version,
			group.creation,
		)
		.orderby(group.title, order=frappe.qb.desc)
	)

	bench = frappe.qb.DocType("Bench")
	if bench_filter["status"] == "Active":
		query = query.inner_join(bench).on(group.name == bench.group)
	elif bench_filter["status"] == "Awaiting Deploy":
		group_names = frappe.get_all("Bench", {"status": "Active"}, pluck="group", distinct=True)
		query = query.inner_join(bench).on(group.name.notin(group_names))
	if bench_filter["tag"]:
		press_tag = frappe.qb.DocType("Resource Tag")
		query = query.inner_join(press_tag).on(
			(press_tag.tag_name == bench_filter["tag"]) & (press_tag.parent == group.name)
		)

	if server:
		group_server = frappe.qb.DocType("Release Group Server")
		query = (
			query.inner_join(group_server)
			.on(group_server.parent == group.name)
			.where(group_server.server == server)
		)
	private_groups = query.run(as_dict=True)

	if not private_groups:
		return []

	app_counts = get_app_counts_for_groups([rg.name for rg in private_groups])
	for group in private_groups:
		group.tags = frappe.get_all("Resource Tag", {"parent": group.name}, pluck="tag_name")
		group.number_of_apps = app_counts[group.name]
		group.status = get_group_status(group.name)

	return private_groups


@jingrow.whitelist()
def bench_tags():
	team = get_current_team()
	return frappe.get_all("Press Tag", {"team": team, "doctype_name": "Release Group"}, pluck="tag")


def get_app_counts_for_groups(rg_names):
	rg_app = frappe.qb.DocType("Release Group App")

	app_counts = (
		frappe.qb.from_(rg_app)
		.where(rg_app.parent.isin(rg_names))
		.groupby(rg_app.parent)
		.select(
			rg_app.parent,
			frappe.query_builder.functions.Count("*"),
		)
		.run()
	)

	app_counts_map = {}
	for rg_name, app_count in app_counts:
		app_counts_map[rg_name] = app_count

	return app_counts_map


@jingrow.whitelist()
def exists(title):
	team = get_current_team()
	return bool(frappe.db.exists("Release Group", {"title": title, "team": team, "enabled": True}))


@jingrow.whitelist()
def get_default_apps():
	press_settings = frappe.get_single("Press Settings")
	default_apps = press_settings.get_default_apps()

	versions, rows = get_app_versions_list()

	version_based_default_apps = {v.version: [] for v in versions}

	for app in default_apps:
		for row in filter(lambda x: x.app == app, rows):
			version_based_default_apps[row.version].append(row)

	return version_based_default_apps


def get_app_versions_list(only_frappe=False):
	AppSource = frappe.qb.DocType("App Source")
	FrappeVersion = frappe.qb.DocType("Frappe Version")
	AppSourceVersion = frappe.qb.DocType("App Source Version")
	rows = (
		frappe.qb.from_(AppSourceVersion)
		.left_join(AppSource)
		.on(AppSourceVersion.parent == AppSource.name)
		.left_join(FrappeVersion)
		.on(AppSourceVersion.version == FrappeVersion.name)
		.where((AppSource.enabled == 1) & (AppSource.public == 1) & (FrappeVersion.public == 1))
		.select(
			FrappeVersion.name.as_("version"),
			FrappeVersion.status,
			FrappeVersion.default,
			AppSource.name.as_("source"),
			AppSource.app,
			AppSource.repository_url,
			AppSource.repository,
			AppSource.repository_owner,
			AppSource.branch,
			AppSource.app_title.as_("title"),
			AppSource.frappe,
		)
		.orderby(AppSource.creation)
	)

	if only_frappe:
		rows = rows.where(AppSource.frappe == 1)

	rows = rows.run(as_dict=True)

	version_list = unique(rows, lambda x: x.version)

	return version_list, rows


@jingrow.whitelist()
def options():
	version_list, rows = get_app_versions_list(only_frappe=True)
	approved_apps = frappe.get_all("Marketplace App", filters={"frappe_approved": 1}, pluck="app")

	versions = []
	for d in version_list:
		version_dict = {"name": d.version, "status": d.status, "default": d.default}
		version_rows = find_all(rows, lambda x: x.version == d.version)
		app_list = frappe.utils.unique([row.app for row in version_rows])
		app_list = sorted(app_list, key=lambda x: x not in approved_apps)

		for app in app_list:
			app_rows = find_all(version_rows, lambda x: x.app == app)
			app_dict = {"name": app, "title": app_rows[0].title}

			for source in app_rows:
				source_dict = {
					"name": source.source,
					"repository_url": source.repository_url,
					"branch": source.branch,
					"repository": source.repository,
					"repository_owner": source.repository_owner,
				}
				app_dict.setdefault("sources", []).append(source_dict)

			app_dict["source"] = app_dict["sources"][0]
			version_dict.setdefault("apps", []).append(app_dict)
		versions.append(version_dict)

	clusters = Cluster.get_all_for_new_bench()

	if not versions:
		frappe.throw("Only enabled and public app sources will reflect here!")

	return {"versions": versions, "clusters": clusters}


@jingrow.whitelist()
@protected("Release Group")
def bench_config(name):
	rg = frappe.get_pg("Release Group", name)

	common_site_config = [
		{"key": config.key, "value": config.value, "type": config.type}
		for config in rg.common_site_config_table
		if not config.internal
	]

	bench_config = frappe.parse_json(rg.bench_config)
	if bench_config.get("http_timeout"):
		bench_config = [
			frappe._dict(
				key="http_timeout",
				value=bench_config.get("http_timeout"),
				type="Number",
				internal=False,
			)
		]
	else:
		bench_config = []

	config = common_site_config + bench_config

	secret_keys = frappe.get_all("Site Config Key", filters={"type": "Password"}, pluck="key")
	for c in config:
		if c["key"] in secret_keys:
			c["value"] = "*******"
			c["type"] = "Password"
	return config


@jingrow.whitelist()
@protected("Release Group")
def update_config(name, config):
	sanitized_common_site_config, sanitized_bench_config = [], []
	bench_config_keys = ["http_timeout"]

	config = frappe.parse_json(config)
	config = [frappe._dict(c) for c in config]

	for c in config:
		if c.key in get_client_blacklisted_keys():
			continue

		if frappe.db.exists("Site Config Key", c.key):
			c.type = frappe.db.get_value("Site Config Key", c.key, "type")
		format_config_value(name, c)

		if c.key in bench_config_keys:
			sanitized_bench_config.append(c)
		else:
			sanitized_common_site_config.append(c)

	rg: ReleaseGroup = frappe.get_pg("Release Group", name)
	rg.update_config_in_release_group(sanitized_common_site_config, sanitized_bench_config)
	rg.update_benches_config()
	return list(filter(lambda x: not x.internal, rg.common_site_config_table))


def format_config_value(group: str, c: frappe._dict):
	if c.type == "Number":
		c.value = flt(c.value)
	elif c.type == "Boolean":
		c.value = bool(sbool(c.value))
	elif c.type == "JSON":
		c.value = frappe.parse_json(c.value)
	elif c.type == "Password" and c.value == "*******":
		c.value = frappe.get_value("Site Config", {"key": c.key, "parent": group}, "value")


@jingrow.whitelist()
@protected("Release Group")
def dependencies(name: str):
	rg: ReleaseGroup = frappe.get_pg("Release Group", name)
	active_dependencies = [{"key": d.dependency, "value": d.version} for d in rg.dependencies]
	supported_dependencies = frappe.db.get_all(
		"Bench Dependency Version",
		{"supported_frappe_version": rg.version},
		["parent as `key`", "version as `value`"],
	)

	bench_dependencies = frappe.get_all("Bench Dependency", ["name", "title", "internal"])

	return {
		"active_dependencies": active_dependencies,
		"supported_dependencies": list(
			# deduplicate dependencies
			{d["value"]: d for d in supported_dependencies + active_dependencies}.values()
		),
		"dependency_title": {d["name"]: d["title"] for d in bench_dependencies},
		"internal_dependencies": [d["name"] for d in bench_dependencies if d["internal"]],
		"update_available": rg.dependency_update_pending,
	}


@jingrow.whitelist()
@protected("Release Group")
def update_dependencies(name: str, dependencies: str):
	dependencies = frappe.parse_json(dependencies)
	rg: ReleaseGroup = frappe.get_pg("Release Group", name)
	if len(rg.dependencies) != len(dependencies):
		frappe.throw("Need all required dependencies")
	if diff := set([d["key"] for d in dependencies]) - set(d.dependency for d in rg.dependencies):
		frappe.throw("Invalid dependencies: " + ", ".join(diff))
	for dep, new in zip(
		sorted(rg.dependencies, key=lambda x: x.dependency),
		sorted(dependencies, key=lambda x: x["key"]),
		strict=False,
	):
		if dep.dependency != new["key"]:
			frappe.throw(f"Invalid dependency: {new['key']}")
		if not re.match(r"^\d+\.\d+\.*\d*$", new["value"]):
			frappe.throw(f"Invalid version for {new['key']}")
		dep.version = new["value"]
	rg.save()


@jingrow.whitelist()
@protected("Release Group")
def apps(name):
	group = frappe.get_pg("Release Group", name)
	apps = []
	deployed_apps = frappe.db.get_all(
		"Bench",
		filters={"group": group.name, "status": ("!=", "Archived")},
		fields=["`tabBench App`.app"],
		pluck="app",
	)
	deployed_apps = unique(deployed_apps)
	updates = deploy_information(name)

	latest_bench = frappe.get_all(
		"Bench",
		filters={"group": group.name, "status": "Active"},
		order_by="creation desc",
		limit=1,
		pluck="name",
	)
	if latest_bench:
		latest_bench = latest_bench[0]
	else:
		latest_bench = None

	latest_deployed_apps = frappe.get_all(
		"Bench",
		filters={"name": latest_bench},
		fields=["`tabBench App`.app", "`tabBench App`.hash"],
	)

	for app in group.apps:
		source = frappe.get_pg("App Source", app.source)
		app = frappe.get_pg("App", app.app)
		update_available = updates["update_available"] and find(
			updates.apps, lambda x: x["app"] == app.name and x["update_available"]
		)

		latest_deployed_app = find(latest_deployed_apps, lambda x: x.app == app.name)
		hash = latest_deployed_app.hash if latest_deployed_app else None
		tag = get_app_tag(source.repository, source.repository_owner, hash)

		marketplace_app_title = frappe.db.get_value("Marketplace App", app.name, "title")
		app_title = marketplace_app_title or app.title

		apps.append(
			{
				"name": app.name,
				"frappe": app.frappe,
				"title": app_title,
				"branch": source.branch,
				"repository_url": source.repository_url,
				"repository": source.repository,
				"repository_owner": source.repository_owner,
				"tag": tag,
				"hash": hash,
				"deployed": app.name in deployed_apps,
				"update_available": bool(update_available),
				"last_github_poll_failed": source.last_github_poll_failed,
			}
		)
	return apps


@jingrow.whitelist()
@protected("Release Group")
def installable_apps(name):
	release_group = frappe.get_pg("Release Group", name)
	installed_apps = [app.app for app in release_group.apps]
	versions = options()["versions"]
	version = find(versions, lambda x: x["name"] == release_group.version)
	apps = version["apps"] if version else []
	return [app for app in apps if app["name"] not in installed_apps]


@jingrow.whitelist()
@protected("Release Group")
def all_apps(name):
	"""Return all apps in the marketplace that are not installed in the release group for adding new apps"""

	release_group = frappe.get_pg("Release Group", name)
	installed_apps = [app.app for app in release_group.apps]
	marketplace_apps = frappe.get_all(
		"Marketplace App",
		filters={"status": "Published", "app": ("not in", installed_apps)},
		fields=["name", "title", "image", "app"],
	)

	if not marketplace_apps:
		return []

	AppSource = frappe.qb.DocType("App Source")
	AppSourceVersion = frappe.qb.DocType("App Source Version")
	marketplace_app_sources = (
		frappe.qb.from_(AppSource)
		.left_join(AppSourceVersion)
		.on(AppSourceVersion.parent == AppSource.name)
		.select(
			AppSource.name,
			AppSource.branch,
			AppSource.repository,
			AppSource.repository_owner,
			AppSource.app,
			AppSourceVersion.version,
		)
		.where(
			(AppSource.app.isin([app.app for app in marketplace_apps]))
			& (AppSource.enabled == 1)
			& (AppSource.public == 1)
		)
	).run(as_dict=1)

	total_installs_by_app = get_total_installs_by_app()

	for app in marketplace_apps:
		app["sources"] = find_all(
			list(filter(lambda x: x.version == release_group.version, marketplace_app_sources)),
			lambda x: x.app == app.app,
		)
		# for fetching repo details for incompatible apps
		app_source = find(marketplace_app_sources, lambda x: x.app == app.app)
		app["repo"] = f"{app_source.repository_owner}/{app_source.repository}" if app_source else None
		app["total_installs"] = total_installs_by_app.get(app["name"], 0)

	return marketplace_apps


@jingrow.whitelist()
@protected("Release Group")
def fetch_latest_app_update(name, app):
	frappe.get_pg("Release Group", name).fetch_latest_app_update(app)


@jingrow.whitelist()
@protected("Release Group")
def add_app(name, source, app):
	add_apps(name, [{"app": app, "source": source}])


@jingrow.whitelist()
@protected("Release Group")
def add_apps(name, apps):
	release_group: "ReleaseGroup" = frappe.get_pg("Release Group", name)
	for app in apps:
		app_name, source = app.values()
		release_group.update_source(frappe._dict(name=source, app=app_name))


@jingrow.whitelist()
@protected("Release Group")
def remove_app(name, app):
	return frappe.get_pg("Release Group", name).remove_app(app)


@jingrow.whitelist()
@protected("Release Group")
def versions(name):
	Bench = frappe.qb.DocType("Bench")
	Server = frappe.qb.DocType("Server")
	deployed_versions = (
		frappe.qb.from_(Bench)
		.left_join(Server)
		.on(Server.name == Bench.server)
		.where((Bench.group == name) & (Bench.status != "Archived"))
		.groupby(Bench.name)
		.select(Bench.name, Bench.status, Bench.is_ssh_proxy_setup, Server.proxy_server)
		.orderby(Bench.creation, order=frappe.qb.desc)
		.run(as_dict=True)
	)

	rg_version = frappe.db.get_value("Release Group", name, "version")

	sites_in_group_details = frappe.db.get_all(
		"Site",
		filters={
			"group": name,
			"status": ("not in", ("Archived", "Suspended")),
			"is_standby": 0,
		},
		fields=["name", "status", "cluster", "plan", "creation", "bench"],
	)

	if sites_in_group_details:
		Cluster = frappe.qb.DocType("Cluster")
		cluster_data = (
			frappe.qb.from_(Cluster)
			.select(Cluster.name, Cluster.title, Cluster.image)
			.where(Cluster.name.isin([site.cluster for site in sites_in_group_details]))
			.run(as_dict=True)
		)

		Plan = frappe.qb.DocType("Site Plan")
		plan_data = (
			frappe.qb.from_(Plan)
			.select(Plan.name, Plan.plan_title, Plan.price_inr, Plan.price_usd)
			.where(Plan.name.isin([site.plan for site in sites_in_group_details]))
			.run(as_dict=True)
		)

		ResourceTag = frappe.qb.DocType("Resource Tag")
		tag_data = (
			frappe.qb.from_(ResourceTag)
			.select(ResourceTag.tag_name, ResourceTag.parent)
			.where(ResourceTag.parent.isin([site.name for site in sites_in_group_details]))
			.run(as_dict=True)
		)
	else:
		cluster_data = plan_data = tag_data = {}

	for version in deployed_versions:
		version.sites = find_all(sites_in_group_details, lambda x: x.bench == version.name)
		version.version = rg_version
		for site in version.sites:
			site.version = rg_version
			site.server_region_info = find(cluster_data, lambda x: x.name == site.cluster)
			site.plan = find(plan_data, lambda x: x.name == site.plan)
			tags = find_all(tag_data, lambda x: x.parent == site.name)
			site.tags = [tag.tag_name for tag in tags]

		version.deployed_on = frappe.db.get_value(
			"Agent Job",
			{"bench": version.name, "job_type": "New Bench", "status": "Success"},
			"end",
		)

	return deployed_versions


@jingrow.whitelist()
@protected("Bench")
def get_installed_apps_in_version(name):
	apps = frappe.db.get_all(
		"Bench App",
		{"parent": name},
		["name", "app", "hash", "source"],
		order_by="idx",
	)
	for app in apps:
		app.update(
			frappe.db.get_value(
				"App Source",
				app.source,
				("branch", "repository", "repository_owner", "repository_url"),
				as_dict=1,
				cache=True,
			)
		)
		app.tag = get_app_tag(app.repository, app.repository_owner, app.hash)

	return apps


@jingrow.whitelist()
@protected("Bench")
def get_processes(name):
	bench: "Bench" = frappe.get_pg("Bench", name)
	if bench.status != "Active" and bench.status != "Broken":
		return []

	return bench.supervisorctl_status()


@jingrow.whitelist()
@protected("Release Group")
def candidates(filters=None, order_by=None, limit_start=None, limit_page_length=None):
	# TODO: Status is redundant here.
	result = frappe.get_all(
		"Deploy Candidate",
		["name", "creation", "status"],
		{"group": filters["group"], "status": ("!=", "Draft")},
		order_by=order_by or "creation desc",
		start=limit_start,
		limit=limit_page_length,
	)
	candidates = OrderedDict()
	for d in result:
		candidates.setdefault(d.name, {})
		candidates[d.name].update(d)
		dc_apps = frappe.get_all(
			"Deploy Candidate App",
			filters={"parent": d.name},
			pluck="app",
			order_by="creation desc",
		)
		candidates[d.name]["apps"] = dc_apps

	return candidates.values()


@jingrow.whitelist()
def candidate(name):
	if not name:
		return None

	candidate: DeployCandidate = frappe.get_pg("Deploy Candidate", name)
	jobs = []
	deploys = frappe.get_all("Deploy", {"candidate": name}, limit=1)
	if deploys:
		deploy = frappe.get_pg("Deploy", deploys[0].name)
		for bench in deploy.benches:
			if not bench.bench:
				continue
			job = frappe.get_all(
				"Agent Job",
				["name", "status", "end", "duration", "bench"],
				{"bench": bench.bench, "job_type": "New Bench"},
				limit=1,
			) or [{}]
			jobs.append(job[0])

	# Taking the latest Build for that Candidate
	build_name = frappe.get_value("Deploy Candidate Build", {"deploy_candidate": name})
	build: DeployCandidateBuild = frappe.get_pg("Deploy Candidate Build", build_name)

	return {
		"name": candidate.name,
		"status": build.status,
		"creation": build.creation,
		"deployed": False,
		"build_steps": build.build_steps,
		"build_start": build.build_start,
		"build_end": build.build_end,
		"build_duration": build.build_duration,
		"apps": build.candidate.apps,
		"jobs": jobs,
	}


@jingrow.whitelist()
@protected("Release Group")
def deploy_information(name):
	rg: ReleaseGroup = frappe.get_pg("Release Group", name)
	return rg.deploy_information()


@jingrow.whitelist()
@protected("Release Group")
def deploy(name, apps):
	team = get_current_team(True)
	rg: ReleaseGroup = frappe.get_pg("Release Group", name)

	if rg.team != team.name:
		frappe.throw("Bench can only be deployed by the bench owner", exc=frappe.PermissionError)

	if rg.deploy_in_progress:
		frappe.throw("A deploy for this bench is already in progress")

	candidate = rg.create_deploy_candidate(apps)
	deploy_candidate_build = candidate.schedule_build_and_deploy()

	return deploy_candidate_build["name"]


@jingrow.whitelist()
@protected("Release Group")
def deploy_and_update(
	name: str,
	apps: list,
	sites: list | None = None,
	run_will_fail_check: bool = True,
):
	# Returns name of the Deploy Candidate that is running the build
	return get_bench_update(
		name,
		apps,
		sites,
		False,
	).deploy(run_will_fail_check)


@jingrow.whitelist()
@protected("Release Group")
def update_inplace(
	name: str,
	apps: list,
	sites: list,
):
	# Returns name of the Agent Job name that runs the inplace update
	return get_bench_update(
		name,
		apps,
		sites,
		True,
	).update_inplace()


@jingrow.whitelist()
@protected("Release Group")
def create_deploy_candidate(name, apps_to_ignore=None):
	apps_to_ignore = [] if apps_to_ignore is None else apps_to_ignore
	rg: ReleaseGroup = frappe.get_pg("Release Group", name)
	return rg.create_deploy_candidate(apps_to_ignore)


@jingrow.whitelist()
@protected("Release Group")
def jobs(filters=None, order_by=None, limit_start=None, limit_page_length=None):
	benches = frappe.get_all("Bench", {"group": filters["name"]}, pluck="name")
	if benches:
		jobs = frappe.get_all(
			"Agent Job",
			fields=["name", "job_type", "creation", "status", "start", "end", "duration"],
			filters={"bench": ("in", benches)},
			order_by=order_by or "creation desc",
			start=limit_start,
			limit=limit_page_length,
			ignore_ifnull=True,
		)
		for job in jobs:
			job["status"] = "Pending" if job["status"] == "Undelivered" else job["status"]
	else:
		jobs = []
	return jobs


@jingrow.whitelist()
@protected("Release Group")
def running_jobs(name):
	benches = frappe.get_all("Bench", {"group": name}, pluck="name")
	jobs = frappe.get_all(
		"Agent Job",
		filters={"status": ("in", ("Pending", "Running")), "bench": ("in", benches)},
	)
	return [job_detail(job.name) for job in jobs]


@jingrow.whitelist()
@protected("Release Group")
def recent_deploys(name):
	return frappe.get_all(
		"Deploy Candidate",
		["name", "creation"],
		{"group": name, "status": ("!=", "Draft")},
		order_by="creation desc",
		limit=3,
	)


@jingrow.whitelist()
@protected("Release Group")
def change_branch(name: str, app: str, to_branch: str):
	"""Switch to `to_branch` for `app` in release group `name`"""
	rg: ReleaseGroup = frappe.get_pg("Release Group", name)
	rg.change_app_branch(app, to_branch)


@jingrow.whitelist()
@protected("Release Group")
def branch_list(name: str, app: str) -> list[dict]:
	"""Return a list of git branches available for the `app`"""
	rg: ReleaseGroup = frappe.get_pg("Release Group", name)
	app_source = rg.get_app_source(app)

	installation_id = app_source.github_installation_id
	repo_owner = app_source.repository_owner
	repo_name = app_source.repository

	marketplace_app = frappe.get_all("Marketplace App", filters={"app": app}, pluck="name", limit=1)

	if marketplace_app and app_source.public and (not belongs_to_current_team(marketplace_app[0])):
		return get_branches_for_marketplace_app(app, marketplace_app[0], app_source)

	return branches(repo_owner, repo_name, installation_id)


def get_branches_for_marketplace_app(app: str, marketplace_app: str, app_source: AppSource) -> list[dict]:
	"""Return list of branches allowed for this `marketplace` app"""
	branch_set = set()
	marketplace_app = frappe.get_pg("Marketplace App", marketplace_app)

	for marketplace_app_source in marketplace_app.sources:
		app_source = frappe.get_pg("App Source", marketplace_app_source.source)
		branch_set.add(app_source.branch)

	# Also, append public source branches
	repo_owner = app_source.repository_owner
	repo_name = app_source.repository

	public_app_sources = frappe.get_all(
		"App Source",
		filters={
			"app": app,
			"repository_owner": repo_owner,
			"repository": repo_name,
			"public": True,
		},
		pluck="branch",
	)
	branch_set.update(public_app_sources)

	branch_list = sorted(list(branch_set))
	return [{"name": b} for b in branch_list]


def belongs_to_current_team(app: str) -> bool:
	"""Does the Marketplace App `app` belong to current team"""
	current_team = get_current_team()
	marketplace_app = frappe.get_pg("Marketplace App", app)

	return marketplace_app.team == current_team


@jingrow.whitelist()
@protected("Release Group")
def regions(name):
	rg = frappe.get_pg("Release Group", name)
	cluster_names = rg.get_clusters()
	return frappe.get_all(
		"Cluster", fields=["name", "title", "image"], filters={"name": ("in", cluster_names)}
	)


@jingrow.whitelist()
@protected("Release Group")
def available_regions(name):
	rg = frappe.get_pg("Release Group", name)
	cluster_names = rg.get_clusters()
	return Cluster.get_all_for_new_bench({"name": ("not in", cluster_names)})


@jingrow.whitelist()
@protected("Release Group")
def add_region(name, region):
	frappe.get_pg("Release Group", name).add_region(region)


@jingrow.whitelist()
@protected("Release Group")
def archive(name):
	benches = frappe.get_all("Bench", filters={"group": name, "status": "Active"}, pluck="name")

	for bench in benches:
		frappe.get_pg("Bench", bench).archive()

	group = frappe.get_pg("Release Group", name)
	new_name = f"{group.title}.archived"
	group.title = append_number_if_name_exists("Release Group", new_name, "title", separator=".")
	group.enabled = 0
	group.save()


@jingrow.whitelist()
@protected("Bench")
def restart(name):
	frappe.get_pg("Bench", name).restart()


@jingrow.whitelist()
@protected("Bench")
def rebuild(name):
	frappe.get_pg("Bench", name).rebuild()


@jingrow.whitelist()
@protected("Bench")
def update(name):
	frappe.get_pg("Bench", name).update_all_sites()


@jingrow.whitelist()
@protected("Release Group")
def update_all_sites(name):
	benches = frappe.get_all("Bench", {"group": name, "status": "Active"})
	for bench in benches:
		frappe.get_cached_pg("Bench", bench).update_all_sites()


@jingrow.whitelist()
@protected("Release Group")
def logs(name, bench):
	from press.agent import AgentRequestSkippedException

	if frappe.db.get_value("Bench", bench, "group") != name:
		return []

	try:
		return frappe.get_pg("Bench", bench).server_logs
	except AgentRequestSkippedException:
		return []


@jingrow.whitelist()
@protected("Release Group")
def log(name, bench, log):
	if frappe.db.get_value("Bench", bench, "group") != name:
		frappe.throw(f"Release Group name {name} does not match Bench Release Group")
	return frappe.get_pg("Bench", bench).get_server_log(log)


@jingrow.whitelist()
@protected("Release Group")
def certificate(name):
	return frappe.get_pg("Release Group", name).get_certificate()


@jingrow.whitelist()
@protected("Release Group")
def generate_certificate(name):
	return frappe.get_pg("Release Group", name).generate_certificate()


@jingrow.whitelist()
@protected("Release Group")
def get_title_and_creation(name):
	result = frappe.db.get_value("Release Group", name, ["title", "creation"], as_dict=True)
	server = frappe.get_all(
		"Release Group Server", {"parent": name}, pluck="server", order_by="idx asc", limit=1
	)[0]
	result["team"] = frappe.db.get_value("Server", server, "team")
	return result


@jingrow.whitelist()
@protected("Release Group")
def rename(name, title):
	return frappe.db.set_value("Release Group", name, "title", title)


@jingrow.whitelist()
@protected("Release Group")
def apply_patch(release_group: str, app: str, patch_config: dict) -> list[str]:
	team = get_current_team()

	return create_app_patch(
		release_group,
		app,
		team,
		patch_config,
	)


@jingrow.whitelist()
@protected("Release Group")
def fail_build(dn: str):
	failed = fail_remote_job(dn)

	if not failed:
		frappe.throw("No running job found!")


@jingrow.whitelist()
@protected("Release Group")
def fail_and_redeploy(name: str, dc_name: str):
	res = fail_and_redeploy_build(dc_name)

	# If failed error is True
	if res.get("error"):
		return None

	# New Deploy Candidate name
	return res.get("message")


@jingrow.whitelist()
@protected("Release Group")
def show_app_versions(name: str, dc_name: str) -> dict[str, str]:
	"""Get app versions from the deploy candidate"""
	candidate = frappe.db.get_value("Deploy Candidate Build", dc_name, "deploy_candidate")
	deploy_candidate: "DeployCandidate" = frappe.get_cached_pg("Deploy Candidate", candidate)
	app_sources = frappe.db.get_all(
		"App Source",
		{"name": ("IN", [app.source for app in deploy_candidate.apps])},
		["name", "branch", "repository", "repository_owner", "repository_url"],
	)
	sources = {
		item["name"]: {
			"branch": item["branch"],
			"repository_url": item["repository_url"],
			"repository": item["repository"],
			"repository_owner": item["repository_owner"],
		}
		for item in app_sources
	}

	return [
		{
			"name": app.app,
			"hash": app.hash[:7],
			"branch": sources.get(app.source).get("branch"),
			"repository": sources.get(app.source).get("repository"),
			"repository_owner": sources.get(app.source).get("repository_owner"),
			"repository_url": sources.get(app.source).get("repository_url"),
		}
		for app in deploy_candidate.apps
	]


@jingrow.whitelist()
@protected("Release Group")
def redeploy(name: str, dc_name: str) -> str:
	response = redeploy_candidate(dc_name)

	if response["error"]:
		frappe.throw("Unable to redeploy this build!", frappe.ValidationError)

	return response["message"]


@jingrow.whitelist(allow_guest=True)
def confirm_bench_transfer(key: str):
	from frappe import _

	if frappe.session.user == "Guest":
		return frappe.respond_as_web_page(
			_("Not Permitted"),
			_("You need to be logged in to confirm the bench group transfer."),
			http_status_code=403,
			indicator_color="red",
			primary_action="/dashboard/login",
			primary_label=_("Login"),
		)

	if not isinstance(key, str):
		return frappe.respond_as_web_page(
			_("Not Permitted"),
			_("The link you are using is invalid."),
			http_status_code=403,
			indicator_color="red",
		)

	if team_change := frappe.db.get_value("Team Change", {"key": key}):
		team_change = frappe.get_pg("Team Change", team_change)
		to_team = team_change.to_team
		if not frappe.db.get_value(
			"Team Member", {"user": frappe.session.user, "parent": to_team, "parenttype": "Team"}
		):
			return frappe.respond_as_web_page(
				_("Not Permitted"),
				_("You are not a member of the team to which the site is being transferred."),
				http_status_code=403,
				indicator_color="red",
			)

		team_change.transfer_completed = True
		team_change.save()
		frappe.db.commit()

		frappe.response.type = "redirect"
		frappe.response.location = f"/dashboard/groups/{team_change.document_name}"
		return None

	return frappe.respond_as_web_page(
		_("Not Permitted"),
		_("The link you are using is invalid or expired."),
		http_status_code=403,
		indicator_color="red",
	)
