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

import os
import shutil

import jingrow
import jingrow.defaults
import jingrow.model.meta
from jingrow import _, get_module_path
from jingrow.desk.pagetype.tag.tag import delete_tags_for_document
from jingrow.model.pagestatus import PageStatus
from jingrow.model.dynamic_links import get_dynamic_link_map
from jingrow.model.naming import revert_series_if_last
from jingrow.model.utils import is_virtual_pagetype
from jingrow.utils.file_manager import remove_all
from jingrow.utils.global_search import delete_for_document
from jingrow.utils.password import delete_all_passwords_for


def delete_page(
	pagetype=None,
	name=None,
	force=0,
	ignore_pagetypes=None,
	for_reload=False,
	ignore_permissions=False,
	flags=None,
	ignore_on_trash=False,
	ignore_missing=True,
	delete_permanently=False,
):
	"""
	Deletes a page(dt, dn) and validates if it is not submitted and not linked in a live record
	"""
	if not ignore_pagetypes:
		ignore_pagetypes = []

	# get from form
	if not pagetype:
		pagetype = jingrow.form_dict.get("dt")
		name = jingrow.form_dict.get("dn")

	is_virtual = is_virtual_pagetype(pagetype)

	names = name
	if isinstance(name, str) or isinstance(name, int):
		names = [name]

	for name in names or []:
		if is_virtual:
			jingrow.get_page(pagetype, name).delete()
			continue

		# already deleted..?
		if not jingrow.db.exists(pagetype, name):
			if not ignore_missing:
				raise jingrow.DoesNotExistError(pagetype=pagetype)
			else:
				return False

		# delete passwords
		delete_all_passwords_for(pagetype, name)

		page = None
		if pagetype == "PageType":
			if for_reload:
				try:
					page = jingrow.get_page(pagetype, name)
				except jingrow.DoesNotExistError:
					pass
				else:
					page.run_method("before_reload")

			else:
				page = jingrow.get_page(pagetype, name)
				if not (page.custom or jingrow.conf.developer_mode or jingrow.flags.in_patch or force):
					jingrow.throw(_("Standard PageType can not be deleted."))

				update_flags(page, flags, ignore_permissions)
				check_permission_and_not_submitted(page)
				# delete custom table fields using this pagetype.
				jingrow.db.delete(
					"Custom Field", {"options": name, "fieldtype": ("in", jingrow.model.table_fields)}
				)
				jingrow.db.delete("__global_search", {"pagetype": name})

			delete_from_table(pagetype, name, ignore_pagetypes, None)

			if (
				jingrow.conf.developer_mode
				and not page.custom
				and not (
					for_reload
					or jingrow.flags.in_migrate
					or jingrow.flags.in_install
					or jingrow.flags.in_uninstall
				)
			):
				try:
					delete_controllers(name, page.module)
				except (OSError, KeyError):
					# in case a pagetype doesnt have any controller code  nor any app and module
					pass

		else:
			# Lock the page without waiting
			try:
				jingrow.db.get_value(pagetype, name, for_update=True, wait=False)
			except jingrow.QueryTimeoutError:
				jingrow.throw(
					_(
						"This document can not be deleted right now as it's being modified by another user. Please try again after some time."
					),
					exc=jingrow.QueryTimeoutError,
				)
			page = jingrow.get_page(pagetype, name)

			if not for_reload:
				update_flags(page, flags, ignore_permissions)
				check_permission_and_not_submitted(page)

				if not ignore_on_trash:
					page.run_method("on_trash")
					page.flags.in_delete = True
					page.run_method("on_change")

				# check if links exist
				if not force:
					try:
						check_if_page_is_linked(page)
						check_if_page_is_dynamically_linked(page)
					except jingrow.LinkExistsError as e:
						if page.meta.has_field("enabled") or page.meta.has_field("disabled"):
							jingrow.throw(
								_("You can disable this {0} instead of deleting it.").format(_(pagetype)),
								jingrow.LinkExistsError,
							)
						else:
							raise e

			update_naming_series(page)
			delete_from_table(pagetype, name, ignore_pagetypes, page)
			page.run_method("after_delete")

			# delete attachments
			remove_all(pagetype, name, from_delete=True, delete_permanently=delete_permanently)

			if not for_reload:
				# Enqueued at the end, because it gets committed
				# All the linked pages should be checked beforehand
				jingrow.enqueue(
					"jingrow.model.delete_page.delete_dynamic_links",
					pagetype=page.pagetype,
					name=page.name,
					now=jingrow.flags.in_test,
					enqueue_after_commit=True,
				)

		# clear cache for Page
		page.clear_cache()
		# delete global search entry
		delete_for_document(page)
		# delete tag link entry
		delete_tags_for_document(page)

		if for_reload:
			delete_permanently = True

		if not delete_permanently:
			add_to_deleted_document(page)

		if page and not for_reload:
			if not jingrow.flags.in_patch:
				try:
					page.notify_update()
					insert_feed(page)
				except ImportError:
					pass


def add_to_deleted_document(page):
	"""Add this document to Deleted Page table. Called after delete"""
	if page.pagetype != "Deleted Page" and jingrow.flags.in_install != "jingrow":
		jingrow.get_page(
			dict(
				pagetype="Deleted Page",
				deleted_pagetype=page.pagetype,
				deleted_name=page.name,
				data=page.as_json(),
				owner=jingrow.session.user,
			)
		).db_insert()


def update_naming_series(page):
	if page.meta.autoname:
		if page.meta.autoname.startswith("naming_series:") and getattr(page, "naming_series", None):
			revert_series_if_last(page.naming_series, page.name, page)

		elif page.meta.autoname.split(":", 1)[0] not in ("Prompt", "field", "hash", "autoincrement"):
			revert_series_if_last(page.meta.autoname, page.name, page)


def delete_from_table(pagetype: str, name: str, ignore_pagetypes: list[str], page):
	if pagetype != "PageType" and pagetype == name:
		jingrow.db.delete("Singles", {"pagetype": name})
	else:
		jingrow.db.delete(pagetype, {"name": name})
	if page:
		child_pagetypes = [
			d.options for d in page.meta.get_table_fields() if jingrow.get_meta(d.options).is_virtual == 0
		]

	else:
		child_pagetypes = jingrow.get_all(
			"PageField",
			fields="options",
			filters={"fieldtype": ["in", jingrow.model.table_fields], "parent": pagetype},
			pluck="options",
		)

	child_pagetypes_to_delete = set(child_pagetypes) - set(ignore_pagetypes)
	for child_pagetype in child_pagetypes_to_delete:
		jingrow.db.delete(child_pagetype, {"parenttype": pagetype, "parent": name})


def update_flags(page, flags=None, ignore_permissions=False):
	if ignore_permissions:
		if not flags:
			flags = {}
		flags["ignore_permissions"] = ignore_permissions

	if flags:
		page.flags.update(flags)


def check_permission_and_not_submitted(page):
	# permission
	if (
		not page.flags.ignore_permissions
		and jingrow.session.user != "Administrator"
		and (not page.has_permission("delete") or (page.pagetype == "PageType" and not page.custom))
	):
		jingrow.msgprint(
			_("User not allowed to delete {0}: {1}").format(page.pagetype, page.name),
			raise_exception=jingrow.PermissionError,
		)

	# check if submitted
	if page.meta.is_submittable and page.pagestatus.is_submitted():
		jingrow.msgprint(
			_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(
				_(page.pagetype),
				page.name,
				"<a href='https://pages.jingrow.com//pages/user/manual/en/setting-up/articles/delete-submitted-document' target='_blank'>",
				"</a>",
			),
			raise_exception=True,
		)


def check_if_page_is_linked(page, method="Delete"):
	"""
	Raises excption if the given page(dt, dn) is linked in another record.
	"""
	from jingrow.model.rename_page import get_link_fields

	link_fields = get_link_fields(page.pagetype)
	ignored_pagetypes = set()

	if method == "Cancel" and (page_ignore_flags := page.get("ignore_linked_pagetypes")):
		ignored_pagetypes.update(page_ignore_flags)
	if method == "Delete":
		ignored_pagetypes.update(jingrow.get_hooks("ignore_links_on_delete"))

	for lf in link_fields:
		link_dt, link_field, issingle = lf["parent"], lf["fieldname"], lf["issingle"]
		if link_dt in ignored_pagetypes or (link_field == "amended_from" and method == "Cancel"):
			continue

		try:
			meta = jingrow.get_meta(link_dt)
		except jingrow.DoesNotExistError:
			jingrow.clear_last_message()
			# This mostly happens when app do not remove their customizations, we shouldn't
			# prevent link checks from failing in those cases
			continue

		if issingle:
			if jingrow.db.get_single_value(link_dt, link_field) == page.name:
				raise_link_exists_exception(page, link_dt, link_dt)
			continue

		fields = ["name", "pagestatus"]

		if meta.istable:
			fields.extend(["parent", "parenttype"])

		for item in jingrow.db.get_values(link_dt, {link_field: page.name}, fields, as_dict=True):
			# available only in child table cases
			item_parent = getattr(item, "parent", None)
			linked_parent_pagetype = item.parenttype if item_parent else link_dt

			if linked_parent_pagetype in ignored_pagetypes:
				continue

			if method != "Delete" and (method != "Cancel" or not PageStatus(item.pagestatus).is_submitted()):
				# don't raise exception if not
				# linked to a non-cancelled page when deleting or to a submitted page when cancelling
				continue
			elif link_dt == page.pagetype and (item_parent or item.name) == page.name:
				# don't raise exception if not
				# linked to same item or page having same name as the item
				continue
			else:
				reference_pagename = item_parent or item.name
				raise_link_exists_exception(page, linked_parent_pagetype, reference_pagename)


def check_if_page_is_dynamically_linked(page, method="Delete"):
	"""Raise `jingrow.LinkExistsError` if the document is dynamically linked"""
	for df in get_dynamic_link_map().get(page.pagetype, []):
		ignore_linked_pagetypes = page.get("ignore_linked_pagetypes") or []

		if df.parent in jingrow.get_hooks("ignore_links_on_delete") or (
			df.parent in ignore_linked_pagetypes and method == "Cancel"
		):
			# don't check for communication and todo!
			continue

		meta = jingrow.get_meta(df.parent)
		if meta.issingle:
			# dynamic link in single page
			refdoc = jingrow.db.get_singles_dict(df.parent)
			if (
				refdoc.get(df.options) == page.pagetype
				and refdoc.get(df.fieldname) == page.name
				and (
					# linked to an non-cancelled page when deleting
					(method == "Delete" and not PageStatus(refdoc.pagestatus).is_cancelled())
					# linked to a submitted page when cancelling
					or (method == "Cancel" and PageStatus(refdoc.pagestatus).is_submitted())
				)
			):
				raise_link_exists_exception(page, df.parent, df.parent)
		else:
			# dynamic link in table
			df["table"] = ", `parent`, `parenttype`, `idx`" if meta.istable else ""
			for refdoc in jingrow.db.sql(
				"""select `name`, `pagestatus` {table} from `tab{parent}` where
				`{options}`=%s and `{fieldname}`=%s""".format(**df),
				(page.pagetype, page.name),
				as_dict=True,
			):
				# linked to an non-cancelled page when deleting
				# or linked to a submitted page when cancelling
				if (method == "Delete" and not PageStatus(refdoc.pagestatus).is_cancelled()) or (
					method == "Cancel" and PageStatus(refdoc.pagestatus).is_submitted()
				):
					reference_pagetype = refdoc.parenttype if meta.istable else df.parent
					reference_pagename = refdoc.parent if meta.istable else refdoc.name

					if reference_pagetype in jingrow.get_hooks("ignore_links_on_delete") or (
						reference_pagetype in ignore_linked_pagetypes and method == "Cancel"
					):
						# don't check for communication and todo!
						continue

					at_position = f"at Row: {refdoc.idx}" if meta.istable else ""

					raise_link_exists_exception(page, reference_pagetype, reference_pagename, at_position)


def raise_link_exists_exception(page, reference_pagetype, reference_pagename, row=""):
	page_link = f'<a href="/app/Form/{page.pagetype}/{page.name}">{page.name}</a>'
	reference_link = f'<a href="/app/Form/{reference_pagetype}/{reference_pagename}">{reference_pagename}</a>'

	# hack to display Single pagetype only once in message
	if reference_pagetype == reference_pagename:
		reference_pagetype = ""

	jingrow.throw(
		_("Cannot delete or cancel because {0} {1} is linked with {2} {3} {4}").format(
			_(page.pagetype), page_link, _(reference_pagetype), reference_link, row
		),
		jingrow.LinkExistsError,
	)


def delete_dynamic_links(pagetype, name):
	delete_references("ToDo", pagetype, name, "reference_type")
	delete_references("Email Unsubscribe", pagetype, name)
	delete_references("PageShare", pagetype, name, "share_pagetype", "share_name")
	delete_references("Version", pagetype, name, "ref_pagetype", "pagename")
	delete_references("Comment", pagetype, name)
	delete_references("View Log", pagetype, name)
	delete_references("Page Follow", pagetype, name, "ref_pagetype", "ref_pagename")
	delete_references("Notification Log", pagetype, name, "document_type", "document_name")

	# unlink communications
	clear_timeline_references(pagetype, name)
	clear_references("Communication", pagetype, name)

	clear_references("System Log", pagetype, name)
	clear_references("System Log", pagetype, name, "timeline_pagetype", "timeline_name")


def delete_references(
	pagetype,
	reference_pagetype,
	reference_name,
	reference_pagetype_field="reference_pagetype",
	reference_name_field="reference_name",
):
	jingrow.db.delete(
		pagetype, {reference_pagetype_field: reference_pagetype, reference_name_field: reference_name}
	)


def clear_references(
	pagetype,
	reference_pagetype,
	reference_name,
	reference_pagetype_field="reference_pagetype",
	reference_name_field="reference_name",
):
	jingrow.db.sql(
		f"""update
			`tab{pagetype}`
		set
			{reference_pagetype_field}=NULL, {reference_name_field}=NULL
		where
			{reference_pagetype_field}=%s and {reference_name_field}=%s""",  # nosec
		(reference_pagetype, reference_name),
	)


def clear_timeline_references(link_pagetype, link_name):
	jingrow.db.delete("Communication Link", {"link_pagetype": link_pagetype, "link_name": link_name})


def insert_feed(page):
	if (
		jingrow.flags.in_install
		or jingrow.flags.in_uninstall
		or jingrow.flags.in_import
		or getattr(page, "no_feed_on_delete", False)
	):
		return

	from jingrow.utils import get_fullname

	jingrow.get_page(
		{
			"pagetype": "Comment",
			"comment_type": "Deleted",
			"reference_pagetype": page.pagetype,
			"subject": f"{_(page.pagetype)} {page.name}",
			"full_name": get_fullname(page.owner),
		}
	).insert(ignore_permissions=True)


def delete_controllers(pagetype, module):
	"""
	Delete controller code in the pagetype folder
	"""
	module_path = get_module_path(module)
	dir_path = os.path.join(module_path, "pagetype", jingrow.scrub(pagetype))

	shutil.rmtree(dir_path)
