# Copyright (c) 2015, JINGROW and Contributors
# License: MIT. See LICENSE

import copy
import json
import os
import re
import shutil
from pathlib import Path
from typing import TYPE_CHECKING, Union

import jingrow
from jingrow import _
from jingrow.cache_manager import clear_controller_cache, clear_user_cache
from jingrow.custom.pagetype.custom_field.custom_field import create_custom_field
from jingrow.custom.pagetype.property_setter.property_setter import make_property_setter
from jingrow.database import savepoint
from jingrow.database.schema import validate_column_length, validate_column_name
from jingrow.desk.notifications import delete_notification_count_for, get_filters_for
from jingrow.desk.utils import validate_route_conflict
from jingrow.model import (
	child_table_fields,
	data_field_options,
	default_fields,
	no_value_fields,
	table_fields,
)
from jingrow.model.base_page import get_controller
from jingrow.model.pagefield import supports_translation
from jingrow.model.page import Page
from jingrow.model.meta import Meta
from jingrow.modules import get_pg_path, make_boilerplate
from jingrow.modules.import_file import get_file_path
from jingrow.permissions import ALL_USER_ROLE, AUTOMATIC_ROLES, SYSTEM_USER_ROLE
from jingrow.query_builder.functions import Concat
from jingrow.utils import cint, flt, get_datetime, is_a_property, random_string
from jingrow.website.utils import clear_cache

if TYPE_CHECKING:
	from jingrow.custom.pagetype.customize_form.customize_form import CustomizeForm

DEPENDS_ON_PATTERN = re.compile(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+')
ILLEGAL_FIELDNAME_PATTERN = re.compile("""['",./%@()<>{}]""")
WHITESPACE_PADDING_PATTERN = re.compile(r"^[ \t\n\r]+|[ \t\n\r]+$", flags=re.ASCII)
START_WITH_LETTERS_PATTERN = re.compile(r"^(?![\W])[^\d_\s][\w -]+$", flags=re.ASCII)
FIELD_PATTERN = re.compile("{(.*?)}", flags=re.UNICODE)


class InvalidFieldNameError(jingrow.ValidationError):
	pass


class UniqueFieldnameError(jingrow.ValidationError):
	pass


class IllegalMandatoryError(jingrow.ValidationError):
	pass


class PagetypeLinkError(jingrow.ValidationError):
	pass


class WrongOptionsPagetypeLinkError(jingrow.ValidationError):
	pass


class HiddenAndMandatoryWithoutDefaultError(jingrow.ValidationError):
	pass


class NonUniqueError(jingrow.ValidationError):
	pass


class CannotIndexedError(jingrow.ValidationError):
	pass


class CannotCreateStandardPagetypeError(jingrow.ValidationError):
	pass


form_grid_templates = {"fields": "templates/form_grid/fields.html"}


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

	from typing import TYPE_CHECKING

	if TYPE_CHECKING:
		from jingrow.core.pagetype.pagefield.pagefield import PageField
		from jingrow.core.pagetype.pageperm.pageperm import PagePerm
		from jingrow.core.pagetype.pagetype_action.pagetype_action import PageTypeAction
		from jingrow.core.pagetype.pagetype_link.pagetype_link import PageTypeLink
		from jingrow.core.pagetype.pagetype_state.pagetype_state import PageTypeState
		from jingrow.types import DF

		actions: DF.Table[PageTypeAction]
		allow_auto_repeat: DF.Check
		allow_copy: DF.Check
		allow_events_in_timeline: DF.Check
		allow_guest_to_view: DF.Check
		allow_import: DF.Check
		allow_rename: DF.Check
		autoname: DF.Data | None
		beta: DF.Check
		color: DF.Data | None
		custom: DF.Check
		default_email_template: DF.Link | None
		default_print_format: DF.Data | None
		default_view: DF.Literal[None]
		description: DF.SmallText | None
		detail_template: DF.Link | None
		document_type: DF.Literal["", "Document", "Setup", "System", "Other"]
		documentation: DF.Data | None
		editable_grid: DF.Check
		email_append_to: DF.Check
		engine: DF.Literal["InnoDB", "MyISAM"]
		fields: DF.Table[PageField]
		force_re_route_to_default_view: DF.Check
		grid_page_length: DF.Int
		has_web_view: DF.Check
		hide_id_in_list: DF.Check
		hide_toolbar: DF.Check
		icon: DF.Data | None
		image_field: DF.Data | None
		in_create: DF.Check
		index_web_pages_for_search: DF.Check
		is_calendar_and_gantt: DF.Check
		is_published_field: DF.Data | None
		is_submittable: DF.Check
		is_tree: DF.Check
		is_virtual: DF.Check
		issingle: DF.Check
		istable: DF.Check
		links: DF.Table[PageTypeLink]
		list_template: DF.Link | None
		make_attachments_public: DF.Check
		max_attachments: DF.Int
		migration_hash: DF.Data | None
		module: DF.Link
		naming_rule: DF.Literal["", "Set by user", "Autoincrement", "By fieldname", "By \"Naming Series\" field", "Expression", "Expression (old style)", "Random", "By script"]
		nsm_parent_field: DF.Data | None
		package: DF.Link | None
		permissions: DF.Table[PagePerm]
		queue_in_background: DF.Check
		quick_entry: DF.Check
		read_only: DF.Check
		restrict_to_domain: DF.Link | None
		route: DF.Data | None
		row_format: DF.Literal["Dynamic", "Compressed"]
		search_fields: DF.Data | None
		sender_field: DF.Data | None
		sender_name_field: DF.Data | None
		show_name_in_global_search: DF.Check
		show_owner_in_list: DF.Check
		show_preview_popup: DF.Check
		show_title_field_in_link: DF.Check
		sort_field: DF.Data | None
		sort_order: DF.Literal["ASC", "DESC"]
		states: DF.Table[PageTypeState]
		subject_field: DF.Data | None
		timeline_field: DF.Data | None
		title_field: DF.Data | None
		track_changes: DF.Check
		track_seen: DF.Check
		track_views: DF.Check
		translated_pagetype: DF.Check
		website_search_field: DF.Data | None
	# end: auto-generated types

	def validate(self):
		"""Validate PageType before saving.

		- Check if developer mode is set.
		- Validate series
		- Check fieldnames (duplication etc)
		- Clear permission table for child tables
		- Set default permissions if not set
		- Add `amended_from` and `amended_by` if Amendable
		- Add custom field `auto_repeat` if Repeatable
		- Check if links point to valid fieldnames"""

		self.check_developer_mode()

		self.validate_name()

		self.set_defaults_for_single_and_table()
		self.set_default_permissions()
		self.set_defaults_for_autoincremented()
		self.scrub_field_names()
		self.set_default_in_list_view()
		self.set_default_translatable()
		validate_series(self)
		self.set("can_change_name_type", validate_autoincrement_autoname(self))
		self.validate_document_type()
		validate_fields(self)
		self.check_indexing_for_dashboard_links()
		if not self.istable:
			validate_permissions(self)

		self.make_amendable()
		self.make_repeatable()
		self.validate_nestedset()
		self.validate_child_table()
		self.validate_website()
		self.validate_virtual_pagetype_methods()
		self.ensure_minimum_max_attachment_limit()
		validate_links_table_fieldnames(self)

		if not self.is_new():
			self.before_update = jingrow.get_pg("PageType", self.name)
			self.setup_fields_to_fetch()
			self.validate_field_name_conflicts()

		check_email_append_to(self)

		if self.default_print_format and not self.custom:
			jingrow.throw(_("Standard PageType cannot have default print format, use Customize Form"))

	def validate_field_name_conflicts(self):
		"""Check if field names dont conflict with controller properties and methods"""
		core_pagetypes = [
			"Custom PagePerm",
			"PagePerm",
			"Custom Field",
			"Customize Form Field",
			"Web Form Field",
			"PageField",
		]

		if self.name in core_pagetypes:
			return

		try:
			controller = get_controller(self.name)
		except ImportError:
			controller = Page

		available_objects = {x for x in dir(controller) if isinstance(x, str)}
		property_set = {x for x in available_objects if is_a_property(getattr(controller, x, None))}
		method_set = {
			x for x in available_objects if x not in property_set and callable(getattr(controller, x, None))
		}

		for pagefield in self.get("fields") or []:
			if pagefield.fieldtype in no_value_fields:
				continue

			conflict_type = None
			field = pagefield.fieldname
			field_label = pagefield.label or pagefield.fieldname

			if pagefield.fieldname in method_set:
				conflict_type = "controller method"
			if pagefield.fieldname in property_set and not pagefield.is_virtual:
				conflict_type = "class property"

			if conflict_type:
				jingrow.throw(
					_("Fieldname '{0}' conflicting with a {1} of the name {2} in {3}").format(
						field_label, conflict_type, field, self.name
					)
				)

	def set_defaults_for_single_and_table(self):
		if self.issingle:
			self.allow_import = 0
			self.is_submittable = 0
			self.istable = 0

		elif self.istable:
			self.allow_import = 0
			self.permissions = []

	def set_default_permissions(self):
		# Only set default permissions for non-child, non-single tables
		if self.istable or self.issingle:
			return

		if not (self.permissions and len(self.permissions)):
			self.append(
				"permissions",
				{
					"role": "System Manager",
					"read": 1,
					"write": 1,
					"create": 1,
					"delete": 1,
					"print": 1,
					"email": 1,
					"report": 1,
					"export": 1,
					"share": 1,
				},
			)

	def set_defaults_for_autoincremented(self):
		if self.autoname and self.autoname == "autoincrement":
			self.allow_rename = 0

	def set_default_in_list_view(self):
		"""Set default in-list-view for first 4 mandatory fields"""
		not_allowed_in_list_view = get_fields_not_allowed_in_list_view(self.meta)

		if not [d.fieldname for d in self.fields if d.in_list_view]:
			cnt = 0
			for d in self.fields:
				if d.reqd and not d.hidden and d.fieldtype not in not_allowed_in_list_view:
					d.in_list_view = 1
					cnt += 1
					if cnt == 4:
						break

	def set_default_translatable(self):
		"""Ensure that non-translatable never will be translatable"""
		for d in self.fields:
			if d.translatable and not supports_translation(d.fieldtype):
				d.translatable = 0

	def check_indexing_for_dashboard_links(self):
		"""Enable indexing for outgoing links used in dashboard"""
		for d in self.fields:
			if d.fieldtype == "Link" and not (d.unique or d.search_index):
				referred_as_link = jingrow.db.exists(
					"PageType Link",
					{"parent": d.options, "link_pagetype": self.name, "link_fieldname": d.fieldname},
				)
				if not referred_as_link:
					continue

				jingrow.msgprint(
					_("{0} should be indexed because it's referred in dashboard connections").format(
						_(d.label, context=d.parent)
					),
					alert=True,
					indicator="orange",
				)

	def check_developer_mode(self):
		"""Throw exception if not developer mode or via patch"""
		if jingrow.flags.in_patch or jingrow.flags.in_test:
			return

		if not jingrow.conf.get("developer_mode") and not self.custom:
			jingrow.throw(
				_("Not in Developer Mode! Set in site_config.json or make 'Custom' PageType."),
				CannotCreateStandardPagetypeError,
			)

		if self.is_virtual and self.custom:
			jingrow.throw(_("Not allowed to create custom Virtual PageType."), CannotCreateStandardPagetypeError)

		if jingrow.conf.developer_mode and not self.owner:
			self.owner = "Administrator"
			self.modified_by = "Administrator"

	def setup_fields_to_fetch(self):
		"""Setup query to update values for newly set fetch values"""
		try:
			old_meta = jingrow.get_meta(jingrow.get_pg("PageType", self.name), cached=False)
			old_fields_to_fetch = [df.fieldname for df in old_meta.get_fields_to_fetch()]
		except jingrow.DoesNotExistError:
			old_fields_to_fetch = []

		new_meta = jingrow.get_meta(self, cached=False)

		self.flags.update_fields_to_fetch_queries = []

		new_fields_to_fetch = [df for df in new_meta.get_fields_to_fetch()]

		if set(old_fields_to_fetch) != {df.fieldname for df in new_fields_to_fetch}:
			for df in new_fields_to_fetch:
				if df.fieldname not in old_fields_to_fetch:
					link_fieldname, source_fieldname = df.fetch_from.split(".", 1)
					if not source_fieldname:
						continue  # Invalid expression
					link_df = new_meta.get_field(link_fieldname)

					if jingrow.db.db_type == "postgres":
						update_query = """
							UPDATE `tab{pagetype}`
							SET `{fieldname}` = source.`{source_fieldname}`
							FROM `tab{link_pagetype}` as source
							WHERE `{link_fieldname}` = source.name
							AND ifnull(`{fieldname}`, '')=''
						"""
					else:
						update_query = """
							UPDATE `tab{pagetype}` as target
							INNER JOIN `tab{link_pagetype}` as source
							ON `target`.`{link_fieldname}` = `source`.`name`
							SET `target`.`{fieldname}` = `source`.`{source_fieldname}`
							WHERE ifnull(`target`.`{fieldname}`, '')=""
						"""

					self.flags.update_fields_to_fetch_queries.append(
						update_query.format(
							link_pagetype=link_df.options,
							source_fieldname=source_fieldname,
							pagetype=self.name,
							fieldname=df.fieldname,
							link_fieldname=link_fieldname,
						)
					)

	def update_fields_to_fetch(self):
		"""Update fetch values based on queries setup"""
		if self.flags.update_fields_to_fetch_queries and not self.issingle:
			for query in self.flags.update_fields_to_fetch_queries:
				jingrow.db.sql(query)

	def validate_document_type(self):
		if self.document_type == "Transaction":
			self.document_type = "Page"
		if self.document_type == "Master":
			self.document_type = "Setup"

	def validate_website(self):
		"""Ensure that website generator has field 'route'"""
		if self.route:
			self.route = self.route.strip("/")

		if self.has_web_view:
			# route field must be present
			if "route" not in [d.fieldname for d in self.fields]:
				jingrow.throw(_('Field "route" is mandatory for Web Views'), title="Missing Field")

			# clear website cache
			clear_cache()

	def validate_virtual_pagetype_methods(self):
		if not self.get("is_virtual") or self.is_new():
			return

		from jingrow.model.virtual_pagetype import validate_controller

		validate_controller(self.name)

	def ensure_minimum_max_attachment_limit(self):
		"""Ensure that max_attachments is *at least* bigger than number of attach fields."""
		from jingrow.model import attachment_fieldtypes

		if not self.max_attachments:
			return

		total_attach_fields = len([d for d in self.fields if d.fieldtype in attachment_fieldtypes])
		if total_attach_fields > self.max_attachments:
			self.max_attachments = total_attach_fields
			field_label = jingrow.bold(self.meta.get_field("max_attachments").label)
			jingrow.msgprint(
				_("Number of attachment fields are more than {}, limit updated to {}.").format(
					field_label, total_attach_fields
				),
				title=_("Insufficient attachment limit"),
				alert=True,
			)

	def change_modified_of_parent(self):
		"""Change the timestamp of parent PageType if the current one is a child to clear caches."""
		if jingrow.flags.in_import:
			return
		parent_list = jingrow.get_all(
			"PageField", "parent", dict(fieldtype=["in", jingrow.model.table_fields], options=self.name)
		)
		for p in parent_list:
			jingrow.db.set_value("PageType", p.parent, {})

	def scrub_field_names(self):
		"""Sluggify fieldnames if not set from Label."""
		restricted = (
			"name",
			"parent",
			"creation",
			"owner",
			"modified",
			"modified_by",
			"parentfield",
			"parenttype",
			"file_list",
			"flags",
			"pagestatus",
		)
		for d in self.get("fields"):
			if d.fieldtype:
				if not getattr(d, "fieldname", None):
					if d.label:
						d.fieldname = d.label.strip().lower().replace(" ", "_").strip("?")
						if d.fieldname in restricted:
							d.fieldname = d.fieldname + "1"
						if d.fieldtype == "Section Break":
							d.fieldname = d.fieldname + "_section"
						elif d.fieldtype == "Column Break":
							d.fieldname = d.fieldname + "_column"
						elif d.fieldtype == "Tab Break":
							d.fieldname = d.fieldname + "_tab"
					elif d.fieldtype in ("Section Break", "Column Break", "Tab Break"):
						d.fieldname = d.fieldtype.lower().replace(" ", "_") + "_" + str(random_string(4))
					else:
						jingrow.throw(
							_("Row #{}: Fieldname is required").format(d.idx), title="Missing Fieldname"
						)
				else:
					if d.fieldname in restricted:
						jingrow.throw(
							_("Fieldname {0} is restricted").format(d.fieldname), InvalidFieldNameError
						)
				d.fieldname = ILLEGAL_FIELDNAME_PATTERN.sub("", d.fieldname)

				# fieldnames should be lowercase
				d.fieldname = d.fieldname.lower()

			# unique is automatically an index
			if d.unique:
				d.search_index = 0

	def on_update(self):
		"""Update database schema, make controller templates if `custom` is not set and clear cache."""

		if self.get("can_change_name_type"):
			self.setup_autoincrement_and_sequence()

		try:
			jingrow.db.updatedb(self.name, Meta(self))
		except Exception as e:
			print(f"\n\nThere was an issue while migrating the PageType: {self.name}\n")
			raise e

		self.change_modified_of_parent()
		make_module_and_roles(self)

		self.update_fields_to_fetch()

		allow_pagetype_export = (
			not self.custom
			and not jingrow.flags.in_import
			and (jingrow.conf.developer_mode or jingrow.flags.allow_pagetype_export)
		)
		if allow_pagetype_export:
			self.export_pg()
			self.make_controller_template()
			self.set_base_class_for_controller()
			self.export_types_to_controller()

		# update index
		if not self.custom:
			self.run_module_method("on_pagetype_update")
			if self.flags.in_insert:
				self.run_module_method("after_pagetype_insert")

		self.sync_pagetype_layouts()
		delete_notification_count_for(pagetype=self.name)

		jingrow.clear_cache(pagetype=self.name)

		# clear user cache so that on the next reload this pagetype is included in boot
		clear_user_cache(jingrow.session.user)

		if not jingrow.flags.in_install and hasattr(self, "before_update"):
			self.sync_global_search()

		clear_linked_pagetype_cache()

	@savepoint(catch=Exception)
	def sync_pagetype_layouts(self):
		"""Sync Pagetype Layout"""
		pagetype_layouts = jingrow.get_all(
			"PageType Layout", filters={"document_type": self.name}, pluck="name", ignore_ddl=True
		)
		for layout in pagetype_layouts:
			layout_pg = jingrow.get_pg("PageType Layout", layout)
			layout_pg.sync_fields()
			layout_pg.save()

	def setup_autoincrement_and_sequence(self):
		"""Changes name type and makes sequence on change (if required)"""

		name_type = f"varchar({jingrow.db.VARCHAR_LEN})"

		if self.autoname == "autoincrement":
			name_type = "bigint"
			jingrow.db.create_sequence(self.name, check_not_exists=True)

		change_name_column_type(self.name, name_type)

	def sync_global_search(self):
		"""If global search settings are changed, rebuild search properties for this table"""
		global_search_fields_before_update = [
			d.fieldname for d in self.before_update.fields if d.in_global_search
		]
		if self.before_update.show_name_in_global_search:
			global_search_fields_before_update.append("name")

		global_search_fields_after_update = [d.fieldname for d in self.fields if d.in_global_search]
		if self.show_name_in_global_search:
			global_search_fields_after_update.append("name")

		if set(global_search_fields_before_update) != set(global_search_fields_after_update):
			now = (not jingrow.request) or jingrow.flags.in_test or jingrow.flags.in_install
			jingrow.enqueue("jingrow.utils.global_search.rebuild_for_pagetype", now=now, pagetype=self.name)

	def set_base_class_for_controller(self):
		"""If PageType.has_web_view has been changed, updates the controller class and import
		from `WebsiteGenertor` to `Page` or viceversa"""

		if not self.has_value_changed("has_web_view"):
			return

		despaced_name = self.name.replace(" ", "_")
		scrubbed_name = jingrow.scrub(self.name)
		scrubbed_module = jingrow.scrub(self.module)
		controller_path = jingrow.get_module_path(
			scrubbed_module, "pagetype", scrubbed_name, f"{scrubbed_name}.py"
		)

		document_cls_tag = f"class {despaced_name}(Page)"
		document_import_tag = "from jingrow.model.page import Page"
		website_generator_cls_tag = f"class {despaced_name}(WebsiteGenerator)"
		website_generator_import_tag = "from jingrow.website.website_generator import WebsiteGenerator"

		with open(controller_path) as f:
			code = f.read()
		updated_code = code

		is_website_generator_class = all(
			[website_generator_cls_tag in code, website_generator_import_tag in code]
		)

		if self.has_web_view and not is_website_generator_class:
			updated_code = updated_code.replace(document_import_tag, website_generator_import_tag).replace(
				document_cls_tag, website_generator_cls_tag
			)
		elif not self.has_web_view and is_website_generator_class:
			updated_code = updated_code.replace(website_generator_import_tag, document_import_tag).replace(
				website_generator_cls_tag, document_cls_tag
			)

		if updated_code != code:
			with open(controller_path, "w") as f:
				f.write(updated_code)

	def run_module_method(self, method):
		from jingrow.modules import load_pagetype_module

		module = load_pagetype_module(self.name, self.module)
		if hasattr(module, method):
			getattr(module, method)()

	def before_rename(self, old, new, merge=False):
		"""Throw exception if merge. PageTypes cannot be merged."""
		if not self.custom and jingrow.session.user != "Administrator":
			jingrow.throw(_("PageType can only be renamed by Administrator"))

		self.check_developer_mode()
		self.validate_name(new)

		if merge:
			jingrow.throw(_("PageType can not be merged"))

	def after_rename(self, old, new, merge=False):
		"""Change table name using `RENAME TABLE` if table exists. Or update
		`pagetype` property for Single type."""

		if self.issingle:
			jingrow.db.sql("""update tabSingles set pagetype=%s where pagetype=%s""", (new, old))
			jingrow.db.sql(
				"""update tabSingles set value=%s
				where pagetype=%s and field='name' and value = %s""",
				(new, new, old),
			)
		else:
			jingrow.db.rename_table(old, new)
			jingrow.db.commit()

		# Do not rename and move files and folders for custom pagetype
		if not self.custom:
			if not jingrow.flags.in_patch:
				self.rename_files_and_folders(old, new)

			clear_controller_cache(old)

	def after_delete(self):
		if not self.custom:
			clear_controller_cache(self.name)

	def rename_files_and_folders(self, old, new):
		# move files
		new_path = get_pg_path(self.module, "pagetype", new)
		old_path = get_pg_path(self.module, "pagetype", old)
		shutil.move(old_path, new_path)

		# rename files
		for fname in os.listdir(new_path):
			if jingrow.scrub(old) in fname:
				old_file_name = os.path.join(new_path, fname)
				new_file_name = os.path.join(new_path, fname.replace(jingrow.scrub(old), jingrow.scrub(new)))
				shutil.move(old_file_name, new_file_name)

		self.rename_inside_controller(new, old, new_path)
		jingrow.msgprint(_("Renamed files and replaced code in controllers, please check!"))

	def rename_inside_controller(self, new, old, new_path):
		for fname in ("{}.js", "{}.py", "{}_list.js", "{}_calendar.js", "test_{}.py", "test_{}.js"):
			fname = os.path.join(new_path, fname.format(jingrow.scrub(new)))
			if os.path.exists(fname):
				with open(fname) as f:
					code = f.read()
				with open(fname, "w") as f:
					if fname.endswith(".js"):
						file_content = code.replace(old, new)  # replace str with full str (js controllers)

					elif fname.endswith(".py"):
						file_content = code.replace(
							jingrow.scrub(old), jingrow.scrub(new)
						)  # replace str with _ (py imports)
						file_content = file_content.replace(
							old.replace(" ", ""), new.replace(" ", "")
						)  # replace str (py controllers)

					f.write(file_content)

		# updating json file with new name
		pagetype_json_path = os.path.join(new_path, f"{jingrow.scrub(new)}.json")
		current_data = jingrow.get_file_json(pagetype_json_path)
		current_data["name"] = new

		with open(pagetype_json_path, "w") as f:
			json.dump(current_data, f, indent=1)

	def before_reload(self):
		"""Preserve naming series changes in Property Setter."""
		if not (self.issingle and self.istable):
			self.preserve_naming_series_options_in_property_setter()

	def preserve_naming_series_options_in_property_setter(self):
		"""Preserve naming_series as property setter if it does not exist"""
		naming_series = self.get("fields", {"fieldname": "naming_series"})

		if not naming_series:
			return

		# check if atleast 1 record exists
		if not (
			jingrow.db.table_exists(self.name)
			and jingrow.get_all(self.name, fields=["name"], limit=1, as_list=True)
		):
			return

		existing_property_setter = jingrow.db.get_value(
			"Property Setter", {"pg_type": self.name, "property": "options", "field_name": "naming_series"}
		)

		if not existing_property_setter:
			make_property_setter(
				self.name,
				"naming_series",
				"options",
				naming_series[0].options,
				"Text",
				validate_fields_for_pagetype=False,
			)
			if naming_series[0].default:
				make_property_setter(
					self.name,
					"naming_series",
					"default",
					naming_series[0].default,
					"Text",
					validate_fields_for_pagetype=False,
				)

	def before_export(self, docdict):
		# remove null and empty fields
		def remove_null_fields(o):
			to_remove = []
			for attr, value in o.items():
				if isinstance(value, list):
					for v in value:
						remove_null_fields(v)
				elif not value:
					to_remove.append(attr)

			for attr in to_remove:
				del o[attr]

		remove_null_fields(docdict)

		# retain order of 'fields' table and change order in 'field_order'
		docdict["field_order"] = [f.fieldname for f in self.fields]

		if self.custom:
			return

		path = get_file_path(self.module, "PageType", self.name)
		if os.path.exists(path):
			try:
				with open(path) as txtfile:
					olddoc = json.loads(txtfile.read())

				old_field_names = [f["fieldname"] for f in olddoc.get("fields", [])]
				if old_field_names:
					new_field_dicts = []
					remaining_field_names = [f.fieldname for f in self.fields]

					for fieldname in old_field_names:
						field_dict = [f for f in docdict["fields"] if f["fieldname"] == fieldname]
						if field_dict:
							new_field_dicts.append(field_dict[0])
							if fieldname in remaining_field_names:
								remaining_field_names.remove(fieldname)

					for fieldname in remaining_field_names:
						field_dict = [f for f in docdict["fields"] if f["fieldname"] == fieldname]
						new_field_dicts.append(field_dict[0])

					docdict["fields"] = new_field_dicts
			except ValueError:
				pass

	@staticmethod
	def prepare_for_import(docdict):
		# set order of fields from field_order
		if docdict.get("field_order"):
			new_field_dicts = []
			remaining_field_names = [f["fieldname"] for f in docdict.get("fields", [])]

			for fieldname in docdict.get("field_order"):
				field_dict = [f for f in docdict.get("fields", []) if f["fieldname"] == fieldname]
				if field_dict:
					new_field_dicts.append(field_dict[0])
					if fieldname in remaining_field_names:
						remaining_field_names.remove(fieldname)

			for fieldname in remaining_field_names:
				field_dict = [f for f in docdict.get("fields", []) if f["fieldname"] == fieldname]
				new_field_dicts.append(field_dict[0])

			docdict["fields"] = new_field_dicts

		if "field_order" in docdict:
			del docdict["field_order"]

	def export_pg(self):
		"""Export to standard folder `[module]/pagetype/[name]/[name].json`."""
		from jingrow.modules.export_file import export_to_files

		export_to_files(record_list=[["PageType", self.name]], create_init=True)

	def make_controller_template(self):
		"""Make boilerplate controller template."""
		make_boilerplate("controller._py", self)

		if not self.istable:
			make_boilerplate("test_controller._py", self.as_dict())
			make_boilerplate("controller.js", self.as_dict())
			# make_boilerplate("controller_list.js", self.as_dict())

		if self.has_web_view:
			templates_path = jingrow.get_module_path(
				jingrow.scrub(self.module), "pagetype", jingrow.scrub(self.name), "templates"
			)
			if not os.path.exists(templates_path):
				os.makedirs(templates_path)
			make_boilerplate("templates/controller.html", self.as_dict())
			make_boilerplate("templates/controller_row.html", self.as_dict())

	def export_types_to_controller(self):
		from jingrow.modules.utils import get_module_app
		from jingrow.types.exporter import TypeExporter

		try:
			app = get_module_app(self.module)
		except jingrow.DoesNotExistError:
			return

		if any(jingrow.get_hooks("export_python_type_annotations", app_name=app)):
			TypeExporter(self).export_types()

	def make_amendable(self):
		"""If is_submittable is set, add amended_from pagefields."""
		if self.is_submittable:
			pagefield_exists = jingrow.get_all(
				"PageField", filters={"fieldname": "amended_from", "parent": self.name}, pluck="name", limit=1
			)
			if not pagefield_exists:
				self.append(
					"fields",
					{
						"label": "Amended From",
						"fieldtype": "Link",
						"fieldname": "amended_from",
						"options": self.name,
						"read_only": 1,
						"print_hide": 1,
						"no_copy": 1,
						"search_index": 1,
					},
				)

	def make_repeatable(self):
		"""If allow_auto_repeat is set, add auto_repeat custom field."""
		if self.allow_auto_repeat:
			if not jingrow.db.exists(
				"Custom Field", {"fieldname": "auto_repeat", "dt": self.name}
			) and not jingrow.db.exists("PageField", {"fieldname": "auto_repeat", "parent": self.name}):
				insert_after = self.fields[len(self.fields) - 1].fieldname
				df = dict(
					fieldname="auto_repeat",
					label="Auto Repeat",
					fieldtype="Link",
					options="Auto Repeat",
					insert_after=insert_after,
					read_only=1,
					no_copy=1,
					print_hide=1,
				)
				create_custom_field(self.name, df)

	def validate_nestedset(self):
		if not self.get("is_tree"):
			return
		self.add_nestedset_fields()

		if not self.nsm_parent_field:
			field_label = jingrow.bold(_("Parent Field (Tree)"))
			jingrow.throw(_("{0} is a mandatory field").format(field_label), jingrow.MandatoryError)

		# check if field is valid
		fieldnames = [df.fieldname for df in self.fields]
		if self.nsm_parent_field and self.nsm_parent_field not in fieldnames:
			jingrow.throw(_("Parent Field must be a valid fieldname"), InvalidFieldNameError)

	def add_nestedset_fields(self):
		"""If is_tree is set, add parent_field, lft, rgt, is_group fields."""
		fieldnames = [df.fieldname for df in self.fields]
		if "lft" in fieldnames:
			return

		self.append(
			"fields",
			{
				"label": "Left",
				"fieldtype": "Int",
				"fieldname": "lft",
				"read_only": 1,
				"hidden": 1,
				"no_copy": 1,
			},
		)

		self.append(
			"fields",
			{
				"label": "Right",
				"fieldtype": "Int",
				"fieldname": "rgt",
				"read_only": 1,
				"hidden": 1,
				"no_copy": 1,
			},
		)

		self.append("fields", {"label": "Is Group", "fieldtype": "Check", "fieldname": "is_group"})
		self.append(
			"fields",
			{"label": "Old Parent", "fieldtype": "Link", "options": self.name, "fieldname": "old_parent"},
		)

		parent_field_label = f"Parent {self.name}"
		parent_field_name = jingrow.scrub(parent_field_label)
		self.append(
			"fields",
			{
				"label": parent_field_label,
				"fieldtype": "Link",
				"options": self.name,
				"fieldname": parent_field_name,
				"ignore_user_permissions": 1,
			},
		)
		self.nsm_parent_field = parent_field_name

	def validate_child_table(self):
		if not self.get("istable") or self.is_new() or self.get("is_virtual"):
			# if the pagetype is not a child table then return
			# if the pagetype is a new pagetype and also a child table then
			# don't move forward as it will be handled via schema
			return

		self.add_child_table_fields()

	def add_child_table_fields(self):
		from jingrow.database.schema import add_column

		add_column(self.name, "parent", "Data")
		add_column(self.name, "parenttype", "Data")
		add_column(self.name, "parentfield", "Data")

	def get_max_idx(self):
		"""Returns the highest `idx`"""
		max_idx = jingrow.db.sql("""select max(idx) from `tabPageField` where parent = %s""", self.name)
		return max_idx and max_idx[0][0] or 0

	def validate_name(self, name=None):
		if not name:
			name = self.name

		# a Pagetype name is the tablename created in database
		# `tab<Pagetype Name>` the length of tablename is limited to 64 characters
		max_length = jingrow.db.MAX_COLUMN_LENGTH - 3
		if len(name) > max_length:
			# length(tab + <Pagetype Name>) should be equal to 64 characters hence pagetype should be 61 characters
			jingrow.throw(
				_("Pagetype name is limited to {0} characters ({1})").format(max_length, name),
				jingrow.NameError,
			)

		# a PageType name should not start or end with an empty space
		if WHITESPACE_PADDING_PATTERN.search(name):
			jingrow.throw(_("PageType's name should not start or end with whitespace"), jingrow.NameError)

		# a PageType's name should not start with a number or underscore
		# and should only contain letters, numbers, underscore, and hyphen
		if not START_WITH_LETTERS_PATTERN.match(name):
			jingrow.throw(
				_(
					"A PageType's name should start with a letter and can only "
					"consist of letters, numbers, spaces, underscores and hyphens"
				),
				jingrow.NameError,
				title="Invalid Name",
			)

		validate_route_conflict(self.pagetype, self.name)

	@jingrow.whitelist()
	def check_pending_migration(self) -> bool:
		"""Checks if all migrations are applied on pagetype."""
		if self.is_new() or self.custom:
			return

		file = Path(get_file_path(jingrow.scrub(self.module), self.pagetype, self.name))
		content = json.loads(file.read_text())
		if content.get("modified") and get_datetime(self.modified) < get_datetime(content.get("modified")):
			jingrow.msgprint(
				_(
					"This pagetype has pending migrations, run 'bench migrate' before modifying the pagetype to avoid losing changes."
				),
				alert=True,
				indicator="yellow",
			)
			return True


def validate_series(dt, autoname=None, name=None):
	"""Validate if `autoname` property is correctly set."""
	if not autoname:
		autoname = dt.autoname
	if not name:
		name = dt.name

	if not autoname and dt.get("fields", {"fieldname": "naming_series"}):
		dt.autoname = "naming_series:"
	elif dt.autoname and dt.autoname.startswith("naming_series:"):
		fieldname = dt.autoname.split("naming_series:", 1)[0] or "naming_series"
		if not dt.get("fields", {"fieldname": fieldname}):
			jingrow.throw(
				_("Fieldname called {0} must exist to enable autonaming").format(jingrow.bold(fieldname)),
				title=_("Field Missing"),
			)

	# validate field name if autoname field:fieldname is used
	# Create unique index on autoname field automatically.
	if autoname and autoname.startswith("field:"):
		field = autoname.split(":")[1]
		if not field or field not in [df.fieldname for df in dt.fields]:
			jingrow.throw(_("Invalid fieldname '{0}' in autoname").format(field))
		else:
			for df in dt.fields:
				if df.fieldname == field:
					df.unique = 1
					break

	if (
		autoname
		and (not autoname.startswith("field:"))
		and (not autoname.startswith("eval:"))
		and (autoname.lower() not in ("prompt", "hash"))
		and (not autoname.startswith("naming_series:"))
		and (not autoname.startswith("format:"))
	):
		prefix = autoname.split(".", 1)[0]
		pagetype = jingrow.qb.PageType("PageType")
		used_in = (
			jingrow.qb.from_(pagetype)
			.select(pagetype.name)
			.where(pagetype.autoname.like(Concat(prefix, ".%")))
			.where(pagetype.name != name)
		).run()
		if used_in:
			jingrow.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))


def validate_autoincrement_autoname(dt: Union[PageType, "CustomizeForm"]) -> bool:
	"""Checks if can pagetype can change to/from autoincrement autoname"""

	def get_autoname_before_save(dt: Union[PageType, "CustomizeForm"]) -> str:
		if dt.pagetype == "Customize Form":
			property_value = jingrow.db.get_value(
				"Property Setter", {"pg_type": dt.pg_type, "property": "autoname"}, "value"
			)
			# initially no property setter is set,
			# hence getting autoname value from the pagetype itself
			if not property_value:
				return jingrow.db.get_value("PageType", dt.pg_type, "autoname") or ""

			return property_value

		return getattr(dt.get_pg_before_save(), "autoname", "")

	if not dt.is_new():
		autoname_before_save = get_autoname_before_save(dt)
		is_autoname_autoincrement = dt.autoname == "autoincrement"

		if (
			is_autoname_autoincrement
			and autoname_before_save != "autoincrement"
			or (not is_autoname_autoincrement and autoname_before_save == "autoincrement")
		):
			if dt.pagetype == "Customize Form":
				jingrow.throw(_("Cannot change to/from autoincrement autoname in Customize Form"))

			if jingrow.get_meta(dt.name).issingle:
				return False

			if not jingrow.get_all(dt.name, limit=1):
				# allow changing the column type if there is no data
				return True

			jingrow.throw(
				_("Can only change to/from Autoincrement naming rule when there is no data in the pagetype")
			)

	return False


def change_name_column_type(pagetype_name: str, type: str) -> None:
	"""Changes name column type"""

	args = (
		(pagetype_name, "name", type, False, True)
		if (jingrow.db.db_type == "postgres")
		else (pagetype_name, "name", type, True)
	)

	jingrow.db.change_column_type(*args)


def validate_links_table_fieldnames(meta):
	"""Validate fieldnames in Links table"""
	if not meta.links or jingrow.flags.in_patch or jingrow.flags.in_fixtures or jingrow.flags.in_migrate:
		return

	fieldnames = tuple(field.fieldname for field in meta.fields)
	for index, link in enumerate(meta.links, 1):
		_test_connection_query(pagetype=link.link_pagetype, field=link.link_fieldname, idx=index)

		if not link.is_child_table:
			continue

		if not link.parent_pagetype:
			message = _("Page Links Row #{0}: Parent PageType is mandatory for internal links").format(
				index
			)
			jingrow.throw(message, jingrow.ValidationError, _("Parent Missing"))

		if not link.table_fieldname:
			message = _("Page Links Row #{0}: Table Fieldname is mandatory for internal links").format(
				index
			)
			jingrow.throw(message, jingrow.ValidationError, _("Table Fieldname Missing"))

		if meta.name == link.parent_pagetype:
			field_exists = link.table_fieldname in fieldnames
		else:
			field_exists = jingrow.get_meta(link.parent_pagetype).has_field(link.table_fieldname)

		if not field_exists:
			message = _("Page Links Row #{0}: Could not find field {1} in {2} PageType").format(
				index, jingrow.bold(link.table_fieldname), jingrow.bold(meta.name)
			)
			jingrow.throw(message, jingrow.ValidationError, _("Invalid Table Fieldname"))


def _test_connection_query(pagetype, field, idx):
	"""Make sure that connection can be queried.

	This function executes query similar to one that would be executed for
	finding count on dashboard and hence validates if fieldname/pagetype are
	correct.
	"""
	filters = get_filters_for(pagetype) or {}
	filters[field] = ""

	try:
		jingrow.get_all(pagetype, filters=filters, limit=1, distinct=True, ignore_ifnull=True)
	except Exception as e:
		jingrow.clear_last_message()
		msg = _("Page Links Row #{0}: Invalid pagetype or fieldname.").format(idx)
		msg += "<br>" + str(e)
		jingrow.throw(msg, InvalidFieldNameError)


def validate_fields_for_pagetype(pagetype):
	meta = jingrow.get_meta(pagetype, cached=False)
	validate_links_table_fieldnames(meta)
	validate_fields(meta)


# this is separate because it is also called via custom field
def validate_fields(meta: Meta):
	"""Validate pagetype fields. Checks
	1. There are no illegal characters in fieldnames
	2. If fieldnames are unique.
	3. Validate column length.
	4. Fields that do have database columns are not mandatory.
	5. `Link` and `Table` options are valid.
	6. **Hidden** and **Mandatory** are not set simultaneously.
	7. `Check` type field has default as 0 or 1.
	8. `Dynamic Links` are correctly defined.
	9. Precision is set in numeric fields and is between 1 & 6.
	10. Fold is not at the end (if set).
	11. `search_fields` are valid.
	12. `title_field` and title field pattern are valid.
	13. `unique` check is only valid for Data, Link and Read Only fieldtypes.
	14. `unique` cannot be checked if there exist non-unique values.

	:param meta: `jingrow.model.meta.Meta` object to check."""

	def check_illegal_characters(fieldname):
		validate_column_name(fieldname)

	def check_invalid_fieldnames(pagename, fieldname):
		if fieldname in Page._reserved_keywords:
			jingrow.throw(
				_("{0}: fieldname cannot be set to reserved keyword {1}").format(
					jingrow.bold(pagename),
					jingrow.bold(fieldname),
				),
				title=_("Invalid Fieldname"),
			)

	def check_unique_fieldname(pagename, fieldname):
		duplicates = list(
			filter(None, map(lambda df: df.fieldname == fieldname and str(df.idx) or None, fields))
		)
		if len(duplicates) > 1:
			jingrow.throw(
				_("{0}: Fieldname {1} appears multiple times in rows {2}").format(
					pagename, fieldname, ", ".join(duplicates)
				),
				UniqueFieldnameError,
			)

	def check_fieldname_length(fieldname):
		validate_column_length(fieldname)

	def check_illegal_mandatory(pagename, d):
		if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd:
			jingrow.throw(
				_("{0}: Field {1} of type {2} cannot be mandatory").format(pagename, d.label, d.fieldtype),
				IllegalMandatoryError,
			)

	def check_link_table_options(pagename, d):
		if jingrow.flags.in_patch or jingrow.flags.in_fixtures:
			return

		if d.fieldtype in ("Link", *table_fields):
			if not d.options:
				jingrow.throw(
					_("{0}: Options required for Link or Table type field {1} in row {2}").format(
						pagename, d.label, d.idx
					),
					PagetypeLinkError,
				)
			if d.options == "[Select]" or d.options == d.parent:
				return
			if d.options != d.parent:
				options = jingrow.db.get_value("PageType", d.options, "name")
				if not options:
					jingrow.throw(
						_("{0}: Options must be a valid PageType for field {1} in row {2}").format(
							pagename, d.label, d.idx
						),
						WrongOptionsPagetypeLinkError,
					)
				elif not (options == d.options):
					jingrow.throw(
						_("{0}: Options {1} must be the same as pagetype name {2} for the field {3}").format(
							pagename, d.options, options, d.label
						),
						PagetypeLinkError,
					)
				else:
					# fix case
					d.options = options

	def check_hidden_and_mandatory(pagename, d):
		if d.hidden and d.reqd and not d.default and not jingrow.flags.in_migrate:
			jingrow.throw(
				_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(
					pagename, d.label, d.idx
				),
				HiddenAndMandatoryWithoutDefaultError,
			)

	def check_width(d):
		if d.fieldtype == "Currency" and cint(d.width) < 100:
			jingrow.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))

	def check_in_list_view(is_table, d):
		if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
			property_label = "In Grid View" if is_table else "In List View"
			jingrow.throw(
				_("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx)
			)

	def check_in_global_search(d):
		if d.in_global_search and d.fieldtype in no_value_fields:
			jingrow.throw(
				_("'In Global Search' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx)
			)

	def check_dynamic_link_options(d):
		if d.fieldtype == "Dynamic Link":
			pagetype_pointer = list(filter(lambda df: df.fieldname == d.options, fields))
			if (
				not pagetype_pointer
				or (pagetype_pointer[0].fieldtype not in ("Link", "Select"))
				or (pagetype_pointer[0].fieldtype == "Link" and pagetype_pointer[0].options != "PageType")
			):
				jingrow.throw(
					_(
						"Options 'Dynamic Link' type of field must point to another Link Field with options as 'PageType'"
					)
				)

	def check_illegal_default(d):
		if d.fieldtype == "Check" and not d.default:
			d.default = "0"
		if d.fieldtype == "Check" and cint(d.default) not in (0, 1):
			jingrow.throw(
				_("Default for 'Check' type of field {0} must be either '0' or '1'").format(
					jingrow.bold(d.fieldname)
				)
			)
		if d.fieldtype == "Select" and d.default:
			if not d.options:
				jingrow.throw(
					_("Options for {0} must be set before setting the default value.").format(
						jingrow.bold(d.fieldname)
					)
				)
			elif d.default not in d.options.split("\n"):
				jingrow.throw(
					_("Default value for {0} must be in the list of options.").format(
						jingrow.bold(d.fieldname)
					)
				)

	def check_precision(d):
		if (
			d.fieldtype in ("Currency", "Float", "Percent")
			and d.precision is not None
			and not (1 <= cint(d.precision) <= 6)
		):
			jingrow.throw(_("Precision should be between 1 and 6"))

	def check_unique_and_text(pagename, d):
		if meta.is_virtual:
			return

		if meta.issingle:
			d.unique = 0
			d.search_index = 0

		if getattr(d, "unique", False):
			if d.fieldtype not in ("Data", "Link", "Read Only", "Int"):
				jingrow.throw(
					_("{0}: Fieldtype {1} for {2} cannot be unique").format(pagename, d.fieldtype, d.label),
					NonUniqueError,
				)

			if not d.get("__islocal") and jingrow.db.has_column(d.parent, d.fieldname):
				has_non_unique_values = jingrow.db.sql(
					f"""select `{d.fieldname}`, count(*)
					from `tab{d.parent}` where ifnull(`{d.fieldname}`, '') != ''
					group by `{d.fieldname}` having count(*) > 1 limit 1"""
				)

				if has_non_unique_values and has_non_unique_values[0][0]:
					jingrow.throw(
						_("{0}: Field '{1}' cannot be set as Unique as it has non-unique values").format(
							pagename, d.label
						),
						NonUniqueError,
					)

		if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"):
			jingrow.throw(
				_("{0}:Fieldtype {1} for {2} cannot be indexed").format(pagename, d.fieldtype, d.label),
				CannotIndexedError,
			)

	def check_fold(fields):
		fold_exists = False
		for i, f in enumerate(fields):
			if f.fieldtype == "Fold":
				if fold_exists:
					jingrow.throw(_("There can be only one Fold in a form"))
				fold_exists = True
				if i < len(fields) - 1:
					nxt = fields[i + 1]
					if nxt.fieldtype != "Section Break":
						jingrow.throw(_("Fold must come before a Section Break"))
				else:
					jingrow.throw(_("Fold can not be at the end of the form"))

	def check_search_fields(meta, fields):
		"""Throw exception if `search_fields` don't contain valid fields."""
		if not meta.search_fields:
			return

		# No value fields should not be included in search field
		search_fields = [field.strip() for field in (meta.search_fields or "").split(",")]
		fieldtype_mapper = {
			field.fieldname: field.fieldtype
			for field in filter(lambda field: field.fieldname in search_fields, fields)
		}

		for fieldname in search_fields:
			fieldname = fieldname.strip()
			if (fieldtype_mapper.get(fieldname) in no_value_fields) or (fieldname not in fieldname_list):
				jingrow.throw(_("Search field {0} is not valid").format(fieldname))

	def check_title_field(meta):
		"""Throw exception if `title_field` isn't a valid fieldname."""
		if not meta.get("title_field"):
			return

		if meta.title_field not in fieldname_list:
			jingrow.throw(_("Title field must be a valid fieldname"), InvalidFieldNameError)

		def _validate_title_field_pattern(pattern):
			if not pattern:
				return

			for fieldname in FIELD_PATTERN.findall(pattern):
				if fieldname.startswith("{"):
					# edge case when double curlies are used for escape
					continue

				if fieldname not in fieldname_list:
					jingrow.throw(
						_("{{{0}}} is not a valid fieldname pattern. It should be {{field_name}}.").format(
							fieldname
						),
						InvalidFieldNameError,
					)

		df = meta.get("fields", filters={"fieldname": meta.title_field})[0]
		if df:
			_validate_title_field_pattern(df.options)
			_validate_title_field_pattern(df.default)

	def check_image_field(meta):
		'''check image_field exists and is of type "Attach Image"'''
		if not meta.image_field:
			return

		df = meta.get("fields", {"fieldname": meta.image_field})
		if not df:
			jingrow.throw(_("Image field must be a valid fieldname"), InvalidFieldNameError)
		if df[0].fieldtype != "Attach Image":
			jingrow.throw(_("Image field must be of type Attach Image"), InvalidFieldNameError)

	def check_is_published_field(meta):
		if not meta.is_published_field:
			return

		if meta.is_published_field not in fieldname_list:
			jingrow.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError)

	def check_website_search_field(meta):
		if not meta.get("website_search_field"):
			return

		if meta.website_search_field not in fieldname_list:
			jingrow.throw(_("Website Search Field must be a valid fieldname"), InvalidFieldNameError)

		if "title" not in fieldname_list:
			jingrow.throw(
				_('Field "title" is mandatory if "Website Search Field" is set.'), title=_("Missing Field")
			)

	def check_timeline_field(meta):
		if not meta.timeline_field:
			return

		if meta.timeline_field not in fieldname_list:
			jingrow.throw(_("Timeline field must be a valid fieldname"), InvalidFieldNameError)

		df = meta.get("fields", {"fieldname": meta.timeline_field})[0]
		if df.fieldtype not in ("Link", "Dynamic Link"):
			jingrow.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError)

	def check_sort_field(meta):
		"""Validate that sort_field(s) is a valid field"""
		if meta.sort_field:
			sort_fields = [meta.sort_field]
			if "," in meta.sort_field:
				sort_fields = [d.split(maxsplit=1)[0] for d in meta.sort_field.split(",")]

			for fieldname in sort_fields:
				if fieldname not in (fieldname_list + list(default_fields) + list(child_table_fields)):
					jingrow.throw(
						_("Sort field {0} must be a valid fieldname").format(fieldname), InvalidFieldNameError
					)

	def check_illegal_depends_on_conditions(pagefield):
		"""assignment operation should not be allowed in the depends on condition."""
		depends_on_fields = [
			"depends_on",
			"collapsible_depends_on",
			"mandatory_depends_on",
			"read_only_depends_on",
		]
		for field in depends_on_fields:
			depends_on = pagefield.get(field, None)
			if depends_on and ("=" in depends_on) and DEPENDS_ON_PATTERN.match(depends_on):
				jingrow.throw(_("Invalid {0} condition").format(jingrow.unscrub(field)), jingrow.ValidationError)

	def check_table_multiselect_option(pagefield):
		"""check if the pagetype provided in Option has atleast 1 Link field"""
		if not pagefield.fieldtype == "Table MultiSelect":
			return

		pagetype = pagefield.options
		meta = jingrow.get_meta(pagetype)
		link_field = [df for df in meta.fields if df.fieldtype == "Link"]

		if not link_field:
			jingrow.throw(
				_(
					"PageType <b>{0}</b> provided for the field <b>{1}</b> must have atleast one Link field"
				).format(pagetype, pagefield.fieldname),
				jingrow.ValidationError,
			)

	def scrub_options_in_select(field):
		"""Strip options for whitespaces"""

		if field.fieldtype == "Select" and field.options is not None:
			options_list = []
			for i, option in enumerate(field.options.split("\n")):
				_option = option.strip()
				if i == 0 or _option:
					options_list.append(_option)
			field.options = "\n".join(options_list)

	def validate_fetch_from(field):
		if not field.get("fetch_from"):
			return

		field.fetch_from = field.fetch_from.strip()

		if "." not in field.fetch_from:
			return
		source_field, _target_field = field.fetch_from.split(".", maxsplit=1)

		if source_field == field.fieldname:
			msg = _(
				"{0} contains an invalid Fetch From expression, Fetch From can't be self-referential."
			).format(_(field.label, context=field.parent))
			jingrow.throw(msg, title=_("Recursive Fetch From"))

	def validate_data_field_type(pagefield):
		if pagefield.get("is_virtual"):
			return

		if pagefield.fieldtype == "Data" and not (pagefield.oldfieldtype and pagefield.oldfieldtype != "Data"):
			if pagefield.options and (pagefield.options not in data_field_options):
				df_str = jingrow.bold(_(pagefield.label, context=pagefield.parent))
				text_str = (
					_("{0} is an invalid Data field.").format(df_str)
					+ "<br>" * 2
					+ _("Only Options allowed for Data field are:")
					+ "<br>"
				)
				df_options_str = "<ul><li>" + "</li><li>".join(_(x) for x in data_field_options) + "</ul>"

				jingrow.msgprint(text_str + df_options_str, title="Invalid Data Field", alert=True)

	def check_child_table_option(pagefield):
		if jingrow.flags.in_fixtures:
			return
		if pagefield.fieldtype not in ["Table MultiSelect", "Table"]:
			return

		pagetype = pagefield.options
		child_pagetype_meta = jingrow.get_meta(pagetype)

		if not child_pagetype_meta.istable:
			jingrow.throw(
				_("Option {0} for field {1} is not a child table").format(
					jingrow.bold(pagetype), jingrow.bold(pagefield.fieldname)
				),
				title=_("Invalid Option"),
			)

		if not (meta.is_virtual == child_pagetype_meta.is_virtual):
			error_msg = " should be virtual." if meta.is_virtual else " cannot be virtual."
			jingrow.throw(
				_("Child Table {0} for field {1}" + error_msg).format(
					jingrow.bold(pagetype), jingrow.bold(pagefield.fieldname)
				),
				title=_("Invalid Option"),
			)

	def check_max_height(pagefield):
		if getattr(pagefield, "max_height", None) and (pagefield.max_height[-2:] not in ("px", "em")):
			jingrow.throw(f"Max for {jingrow.bold(pagefield.fieldname)} height must be in px, em, rem")

	def check_no_of_ratings(pagefield):
		if pagefield.fieldtype == "Rating":
			if pagefield.options and (int(pagefield.options) > 10 or int(pagefield.options) < 3):
				jingrow.throw(_("Options for Rating field can range from 3 to 10"))

	fields = meta.get("fields")
	fieldname_list = [d.fieldname for d in fields]

	not_allowed_in_list_view = get_fields_not_allowed_in_list_view(meta)

	for d in fields:
		if not d.permlevel:
			d.permlevel = 0
		if d.fieldtype not in table_fields:
			d.allow_bulk_edit = 0
		if not d.fieldname:
			d.fieldname = d.fieldname.lower().strip("?")

		check_illegal_characters(d.fieldname)
		check_invalid_fieldnames(meta.get("name"), d.fieldname)
		check_fieldname_length(d.fieldname)
		check_hidden_and_mandatory(meta.get("name"), d)
		check_unique_and_text(meta.get("name"), d)
		check_table_multiselect_option(d)
		scrub_options_in_select(d)
		validate_fetch_from(d)
		validate_data_field_type(d)

		if not jingrow.flags.in_migrate:
			check_unique_fieldname(meta.get("name"), d.fieldname)
			check_link_table_options(meta.get("name"), d)
			check_illegal_mandatory(meta.get("name"), d)
			check_dynamic_link_options(d)
			check_in_list_view(meta.get("istable"), d)
			check_in_global_search(d)
			check_illegal_depends_on_conditions(d)
			check_illegal_default(d)
			check_child_table_option(d)
			check_max_height(d)
			check_no_of_ratings(d)

	if not jingrow.flags.in_migrate:
		check_fold(fields)
		check_search_fields(meta, fields)
		check_title_field(meta)
		check_timeline_field(meta)
		check_is_published_field(meta)
		check_website_search_field(meta)
		check_sort_field(meta)
		check_image_field(meta)


def get_fields_not_allowed_in_list_view(meta) -> list[str]:
	not_allowed_in_list_view = list(copy.copy(no_value_fields))
	not_allowed_in_list_view.append("Attach Image")
	if meta.istable:
		not_allowed_in_list_view.remove("Button")
		not_allowed_in_list_view.remove("HTML")
	return not_allowed_in_list_view


def validate_permissions_for_pagetype(pagetype, for_remove=False, alert=False):
	"""Validates if permissions are set correctly."""
	pagetype = jingrow.get_pg("PageType", pagetype)
	validate_permissions(pagetype, for_remove, alert=alert)

	# save permissions
	for perm in pagetype.get("permissions"):
		perm.db_update()

	clear_permissions_cache(pagetype.name)


def clear_permissions_cache(pagetype):
	from jingrow.cache_manager import clear_user_cache

	jingrow.clear_cache(pagetype=pagetype)
	delete_notification_count_for(pagetype)

	clear_user_cache()


def validate_permissions(pagetype, for_remove=False, alert=False):
	permissions = pagetype.get("permissions")
	# Some PageTypes may not have permissions by default, don't show alert for them
	if not permissions and alert:
		jingrow.msgprint(_("No Permissions Specified"), alert=True, indicator="orange")
	issingle = issubmittable = isimportable = False
	if pagetype:
		issingle = cint(pagetype.issingle)
		issubmittable = cint(pagetype.is_submittable)
		isimportable = cint(pagetype.allow_import)

	def get_txt(d):
		return _("For {0} at level {1} in {2} in row {3}").format(d.role, d.permlevel, d.parent, d.idx)

	def check_atleast_one_set(d):
		if not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create:
			jingrow.throw(_("{0}: No basic permissions set").format(get_txt(d)))

	def check_double(d):
		has_similar = False
		similar_because_of = ""
		for p in permissions:
			if p.role == d.role and p.permlevel == d.permlevel and p != d:
				if p.if_owner == d.if_owner:
					similar_because_of = _("If Owner")
					has_similar = True
					break

		if has_similar:
			jingrow.throw(
				_("{0}: Only one rule allowed with the same Role, Level and {1}").format(
					get_txt(d), similar_because_of
				)
			)

	def check_level_zero_is_set(d):
		if cint(d.permlevel) > 0 and d.role not in (ALL_USER_ROLE, SYSTEM_USER_ROLE):
			has_zero_perm = False
			for p in permissions:
				if p.role == d.role and (p.permlevel or 0) == 0 and p != d:
					has_zero_perm = True
					break

			if not has_zero_perm:
				jingrow.throw(
					_("{0}: Permission at level 0 must be set before higher levels are set").format(
						get_txt(d)
					)
				)

			for invalid in ("create", "submit", "cancel", "amend"):
				if d.get(invalid):
					d.set(invalid, 0)

	def check_permission_dependency(d):
		if d.cancel and not d.submit:
			jingrow.throw(_("{0}: Cannot set Cancel without Submit").format(get_txt(d)))

		if (d.submit or d.cancel or d.amend) and not d.write:
			jingrow.throw(_("{0}: Cannot set Submit, Cancel, Amend without Write").format(get_txt(d)))
		if d.amend and not d.write:
			jingrow.throw(_("{0}: Cannot set Amend without Cancel").format(get_txt(d)))
		if d.get("import") and not d.create:
			jingrow.throw(_("{0}: Cannot set Import without Create").format(get_txt(d)))

	def remove_rights_for_single(d):
		if not issingle:
			return

		if d.report:
			jingrow.msgprint(_("Report cannot be set for Single types"))
			d.report = 0
			d.set("import", 0)
			d.set("export", 0)

	def check_if_submittable(d):
		if d.submit and not issubmittable:
			jingrow.throw(_("{0}: Cannot set Assign Submit if not Submittable").format(get_txt(d)))
		elif d.amend and not issubmittable:
			jingrow.throw(_("{0}: Cannot set Assign Amend if not Submittable").format(get_txt(d)))

	def check_if_importable(d):
		if d.get("import") and not isimportable:
			jingrow.throw(_("{0}: Cannot set import as {1} is not importable").format(get_txt(d), pagetype))

	def validate_permission_for_all_role(d):
		if jingrow.session.user == "Administrator":
			return

		if pagetype.custom:
			if d.role in AUTOMATIC_ROLES:
				jingrow.throw(
					_(
						"Row # {0}: Non administrator user can not set the role {1} to the custom pagetype"
					).format(d.idx, jingrow.bold(_(d.role))),
					title=_("Permissions Error"),
				)

			roles = [row.name for row in jingrow.get_all("Role", filters={"is_custom": 1})]

			if d.role in roles:
				jingrow.throw(
					_(
						"Row # {0}: Non administrator user can not set the role {1} to the custom pagetype"
					).format(d.idx, jingrow.bold(_(d.role))),
					title=_("Permissions Error"),
				)

	for d in permissions:
		if not d.permlevel:
			d.permlevel = 0
		check_atleast_one_set(d)
		if not for_remove:
			check_double(d)
			check_permission_dependency(d)
			check_if_submittable(d)
			check_if_importable(d)
		check_level_zero_is_set(d)
		remove_rights_for_single(d)
		validate_permission_for_all_role(d)


def make_module_and_roles(pg, perm_fieldname="permissions"):
	"""Make `Module Def` and `Role` records if already not made. Called while installing."""
	try:
		if (
			hasattr(pg, "restrict_to_domain")
			and pg.restrict_to_domain
			and not jingrow.db.exists("Domain", pg.restrict_to_domain)
		):
			jingrow.get_pg(dict(pagetype="Domain", domain=pg.restrict_to_domain)).insert()

		if "tabModule Def" in jingrow.db.get_tables() and not jingrow.db.exists("Module Def", pg.module):
			m = jingrow.get_pg({"pagetype": "Module Def", "module_name": pg.module})
			if jingrow.scrub(pg.module) in jingrow.local.module_app:
				m.app_name = jingrow.local.module_app[jingrow.scrub(pg.module)]
			else:
				m.app_name = "jingrow"
			m.flags.ignore_mandatory = m.flags.ignore_permissions = True
			if jingrow.flags.package:
				m.package = jingrow.flags.package.name
				m.custom = 1
			m.insert()

		roles = [p.role for p in pg.get("permissions") or []] + list(AUTOMATIC_ROLES)

		for role in list(set(roles)):
			if jingrow.db.table_exists("Role", cached=False) and not jingrow.db.exists("Role", role):
				r = jingrow.new_pg("Role")
				r.role_name = role
				r.desk_access = 1
				r.flags.ignore_mandatory = r.flags.ignore_permissions = True
				r.insert()
	except jingrow.DoesNotExistError:
		pass
	except jingrow.db.ProgrammingError as e:
		if jingrow.db.is_table_missing(e):
			pass
		else:
			raise


def check_fieldname_conflicts(pagefield):
	"""Checks if fieldname conflicts with methods or properties"""
	pg = jingrow.get_pg({"pagetype": pagefield.dt})
	available_objects = [x for x in dir(pg) if isinstance(x, str)]
	property_list = [x for x in available_objects if is_a_property(getattr(type(pg), x, None))]
	method_list = [x for x in available_objects if x not in property_list and callable(getattr(pg, x))]
	msg = _("Fieldname {0} conflicting with meta object").format(pagefield.fieldname)

	if pagefield.fieldname in method_list + property_list:
		jingrow.msgprint(msg, raise_exception=not pagefield.is_virtual)


def clear_linked_pagetype_cache():
	jingrow.cache.delete_value("linked_pagetypes_without_ignore_user_permissions_enabled")


def check_email_append_to(pg):
	if not hasattr(pg, "email_append_to") or not pg.email_append_to:
		return

	# Subject Field
	pg.subject_field = pg.subject_field.strip() if pg.subject_field else None
	subject_field = get_field(pg, pg.subject_field)

	if pg.subject_field and not subject_field:
		jingrow.throw(_("Select a valid Subject field for creating documents from Email"))

	if subject_field and subject_field.fieldtype not in [
		"Data",
		"Text",
		"Long Text",
		"Small Text",
		"Text Editor",
		"Jeditor",
	]:
		jingrow.throw(_("Subject Field type should be Data, Text, Long Text, Small Text, Text Editor"))

	# Sender Field is mandatory
	pg.sender_field = pg.sender_field.strip() if pg.sender_field else None
	sender_field = get_field(pg, pg.sender_field)

	if pg.sender_field and not sender_field:
		jingrow.throw(_("Select a valid Sender Field for creating documents from Email"))

	if not sender_field.options == "Email":
		jingrow.throw(_("Sender Field should have Email in options"))


def get_field(pg, fieldname):
	if not (pg or fieldname):
		return

	for field in pg.fields:
		if field.fieldname == fieldname:
			return field


@jingrow.whitelist()
def get_row_size_utilization(pagetype: str) -> float:
	"""Get row size utilization in percentage"""

	jingrow.has_permission("PageType", throw=True)
	try:
		return flt(jingrow.db.get_row_size(pagetype) / jingrow.db.MAX_ROW_SIZE_LIMIT * 100, 2)
	except Exception:
		return 0.0
