# Copyright (c) 2023, JINGROW
# MIT License. See license.txt

from __future__ import annotations

import inspect
import typing

import jingrow
from jingrow.client import set_value as _set_value
from jingrow.handler import run_pg_method as _run_pg_method
from jingrow.model import child_table_fields, default_fields
from jingrow.model.base_page import get_controller
from jingrow.utils import cstr
from pypika.queries import QueryBuilder

from jcloud.exceptions import TeamHeaderNotInRequestError
from jcloud.utils import has_role

if typing.TYPE_CHECKING:
	from jingrow.model.meta import Meta

ALLOWED_PAGETYPES = [
	"Site",
	"Site App",
	"Site Domain",
	"Site Backup",
	"Site Activity",
	"Site Config",
	"Site Plan",
	"Site Update",
	"Site Group Deploy",
	"Invoice",
	"Balance Transaction",
	"Stripe Payment Method",
	"Bench",
	"Bench App",
	"Bench Dependency Version",
	"Release Group",
	"Release Group App",
	"Release Group Dependency",
	"Cluster",
	"Jcloud Permission Group",
	"Jcloud Role",
	"Jcloud Role Permission",
	"Team",
	"Product Trial Request",
	"Deploy Candidate",
	"Deploy Candidate Difference",
	"Deploy Candidate Difference App",
	"Agent Job",
	"Agent Job Type",
	"Common Site Config",
	"Server",
	"Database Server",
	"Ansible Play",
	"Server Plan",
	"Release Group Variable",
	"Resource Tag",
	"Jcloud Tag",
	"Partner Approval Request",
	"Marketplace App",
	"Subscription",
	"Marketplace App Version",
	"Marketplace App Plan",
	"App Release",
	"Payout Order",
	"App Patch",
	"Product Trial",
	"Jcloud Notification",
	"User SSH Key",
	"Jingrow Version",
	"Dashboard Banner",
	"App Release Approval Request",
	"Jcloud Webhook",
	"SQL Playground Log",
	"Site Database User",
	"Jcloud Settings",
	"Mpesa Payment Record",
	"Jsite Server",
	"Jsite Domain",
	"Domain Owner",
]

ALLOWED_PAGETYPES_FOR_SUPPORT = [
	"Site",
	"Bench",
	"Release Group",
]

whitelisted_methods = set()


@jingrow.whitelist()
def get_list(
	pagetype: str,
	fields: list | None = None,
	filters: dict | None = None,
	order_by: str | None = None,
	start: int = 0,
	limit: int = 20,
	parent: str | None = None,
	debug: bool = False,
):
	if filters is None:
		filters = {}

	# these pagetypes doesn't have a team field to filter by but are used in get or run_pg_method
	if pagetype in ["Team", "User SSH Key"]:
		return []

	check_permissions(pagetype)
	valid_fields = validate_fields(pagetype, fields)
	valid_filters = validate_filters(pagetype, filters)

	meta = jingrow.get_meta(pagetype)
	if meta.istable and not (filters.get("parenttype") and filters.get("parent")):
		jingrow.throw("parenttype and parent are required to get child records")

	apply_team_filter = not (
		filters.get("skip_team_filter_for_system_user_and_support_agent")
		and (jingrow.local.system_user() or has_role("Jcloud Support Agent"))
	)
	if apply_team_filter and meta.has_field("team"):
		valid_filters.team = jingrow.local.team().name

	query = get_list_query(
		pagetype,
		meta,
		filters,
		valid_filters,
		valid_fields,
		start,
		limit,
		order_by,
	)
	filters = jingrow._dict(filters or {})
	list_args = dict(
		fields=fields,
		filters=filters,
		order_by=order_by,
		start=start,
		limit=limit,
		parent=parent,
		debug=debug,
	)
	query = apply_custom_filters(pagetype, query, **list_args)
	if isinstance(query, QueryBuilder):
		return query.run(as_dict=1, debug=debug)

	if isinstance(query, list):
		return query

	return []


def get_list_query(
	pagetype: str,
	meta: "Meta",
	filters: dict,
	valid_filters: jingrow._dict,
	valid_fields: list | None,
	start: int,
	limit: int,
	order_by: str | None,
):
	from jcloud.jcloud.pagetype.jcloud_role.jcloud_role import check_role_permissions

	query = jingrow.qb.get_query(
		pagetype,
		filters=valid_filters,
		fields=valid_fields,
		offset=start,
		limit=limit,
		order_by=order_by,
	)

	if meta.istable and jingrow.get_meta(filters.get("parenttype")).has_field("team"):
		ParentDocType = jingrow.qb.PageType(filters.get("parenttype"))
		ChildDocType = jingrow.qb.PageType(pagetype)

		query = (
			query.join(ParentDocType)
			.on(ParentDocType.name == ChildDocType.parent)
			.where(ParentDocType.team == jingrow.local.team().name)
		)

	if roles := check_role_permissions(pagetype):
		JcloudRolePermission = jingrow.qb.PageType("Jcloud Role Permission")
		QueriedDocType = jingrow.qb.PageType(pagetype)

		field = pagetype.lower().replace(" ", "_")
		query = (
			query.join(JcloudRolePermission)
			.on(JcloudRolePermission[field] == QueriedDocType.name & JcloudRolePermission.role.isin(roles))
			.distinct()
		)

	return query


@jingrow.whitelist()
def get(pagetype, name):
	from jcloud.jcloud.pagetype.jcloud_role.jcloud_role import check_role_permissions

	check_permissions(pagetype)
	try:
		pg = jingrow.get_pg(pagetype, name)
	except jingrow.DoesNotExistError:
		controller = get_controller(pagetype)
		if hasattr(controller, "on_not_found"):
			return controller.on_not_found(name)
		raise

	if (
		not (jingrow.local.system_user() or has_role("Jcloud Support Agent"))
		and jingrow.get_meta(pagetype).has_field("team")
		and pg.team != jingrow.local.team().name
	):
		raise_not_permitted()

	check_role_permissions(pagetype, name)

	fields = tuple(default_fields)
	if hasattr(pg, "dashboard_fields"):
		fields += tuple(pg.dashboard_fields)

	_pg = jingrow._dict()
	for fieldname in fields:
		_pg[fieldname] = pg.get(fieldname)

	if hasattr(pg, "get_pg"):
		result = pg.get_pg(_pg)
		if isinstance(result, dict):
			_pg.update(result)

	return _pg


@jingrow.whitelist(methods=["POST", "PUT"])
def insert(pg=None):
	if not pg or not pg.get("pagetype"):
		jingrow.throw(jingrow._("pg.pagetype is required"))

	check_permissions(pg.get("pagetype"))

	pg = jingrow._dict(pg)
	if jingrow.is_table(pg.pagetype):
		if not (pg.parenttype and pg.parent and pg.parentfield):
			jingrow.throw(jingrow._("Parenttype, Parent and Parentfield are required to insert a child record"))

		# inserting a child record
		parent = jingrow.get_pg(pg.parenttype, pg.parent)

		if jingrow.get_meta(parent.pagetype).has_field("team") and parent.team != jingrow.local.team().name:
			raise_not_permitted()

		parent.append(pg.parentfield, pg)
		parent.save()
		return get(parent.pagetype, parent.name)

	_pg = jingrow.get_pg(pg)

	if jingrow.get_meta(pg.pagetype).has_field("team"):
		if not _pg.team:
			# set team if not set
			_pg.team = jingrow.local.team().name
		if not jingrow.local.system_user():
			# don't allow dashboard user to set any other team
			_pg.team = jingrow.local.team().name
	_pg.insert()
	return get(_pg.pagetype, _pg.name)


@jingrow.whitelist(methods=["POST", "PUT"])
def set_value(pagetype: str, name: str, fieldname: dict | str, value: str | None = None):
	check_permissions(pagetype)
	check_document_access(pagetype, name)

	for field in fieldname:
		# fields mentioned in dashboard_fields are allowed to be set via set_value
		is_allowed_field(pagetype, field)

	_set_value(pagetype, name, fieldname, value)

	# jingrow set_value returns just the pg and not jcloud's overriden `get_pg`
	return get(pagetype, name)


@jingrow.whitelist(methods=["DELETE", "POST"])
def delete(pagetype: str, name: str):
	method = "delete"

	check_permissions(pagetype)
	check_document_access(pagetype, name)
	check_dashboard_actions(pagetype, name, method)

	_run_pg_method(dt=pagetype, dn=name, method=method, args=None)


@jingrow.whitelist()
def run_pg_method(dt: str, dn: str, method: str, args: dict | None = None):
	check_permissions(dt)
	check_document_access(dt, dn)
	check_dashboard_actions(dt, dn, method)

	_run_pg_method(
		dt=dt,
		dn=dn,
		method=method,
		args=fix_args(method, args),
	)
	jingrow.response.docs = [get(dt, dn)]


@jingrow.whitelist()
def search_link(
	pagetype: str,
	query: str | None = None,
	filters: dict | None = None,
	order_by: str | None = None,
	page_length: int | None = None,
):
	check_permissions(pagetype)
	if pagetype == "Team" and not jingrow.local.system_user():
		raise_not_permitted()

	meta = jingrow.get_meta(pagetype)
	PageType = jingrow.qb.PageType(pagetype)
	valid_filters = validate_filters(pagetype, filters)
	valid_fields = validate_fields(pagetype, ["name", meta.title_field or "name"])
	q = get_list_query(
		pagetype,
		meta,
		filters,
		valid_filters,
		valid_fields,
		0,
		page_length or 10,
		order_by or "modified desc",
	)
	q = q.select(PageType.name.as_("value"))
	if meta.title_field:
		q = q.select(PageType[meta.title_field].as_("label"))
	if meta.has_field("enabled"):
		q = q.where(PageType.enabled == 1)
	if meta.has_field("disabled"):
		q = q.where(PageType.disabled != 1)
	if meta.has_field("team") and (not jingrow.local.system_user() or 1):
		q = q.where(PageType.team == jingrow.local.team().name)
	if query:
		condition = PageType.name.like(f"%{query}%")
		if meta.title_field:
			condition = condition | PageType[meta.title_field].like(f"%{query}%")
		q = q.where(condition)
	return q.run(as_dict=1)


def check_document_access(pagetype: str, name: str):
	if jingrow.local.system_user():
		return

	if has_role("Jcloud Support Agent") and pagetype in ALLOWED_PAGETYPES_FOR_SUPPORT:
		return

	team = ""
	meta = jingrow.get_meta(pagetype)
	if meta.has_field("team"):
		team = jingrow.db.get_value(pagetype, name, "team")
	elif meta.has_field("bench"):
		bench = jingrow.db.get_value(pagetype, name, "bench")
		team = jingrow.db.get_value("Bench", bench, "team")
	elif meta.has_field("group"):
		group = jingrow.db.get_value(pagetype, name, "group")
		team = jingrow.db.get_value("Release Group", group, "team")
	else:
		return

	if team == jingrow.local.team().name:
		return

	raise_not_permitted()


def check_dashboard_actions(pagetype, name, method):
	pg = jingrow.get_pg(pagetype, name)
	method_obj = getattr(pg, method)
	fn = getattr(method_obj, "__func__", method_obj)

	if fn not in whitelisted_methods:
		raise_not_permitted()


def apply_custom_filters(pagetype, query, **list_args):
	"""Apply custom filters to query"""
	controller = get_controller(pagetype)
	if hasattr(controller, "get_list_query"):
		if inspect.getfullargspec(controller.get_list_query).varkw:
			return controller.get_list_query(query, **list_args)
		return controller.get_list_query(query)

	return query


def validate_filters(pagetype, filters):
	"""Filter filters based on permissions"""
	if not filters:
		filters = {}

	out = jingrow._dict()
	for fieldname, value in filters.items():
		if is_allowed_field(pagetype, fieldname):
			out[fieldname] = value

	return out


def validate_fields(pagetype, fields):
	"""Filter fields based on permissions"""
	if not fields:
		return fields

	filtered_fields = []
	for field in fields:
		if is_allowed_field(pagetype, field):
			filtered_fields.append(field)

	return filtered_fields


def is_allowed_field(pagetype, field):
	"""Check if field is valid"""
	if not field:
		return False

	controller = get_controller(pagetype)
	dashboard_fields = getattr(controller, "dashboard_fields", ())

	if field in dashboard_fields:
		return True

	if "." in field and is_allowed_linked_field(pagetype, field):
		return True

	if isinstance(field, dict) and is_allowed_table_field(pagetype, field):
		return True

	if field in [*default_fields, *child_table_fields]:
		return True

	return False


def is_allowed_linked_field(pagetype, field):
	linked_field = linked_field_fieldname = None
	if " as " in field:
		linked_field, _ = field.split(" as ")
	else:
		linked_field = field

	linked_field, linked_field_fieldname = linked_field.split(".")
	if not is_allowed_field(pagetype, linked_field):
		return False

	linked_field_pagetype = jingrow.get_meta(pagetype).get_field(linked_field).options
	if not is_allowed_field(linked_field_pagetype, linked_field_fieldname):
		return False

	return True


def is_allowed_table_field(pagetype, field):
	for table_fieldname, table_fields in field.items():
		if not is_allowed_field(pagetype, table_fieldname):
			return False

		table_pagetype = jingrow.get_meta(pagetype).get_field(table_fieldname).options
		for table_field in table_fields:
			if not is_allowed_field(table_pagetype, table_field):
				return False
	return True


def check_permissions(pagetype):
	if pagetype not in ALLOWED_PAGETYPES:
		raise_not_permitted()

	if not hasattr(jingrow.local, "team") or not jingrow.local.team():
		jingrow.throw(
			"current_team is not set. Use X-JCLOUD-TEAM header in the request to set it.",
			TeamHeaderNotInRequestError,
		)

	return True


def is_owned_by_team(pagetype, docname, raise_exception=True):
	if not jingrow.local.team():
		return False

	docname = cstr(docname)
	owned = jingrow.db.get_value(pagetype, docname, "team") == jingrow.local.team().name
	if not owned and raise_exception:
		raise_not_permitted()
	return owned


def raise_not_permitted():
	jingrow.throw("不允许", jingrow.PermissionError)


def dashboard_whitelist(allow_guest=False, xss_safe=False, methods=None):
	def wrapper(func):
		global whitelisted_methods

		decorated_func = jingrow.whitelist(allow_guest=allow_guest, xss_safe=xss_safe, methods=methods)(func)

		def inner(*args, **kwargs):
			return decorated_func(*args, **kwargs)

		whitelisted_methods.add(decorated_func)
		return decorated_func

	return wrapper


def fix_args(method, args):
	# This is a fixer function. Certain callers of `run_pg_method`
	# pass duplicates of the passed kwargs in the `args` arg.
	#
	# This causes "got multiple values for argument 'method'"
	if not isinstance(args, dict):
		return args

	# Even if it doesn't match it'll probably throw
	# down the call stack, but in that case it's unexpected
	# behavior and so it's better to error-out.
	if args.get("method") == method:
		del args["method"]

	return args
