# Copyright (c) 2022, JINGROW and Contributors
# License: MIT. See LICENSE
from types import NoneType
from typing import TYPE_CHECKING

import jingrow
from jingrow import _, bold
from jingrow.model.page import Page
from jingrow.model.dynamic_links import get_dynamic_link_map
from jingrow.model.naming import validate_name
from jingrow.model.utils.user_settings import sync_user_settings, update_user_settings_data
from jingrow.query_builder import Field
from jingrow.utils.data import sbool
from jingrow.utils.password import rename_password
from jingrow.utils.scheduler import is_scheduler_inactive

if TYPE_CHECKING:
	from jingrow.model.meta import Meta


@jingrow.whitelist()
def update_document_title(
	*,
	pagetype: str,
	pagename: str,
	title: str | None = None,
	name: str | None = None,
	merge: bool = False,
	enqueue: bool = False,
	**kwargs,
) -> str:
	"""
	Update the name or title of a document. Returns `name` if document was renamed,
	`pagename` if renaming operation was queued.

	:param pagetype: PageType of the document
	:param pagename: Name of the document
	:param title: New Title of the document
	:param name: New Name of the document
	:param merge: Merge the current Page with the existing one if exists
	:param enqueue: Enqueue the rename operation, title is updated in current process
	"""

	# to maintain backwards API compatibility
	updated_title = kwargs.get("new_title") or title
	updated_name = kwargs.get("new_name") or name

	# TODO: omit this after runtime type checking (ref: http://git.jingrow.com/jingrow/jingrow/pull/14927)
	for obj in [pagename, updated_title, updated_name]:
		if not isinstance(obj, str | NoneType):
			jingrow.throw(f"{obj=} must be of type str or None")

	# handle bad API usages
	merge = sbool(merge)
	enqueue = sbool(enqueue)
	action_enqueued = enqueue and not is_scheduler_inactive()

	page = jingrow.get_page(pagetype, pagename)
	page.check_permission(permtype="write")

	title_field = page.meta.get_title_field()

	title_updated = updated_title and (title_field != "name") and (updated_title != page.get(title_field))
	name_updated = updated_name and (updated_name != page.name)

	queue = kwargs.get("queue") or "long"

	if name_updated:
		if action_enqueued:
			current_name = page.name

			# before_name hook may have PageType specific validations or transformations
			transformed_name = page.run_method("before_rename", current_name, updated_name, merge)
			if isinstance(transformed_name, dict):
				transformed_name = transformed_name.get("new")
			transformed_name = transformed_name or updated_name

			# run rename validations before queueing
			# use savepoints to avoid partial renames / commits
			validate_rename(
				pagetype=pagetype,
				old=current_name,
				new=transformed_name,
				meta=page.meta,
				merge=merge,
				save_point=True,
			)

			page.queue_action("rename", name=transformed_name, merge=merge, queue=queue, timeout=36000)
		else:
			page.rename(updated_name, merge=merge)

	if title_updated:
		if action_enqueued and name_updated:
			jingrow.enqueue(
				"jingrow.client.set_value",
				pagetype=page.pagetype,
				name=updated_name,
				fieldname=title_field,
				value=updated_title,
			)
		else:
			try:
				setattr(page, title_field, updated_title)
				page.save()
				jingrow.msgprint(_("Saved"), alert=True, indicator="green")
			except Exception as e:
				if jingrow.db.is_duplicate_entry(e):
					jingrow.throw(
						_("{0} {1} already exists").format(pagetype, jingrow.bold(pagename)),
						title=_("Duplicate Name"),
						exc=jingrow.DuplicateEntryError,
					)
				raise

	return page.name


def rename_page(
	pagetype: str | None = None,
	old: str | None = None,
	new: str | None = None,
	force: bool = False,
	merge: bool = False,
	ignore_permissions: bool = False,
	ignore_if_exists: bool = False,
	show_alert: bool = True,
	rebuild_search: bool = True,
	page: Page | None = None,
	validate: bool = True,
) -> str:
	"""Rename a page(dt, old) to page(dt, new) and update all linked fields of type "Link".

	page: Page object to be renamed.
	new: New name for the record. If None, and pagetype is specified, new name may be automatically generated via before_rename hooks.
	pagetype: PageType of the document. Not required if page is passed.
	old: Current name of the document. Not required if page is passed.
	force: Allow even if document is not allowed to be renamed.
	merge: Merge with existing document of new name.
	ignore_permissions: Ignore user permissions while renaming.
	ignore_if_exists: Don't raise exception if document with new name already exists. This will quietely overwrite the existing document.
	show_alert: Display alert if document is renamed successfully.
	rebuild_search: Rebuild linked pagetype search after renaming.
	validate: Validate before renaming. If False, it is assumed that the caller has already validated.
	"""
	old_usage_style = pagetype and old and new
	new_usage_style = page and new

	if not (new_usage_style or old_usage_style):
		raise TypeError(
			"{pagetype, old, new} or {page, new} are required arguments for jingrow.model.rename_page"
		)

	old = old or page.name
	pagetype = pagetype or page.pagetype
	force = sbool(force)
	merge = sbool(merge)
	meta = jingrow.get_meta(pagetype)

	if validate:
		old_page = page or jingrow.get_page(pagetype, old)
		out = old_page.run_method("before_rename", old, new, merge) or {}
		new = (out.get("new") or new) if isinstance(out, dict) else (out or new)
		new = validate_rename(
			pagetype=pagetype,
			old=old,
			new=new,
			meta=meta,
			merge=merge,
			force=force,
			ignore_permissions=ignore_permissions,
			ignore_if_exists=ignore_if_exists,
			old_page=old_page,
		)

	if not merge:
		rename_parent_and_child(pagetype, old, new, meta)
	else:
		update_assignments(old, new, pagetype)

	# update link fields' values
	link_fields = get_link_fields(pagetype)
	update_link_field_values(link_fields, old, new, pagetype)

	rename_dynamic_links(pagetype, old, new)

	# save the user settings in the db
	update_user_settings(old, new, link_fields)

	if pagetype == "PageType":
		rename_pagetype(pagetype, old, new)
		update_customizations(old, new)

	update_attachments(pagetype, old, new)

	rename_versions(pagetype, old, new)

	rename_eps_records(pagetype, old, new)

	# call after_rename
	new_page = jingrow.get_page(pagetype, new)

	if validate:
		# copy any flags if required
		new_page._local = getattr(old_page, "_local", None)

	new_page.run_method("after_rename", old, new, merge)

	if not merge:
		rename_password(pagetype, old, new)

	if merge:
		new_page.add_comment("Merge", _("merged {0} into {1}").format(jingrow.bold(old), jingrow.bold(new)))
	else:
		new_page.add_comment("Rename", _("renamed from {0} to {1}").format(jingrow.bold(old), jingrow.bold(new)))

	if merge:
		jingrow.delete_page(pagetype, old)

	new_page.clear_cache()
	jingrow.clear_cache()
	if rebuild_search:
		jingrow.enqueue("jingrow.utils.global_search.rebuild_for_pagetype", pagetype=pagetype)

	if show_alert:
		jingrow.msgprint(
			_("Page renamed from {0} to {1}").format(bold(old), bold(new)),
			alert=True,
			indicator="green",
		)

	return new


def update_assignments(old: str, new: str, pagetype: str) -> None:
	old_assignments = jingrow.parse_json(jingrow.db.get_value(pagetype, old, "_assign")) or []
	new_assignments = jingrow.parse_json(jingrow.db.get_value(pagetype, new, "_assign")) or []
	common_assignments = list(set(old_assignments).intersection(new_assignments))

	for user in common_assignments:
		# delete todos linked to old page
		todos = jingrow.get_all(
			"ToDo",
			{
				"owner": user,
				"reference_type": pagetype,
				"reference_name": old,
			},
			["name", "description"],
		)

		for todo in todos:
			jingrow.delete_page("ToDo", todo.name)

	unique_assignments = list(set(old_assignments + new_assignments))
	jingrow.db.set_value(pagetype, new, "_assign", jingrow.as_json(unique_assignments, indent=0))


def update_user_settings(old: str, new: str, link_fields: list[dict]) -> None:
	"""
	Update the user settings of all the linked pagetypes while renaming.
	"""

	# store the user settings data from the redis to db
	sync_user_settings()

	if not link_fields:
		return

	# find the user settings for the linked pagetypes
	linked_pagetypes = {d.parent for d in link_fields if not d.issingle}
	UserSettings = jingrow.qb.Table("__UserSettings")

	user_settings_details = (
		jingrow.qb.from_(UserSettings)
		.select("user", "pagetype", "data")
		.where(UserSettings.data.like(old) & UserSettings.pagetype.isin(linked_pagetypes))
		.run(as_dict=True)
	)

	# create the dict using the pagetype name as key and values as list of the user settings
	from collections import defaultdict

	user_settings_dict = defaultdict(list)
	for user_setting in user_settings_details:
		user_settings_dict[user_setting.pagetype].append(user_setting)

	# update the name in linked pagetype whose user settings exists
	for fields in link_fields:
		user_settings = user_settings_dict.get(fields.parent)
		if user_settings:
			for user_setting in user_settings:
				update_user_settings_data(user_setting, "value", old, new, "pagefield", fields.fieldname)
		else:
			continue


def update_customizations(old: str, new: str) -> None:
	jingrow.db.set_value("Custom PagePerm", {"parent": old}, "parent", new, update_modified=False)


def update_attachments(pagetype: str, old: str, new: str) -> None:
	if pagetype != "PageType":
		File = jingrow.qb.PageType("File")

		jingrow.qb.update(File).set(File.attached_to_name, new).where(
			(File.attached_to_name == old) & (File.attached_to_pagetype == pagetype)
		).run()


def rename_versions(pagetype: str, old: str, new: str) -> None:
	Version = jingrow.qb.PageType("Version")

	jingrow.qb.update(Version).set(Version.pagename, new).where(
		(Version.pagename == old) & (Version.ref_pagetype == pagetype)
	).run()


def rename_eps_records(pagetype: str, old: str, new: str) -> None:
	EPL = jingrow.qb.PageType("Energy Point Log")

	jingrow.qb.update(EPL).set(EPL.reference_name, new).where(
		(EPL.reference_pagetype == pagetype) & (EPL.reference_name == old)
	).run()


def rename_parent_and_child(pagetype: str, old: str, new: str, meta: "Meta") -> None:
	jingrow.qb.update(pagetype).set("name", new).where(Field("name") == old).run()

	update_autoname_field(pagetype, new, meta)
	update_child_pages(old, new, meta)


def update_autoname_field(pagetype: str, new: str, meta: "Meta") -> None:
	# update the value of the autoname field on rename of the pagename
	if meta.get("autoname"):
		field = meta.get("autoname").split(":")
		if field and field[0] == "field":
			jingrow.qb.update(pagetype).set(field[1], new).where(Field("name") == new).run()


def validate_rename(
	pagetype: str,
	old: str,
	new: str,
	meta: "Meta",
	merge: bool,
	force: bool = False,
	ignore_permissions: bool = False,
	ignore_if_exists: bool = False,
	save_point=False,
	old_page: Page | None = None,
) -> str:
	# using for update so that it gets locked and someone else cannot edit it while this rename is going on!
	if save_point:
		_SAVE_POINT = f"validate_rename_{jingrow.generate_hash(length=8)}"
		jingrow.db.savepoint(_SAVE_POINT)

	exists = jingrow.qb.from_(pagetype).where(Field("name") == new).for_update().select("name").run(pluck=True)
	exists = exists[0] if exists else None

	if not jingrow.db.exists(pagetype, old):
		jingrow.throw(_("Can't rename {0} to {1} because {0} doesn't exist.").format(old, new))

	if old == new:
		jingrow.throw(_("No changes made because old and new name are the same.").format(old, new))

	if exists and exists != new:
		# for fixing case, accents
		exists = None

	if merge and not exists:
		jingrow.throw(_("{0} {1} does not exist, select a new target to merge").format(pagetype, new))

	if not merge and exists and not ignore_if_exists:
		jingrow.throw(_("Another {0} with name {1} exists, select another name").format(pagetype, new))

	kwargs = {"pagetype": pagetype, "ptype": "write", "raise_exception": False}
	if old_page:
		kwargs["page"] = old_page

	if not (ignore_permissions or jingrow.permissions.has_permission(**kwargs)):
		jingrow.throw(_("You need write permission on {0} {1} to rename").format(pagetype, old))

	if merge:
		kwargs["page"] = jingrow.get_page(pagetype, new)
		if not (ignore_permissions or jingrow.permissions.has_permission(**kwargs)):
			jingrow.throw(_("You need write permission on {0} {1} to merge").format(pagetype, new))

	if not (force or ignore_permissions) and not meta.allow_rename:
		jingrow.throw(_("{0} not allowed to be renamed").format(_(pagetype)))

	# validate naming like it's done in page.py
	new = validate_name(pagetype, new)

	if save_point:
		jingrow.db.rollback(save_point=_SAVE_POINT)

	return new


def rename_pagetype(pagetype: str, old: str, new: str) -> None:
	# change options for fieldtype Table, Table MultiSelect and Link
	fields_with_options = ("Link", *jingrow.model.table_fields)

	for fieldtype in fields_with_options:
		update_options_for_fieldtype(fieldtype, old, new)

	# change parenttype for fieldtype Table
	update_parenttype_values(old, new)


def update_child_pages(old: str, new: str, meta: "Meta") -> None:
	# update "parent"
	for df in meta.get_table_fields():
		(
			jingrow.qb.update(df.options)
			.set("parent", new)
			.where((Field("parent") == old) & (Field("parenttype") == meta.name))
		).run()


def update_link_field_values(link_fields: list[dict], old: str, new: str, pagetype: str) -> None:
	for field in link_fields:
		if field["issingle"]:
			try:
				single_page = jingrow.get_page(field["parent"])
				if single_page.get(field["fieldname"]) == old:
					single_page.set(field["fieldname"], new)
					# update single pages using ORM rather then query
					# as single pages also sometimes sets defaults!
					single_page.flags.ignore_mandatory = True
					single_page.flags.ignore_links = True
					single_page.save(ignore_permissions=True)
			except ImportError:
				# fails in patches where the pagetype has been renamed
				# or no longer exists
				pass
		else:
			parent = field["parent"]
			pagefield = field["fieldname"]

			# Handles the case where one of the link fields belongs to
			# the PageType being renamed.
			# Here this field could have the current PageType as its value too.

			# In this case while updating link field value, the field's parent
			# or the current PageType table name hasn't been renamed yet,
			# so consider it's old name.
			if parent == new and pagetype == "PageType":
				parent = old

			jingrow.db.set_value(parent, {pagefield: old}, pagefield, new, update_modified=False)

		# update cached link_fields as per new
		if pagetype == "PageType" and field["parent"] == old:
			field["parent"] = new


def get_link_fields(pagetype: str) -> list[dict]:
	# get link fields from tabPageField
	if not jingrow.flags.link_fields:
		jingrow.flags.link_fields = {}

	if pagetype not in jingrow.flags.link_fields:
		dt = jingrow.qb.PageType("PageType")
		df = jingrow.qb.PageType("PageField")
		cf = jingrow.qb.PageType("Custom Field")
		ps = jingrow.qb.PageType("Property Setter")

		standard_fields_query = (
			jingrow.qb.from_(df)
			.inner_join(dt)
			.on(df.parent == dt.name)
			.select(df.parent, df.fieldname, dt.issingle.as_("issingle"))
			.where((df.options == pagetype) & (df.fieldtype == "Link"))
		)

		if jingrow.db.has_column("PageField", "is_virtual"):
			standard_fields_query = standard_fields_query.where(df.is_virtual == 0)

		virtual_pagetypes = []
		if jingrow.db.has_column("PageType", "is_virtual"):
			virtual_pagetypes = jingrow.get_all("PageType", {"is_virtual": 1}, pluck="name")
			standard_fields_query = standard_fields_query.where(dt.is_virtual == 0)

		standard_fields = standard_fields_query.run(as_dict=True)

		cf_issingle = jingrow.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle")
		custom_fields = (
			jingrow.qb.from_(cf)
			.select(cf.dt.as_("parent"), cf.fieldname, cf_issingle)
			.where((cf.options == pagetype) & (cf.fieldtype == "Link"))
		)
		if virtual_pagetypes:
			custom_fields = custom_fields.where(cf.dt.notin(virtual_pagetypes))
		custom_fields = custom_fields.run(as_dict=True)

		ps_issingle = jingrow.qb.from_(dt).select(dt.issingle).where(dt.name == ps.page_type).as_("issingle")
		property_setter_fields = (
			jingrow.qb.from_(ps)
			.select(ps.page_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle)
			.where((ps.property == "options") & (ps.value == pagetype) & (ps.field_name.notnull()))
		)
		if virtual_pagetypes:
			property_setter_fields = property_setter_fields.where(ps.page_type.notin(virtual_pagetypes))
		property_setter_fields = property_setter_fields.run(as_dict=True)

		jingrow.flags.link_fields[pagetype] = standard_fields + custom_fields + property_setter_fields

	return jingrow.flags.link_fields[pagetype]


def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
	CustomField = jingrow.qb.PageType("Custom Field")
	PropertySetter = jingrow.qb.PageType("Property Setter")

	if jingrow.conf.developer_mode:
		for name in jingrow.get_all("PageField", filters={"options": old}, pluck="parent"):
			if name in (old, new):
				continue

			pagetype = jingrow.get_page("PageType", name)
			save = False
			for f in pagetype.fields:
				if f.options == old:
					f.options = new
					save = True
			if save:
				pagetype.save()

	PageField = jingrow.qb.PageType("PageField")
	jingrow.qb.update(PageField).set(PageField.options, new).where(
		(PageField.fieldtype == fieldtype) & (PageField.options == old)
	).run()

	jingrow.qb.update(CustomField).set(CustomField.options, new).where(
		(CustomField.fieldtype == fieldtype) & (CustomField.options == old)
	).run()

	jingrow.qb.update(PropertySetter).set(PropertySetter.value, new).where(
		(PropertySetter.property == "options") & (PropertySetter.value == old)
	).run()


def get_select_fields(old: str, new: str) -> list[dict]:
	"""
	get select type fields where pagetype's name is hardcoded as
	new line separated list
	"""
	df = jingrow.qb.PageType("PageField")
	dt = jingrow.qb.PageType("PageType")
	cf = jingrow.qb.PageType("Custom Field")
	ps = jingrow.qb.PageType("Property Setter")

	# get link fields from tabPageField
	st_issingle = jingrow.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle")
	standard_fields = (
		jingrow.qb.from_(df)
		.select(df.parent, df.fieldname, st_issingle)
		.where(
			(df.parent != new)
			& (df.fieldname != "fieldtype")
			& (df.fieldtype == "Select")
			& (df.options.like(f"%{old}%"))
		)
		.run(as_dict=True)
	)

	# get link fields from tabCustom Field
	cf_issingle = jingrow.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle")
	custom_select_fields = (
		jingrow.qb.from_(cf)
		.select(cf.dt.as_("parent"), cf.fieldname, cf_issingle)
		.where((cf.dt != new) & (cf.fieldtype == "Select") & (cf.options.like(f"%{old}%")))
		.run(as_dict=True)
	)

	# remove fields whose options have been changed using property setter
	ps_issingle = jingrow.qb.from_(dt).select(dt.issingle).where(dt.name == ps.page_type).as_("issingle")
	property_setter_select_fields = (
		jingrow.qb.from_(ps)
		.select(ps.page_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle)
		.where(
			(ps.page_type != new)
			& (ps.property == "options")
			& (ps.field_name.notnull())
			& (ps.value.like(f"%{old}%"))
		)
		.run(as_dict=True)
	)

	return standard_fields + custom_select_fields + property_setter_select_fields


def update_select_field_values(old: str, new: str):
	from jingrow.query_builder.functions import Replace

	PageField = jingrow.qb.PageType("PageField")
	CustomField = jingrow.qb.PageType("Custom Field")
	PropertySetter = jingrow.qb.PageType("Property Setter")

	jingrow.qb.update(PageField).set(PageField.options, Replace(PageField.options, old, new)).where(
		(PageField.fieldtype == "Select")
		& (PageField.parent != new)
		& (PageField.options.like(f"%\n{old}%") | PageField.options.like(f"%{old}\n%"))
	).run()

	jingrow.qb.update(CustomField).set(CustomField.options, Replace(CustomField.options, old, new)).where(
		(CustomField.fieldtype == "Select")
		& (CustomField.dt != new)
		& (CustomField.options.like(f"%\n{old}%") | CustomField.options.like(f"%{old}\n%"))
	).run()

	jingrow.qb.update(PropertySetter).set(PropertySetter.value, Replace(PropertySetter.value, old, new)).where(
		(PropertySetter.property == "options")
		& (PropertySetter.field_name.notnull())
		& (PropertySetter.page_type != new)
		& (PropertySetter.value.like(f"%\n{old}%") | PropertySetter.value.like(f"%{old}\n%"))
	).run()


def update_parenttype_values(old: str, new: str):
	child_pagetypes = jingrow.get_all(
		"PageField",
		fields=["options", "fieldname"],
		filters={"parent": new, "fieldtype": ["in", jingrow.model.table_fields]},
	)

	custom_child_pagetypes = jingrow.get_all(
		"Custom Field",
		fields=["options", "fieldname"],
		filters={"dt": new, "fieldtype": ["in", jingrow.model.table_fields]},
	)

	child_pagetypes += custom_child_pagetypes
	fields = [d["fieldname"] for d in child_pagetypes]

	property_setter_child_pagetypes = jingrow.get_all(
		"Property Setter",
		filters={"page_type": new, "property": "options", "field_name": ("in", fields)},
		pluck="value",
	)

	child_pagetypes = set(list(d["options"] for d in child_pagetypes) + property_setter_child_pagetypes)

	for pagetype in child_pagetypes:
		table = jingrow.qb.PageType(pagetype)
		jingrow.qb.update(table).set(table.parenttype, new).where(table.parenttype == old).run()


def rename_dynamic_links(pagetype: str, old: str, new: str):
	Singles = jingrow.qb.PageType("Singles")
	for df in get_dynamic_link_map().get(pagetype, []):
		# dynamic link in single, just one value to check
		meta = jingrow.get_meta(df.parent)
		if meta.is_virtual:
			continue
		if meta.issingle:
			refdoc = jingrow.db.get_singles_dict(df.parent)
			if refdoc.get(df.options) == pagetype and refdoc.get(df.fieldname) == old:
				jingrow.qb.update(Singles).set(Singles.value, new).where(
					(Singles.field == df.fieldname) & (Singles.pagetype == df.parent) & (Singles.value == old)
				).run()
		else:
			# because the table hasn't been renamed yet!
			parent = df.parent if df.parent != new else old

			jingrow.qb.update(parent).set(df.fieldname, new).where(
				(Field(df.options) == pagetype) & (Field(df.fieldname) == old)
			).run()


def bulk_rename(pagetype: str, rows: list[list] | None = None, via_console: bool = False) -> list[str] | None:
	"""Bulk rename documents

	:param pagetype: PageType to be renamed
	:param rows: list of documents as `((oldname, newname, merge(optional)), ..)`"""
	if not rows:
		jingrow.throw(_("Please select a valid csv file with data"))

	if not via_console:
		max_rows = 500
		if len(rows) > max_rows:
			jingrow.throw(_("Maximum {0} rows allowed").format(max_rows))

	rename_log = []
	for row in rows:
		# if row has some content
		if len(row) > 1 and row[0] and row[1]:
			merge = len(row) > 2 and (row[2] == "1" or row[2].lower() == "true")
			try:
				if rename_page(pagetype, row[0], row[1], merge=merge, rebuild_search=False):
					msg = _("Successful: {0} to {1}").format(row[0], row[1])
					jingrow.db.commit()
				else:
					msg = None
			except Exception as e:
				msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e))
				jingrow.db.rollback()

			if msg:
				if via_console:
					print(msg)
				else:
					rename_log.append(msg)

	jingrow.enqueue("jingrow.utils.global_search.rebuild_for_pagetype", pagetype=pagetype)

	if not via_console:
		return rename_log
