# Copyright (c) 2015, InfluxFramework LLC
# License: MIT. See LICENSE

import os
import shutil

import influxframework
import influxframework.defaults
import influxframework.model.meta
from influxframework import _, get_module_path
from influxframework.desk.doctype.tag.tag import delete_tags_for_document
from influxframework.model.dynamic_links import get_dynamic_link_map
from influxframework.model.naming import revert_series_if_last
from influxframework.model.utils import is_virtual_doctype
from influxframework.utils.file_manager import remove_all
from influxframework.utils.global_search import delete_for_document
from influxframework.utils.password import delete_all_passwords_for

doctypes_to_skip = (
	"Communication",
	"ToDo",
	"DocShare",
	"Email Unsubscribe",
	"Activity Log",
	"File",
	"Version",
	"Document Follow",
	"Comment",
	"View Log",
	"Tag Link",
	"Notification Log",
	"Email Queue",
	"Document Share Key",
	"Integration Request",
)


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

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

	is_virtual = is_virtual_doctype(doctype)

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

	for name in names or []:
		if is_virtual:
			influxframework.get_doc(doctype, name).delete()
			continue

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

		# delete passwords
		delete_all_passwords_for(doctype, name)

		doc = None
		if doctype == "DocType":
			if for_reload:

				try:
					doc = influxframework.get_doc(doctype, name)
				except influxframework.DoesNotExistError:
					pass
				else:
					doc.run_method("before_reload")

			else:
				doc = influxframework.get_doc(doctype, name)

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

			delete_from_table(doctype, name, ignore_doctypes, None)

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

		else:
			doc = influxframework.get_doc(doctype, name)

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

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

				# check if links exist
				if not force:
					check_if_doc_is_linked(doc)
					check_if_doc_is_dynamically_linked(doc)

			update_naming_series(doc)
			delete_from_table(doctype, name, ignore_doctypes, doc)
			doc.run_method("after_delete")

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

			if not for_reload:
				# Enqueued at the end, because it gets committed
				# All the linked docs should be checked beforehand
				influxframework.enqueue(
					"influxframework.model.delete_doc.delete_dynamic_links",
					doctype=doc.doctype,
					name=doc.name,
					now=influxframework.flags.in_test,
				)

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

		if for_reload:
			delete_permanently = True

		if not delete_permanently:
			add_to_deleted_document(doc)

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


def add_to_deleted_document(doc):
	"""Add this document to Deleted Document table. Called after delete"""
	if doc.doctype != "Deleted Document" and influxframework.flags.in_install != "influxframework":
		influxframework.get_doc(
			dict(
				doctype="Deleted Document",
				deleted_doctype=doc.doctype,
				deleted_name=doc.name,
				data=doc.as_json(),
				owner=influxframework.session.user,
			)
		).db_insert()


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

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


def delete_from_table(doctype: str, name: str, ignore_doctypes: list[str], doc):
	if doctype != "DocType" and doctype == name:
		influxframework.db.delete("Singles", {"doctype": name})
	else:
		influxframework.db.delete(doctype, {"name": name})
	if doc:
		child_doctypes = [
			d.options for d in doc.meta.get_table_fields() if influxframework.get_meta(d.options).is_virtual == 0
		]

	else:
		child_doctypes = influxframework.get_all(
			"DocField",
			fields="options",
			filters={"fieldtype": ["in", influxframework.model.table_fields], "parent": doctype},
			pluck="options",
		)

	child_doctypes_to_delete = set(child_doctypes) - set(ignore_doctypes)
	for child_doctype in child_doctypes_to_delete:
		influxframework.db.delete(child_doctype, {"parenttype": doctype, "parent": name})


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

	if flags:
		doc.flags.update(flags)


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

	# check if submitted
	if doc.docstatus.is_submitted():
		influxframework.msgprint(
			_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(
				_(doc.doctype),
				doc.name,
				"<a href='https://docs.influxerp.com//docs/user/manual/en/setting-up/articles/delete-submitted-document' target='_blank'>",
				"</a>",
			),
			raise_exception=True,
		)


def check_if_doc_is_linked(doc, method="Delete"):
	"""
	Raises excption if the given doc(dt, dn) is linked in another record.
	"""
	from influxframework.model.rename_doc import get_link_fields

	link_fields = get_link_fields(doc.doctype)
	ignore_linked_doctypes = doc.get("ignore_linked_doctypes") or []

	for lf in link_fields:
		link_dt, link_field, issingle = lf["parent"], lf["fieldname"], lf["issingle"]

		if not issingle:
			fields = ["name", "docstatus"]
			if influxframework.get_meta(link_dt).istable:
				fields.extend(["parent", "parenttype"])

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

				if linked_doctype in doctypes_to_skip or (
					linked_doctype in ignore_linked_doctypes and method == "Cancel"
				):
					# don't check for communication and todo!
					continue

				if method != "Delete" and (method != "Cancel" or item.docstatus != 1):
					# don't raise exception if not
					# linked to a non-cancelled doc when deleting or to a submitted doc when cancelling
					continue
				elif link_dt == doc.doctype and (item_parent or item.name) == doc.name:
					# don't raise exception if not
					# linked to same item or doc having same name as the item
					continue
				else:
					reference_docname = item_parent or item.name
					raise_link_exists_exception(doc, linked_doctype, reference_docname)

		else:
			if influxframework.db.get_value(link_dt, None, link_field) == doc.name:
				raise_link_exists_exception(doc, link_dt, link_dt)


def check_if_doc_is_dynamically_linked(doc, method="Delete"):
	"""Raise `influxframework.LinkExistsError` if the document is dynamically linked"""
	for df in get_dynamic_link_map().get(doc.doctype, []):

		ignore_linked_doctypes = doc.get("ignore_linked_doctypes") or []

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

		meta = influxframework.get_meta(df.parent)
		if meta.issingle:
			# dynamic link in single doc
			refdoc = influxframework.db.get_singles_dict(df.parent)
			if (
				refdoc.get(df.options) == doc.doctype
				and refdoc.get(df.fieldname) == doc.name
				and (
					(method == "Delete" and refdoc.docstatus < 2)
					or (method == "Cancel" and refdoc.docstatus == 1)
				)
			):
				# raise exception only if
				# linked to an non-cancelled doc when deleting
				# or linked to a submitted doc when cancelling
				raise_link_exists_exception(doc, df.parent, df.parent)
		else:
			# dynamic link in table
			df["table"] = ", `parent`, `parenttype`, `idx`" if meta.istable else ""
			for refdoc in influxframework.db.sql(
				"""select `name`, `docstatus` {table} from `tab{parent}` where
				{options}=%s and {fieldname}=%s""".format(
					**df
				),
				(doc.doctype, doc.name),
				as_dict=True,
			):

				if (method == "Delete" and refdoc.docstatus < 2) or (
					method == "Cancel" and refdoc.docstatus == 1
				):
					# raise exception only if
					# linked to an non-cancelled doc when deleting
					# or linked to a submitted doc when cancelling

					reference_doctype = refdoc.parenttype if meta.istable else df.parent
					reference_docname = refdoc.parent if meta.istable else refdoc.name
					at_position = f"at Row: {refdoc.idx}" if meta.istable else ""

					raise_link_exists_exception(doc, reference_doctype, reference_docname, at_position)


def raise_link_exists_exception(doc, reference_doctype, reference_docname, row=""):
	doc_link = '<a href="/app/Form/{0}/{1}">{1}</a>'.format(doc.doctype, doc.name)
	reference_link = '<a href="/app/Form/{0}/{1}">{1}</a>'.format(
		reference_doctype, reference_docname
	)

	# hack to display Single doctype only once in message
	if reference_doctype == reference_docname:
		reference_doctype = ""

	influxframework.throw(
		_("Cannot delete or cancel because {0} {1} is linked with {2} {3} {4}").format(
			doc.doctype, doc_link, reference_doctype, reference_link, row
		),
		influxframework.LinkExistsError,
	)


def delete_dynamic_links(doctype, name):
	delete_references("ToDo", doctype, name, "reference_type")
	delete_references("Email Unsubscribe", doctype, name)
	delete_references("DocShare", doctype, name, "share_doctype", "share_name")
	delete_references("Version", doctype, name, "ref_doctype", "docname")
	delete_references("Comment", doctype, name)
	delete_references("View Log", doctype, name)
	delete_references("Document Follow", doctype, name, "ref_doctype", "ref_docname")
	delete_references("Notification Log", doctype, name, "document_type", "document_name")

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

	clear_references("Activity Log", doctype, name)
	clear_references("Activity Log", doctype, name, "timeline_doctype", "timeline_name")


def delete_references(
	doctype,
	reference_doctype,
	reference_name,
	reference_doctype_field="reference_doctype",
	reference_name_field="reference_name",
):
	influxframework.db.delete(
		doctype, {reference_doctype_field: reference_doctype, reference_name_field: reference_name}
	)


def clear_references(
	doctype,
	reference_doctype,
	reference_name,
	reference_doctype_field="reference_doctype",
	reference_name_field="reference_name",
):
	influxframework.db.sql(
		"""update
			`tab{0}`
		set
			{1}=NULL, {2}=NULL
		where
			{1}=%s and {2}=%s""".format(
			doctype, reference_doctype_field, reference_name_field
		),  # nosec
		(reference_doctype, reference_name),
	)


def clear_timeline_references(link_doctype, link_name):
	influxframework.db.delete("Communication Link", {"link_doctype": link_doctype, "link_name": link_name})


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

	from influxframework.utils import get_fullname

	influxframework.get_doc(
		{
			"doctype": "Comment",
			"comment_type": "Deleted",
			"reference_doctype": doc.doctype,
			"subject": f"{_(doc.doctype)} {doc.name}",
			"full_name": get_fullname(doc.owner),
		}
	).insert(ignore_permissions=True)


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

	shutil.rmtree(dir_path)
