# Copyright (c) 2018, JINGROW
# License: MIT. See LICENSE

import json
import os
from collections import namedtuple

import jingrow
from jingrow import _
from jingrow.core.pagetype.role.role import get_info_based_on_role, get_user_info
from jingrow.core.pagetype.sms_settings.sms_settings import send_sms
from jingrow.desk.pagetype.notification_log.notification_log import enqueue_create_notification
from jingrow.integrations.pagetype.slack_webhook_url.slack_webhook_url import send_slack_message
from jingrow.model.page import Page
from jingrow.modules.utils import export_module_json, get_pg_module
from jingrow.utils import add_to_date, cast, nowdate, validate_email_address
from jingrow.utils.jinja import validate_template
from jingrow.utils.safe_exec import get_safe_globals

FORMATS = {"HTML": ".html", "Markdown": ".md", "Plain Text": ".txt"}
FORBIDDEN_DOCUMENT_TYPES = frozenset(("Email Queue",))
DATE_BASED_EVENTS = frozenset(("Days Before", "Days After"))


class Notification(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.email.pagetype.notification_recipient.notification_recipient import NotificationRecipient
		from jingrow.types import DF

		attach_print: DF.Check
		channel: DF.Literal["Email", "Slack", "System Notification", "SMS"]
		condition: DF.Code | None
		date_changed: DF.Literal[None]
		days_in_advance: DF.Int
		document_type: DF.Link
		enabled: DF.Check
		event: DF.Literal[
			"",
			"New",
			"Save",
			"Submit",
			"Cancel",
			"Days After",
			"Days Before",
			"Value Change",
			"Method",
			"Custom",
		]
		is_standard: DF.Check
		message: DF.Code | None
		message_type: DF.Literal["Markdown", "HTML", "Plain Text"]
		method: DF.Data | None
		module: DF.Link | None
		print_format: DF.Link | None
		property_value: DF.Data | None
		recipients: DF.Table[NotificationRecipient]
		send_system_notification: DF.Check
		send_to_all_assignees: DF.Check
		sender: DF.Link | None
		sender_email: DF.Data | None
		set_property_after_alert: DF.Literal[None]
		slack_webhook_url: DF.Link | None
		subject: DF.Data | None
		value_changed: DF.Literal[None]
	# end: auto-generated types

	def onload(self):
		"""load message"""
		if self.is_standard:
			self.message = self.get_template()

	def autoname(self):
		if not self.name:
			self.name = self.subject

	def validate(self):
		if self.channel in ("Email", "Slack", "System Notification"):
			validate_template(self.subject)

		validate_template(self.message)

		if self.event in ("Days Before", "Days After") and not self.date_changed:
			jingrow.throw(_("Please specify which date field must be checked"))

		if self.event == "Value Change" and not self.value_changed:
			jingrow.throw(_("Please specify which value field must be checked"))

		self.validate_forbidden_document_types()
		self.validate_condition()
		self.validate_standard()
		jingrow.cache.hdel("notifications", self.document_type)

	def on_update(self):
		jingrow.cache.hdel("notifications", self.document_type)
		path = export_module_json(self, self.is_standard, self.module)
		if path and self.message:
			extension = FORMATS.get(self.message_type, ".md")
			file_path = path + extension
			with open(file_path, "w") as f:
				f.write(self.message)

			# py
			if not os.path.exists(path + ".py"):
				with open(path + ".py", "w") as f:
					f.write(
						"""import jingrow

def get_context(context):
	# do your magic here
	pass
"""
					)

	def validate_standard(self):
		if self.is_standard and self.enabled and not jingrow.conf.developer_mode:
			jingrow.throw(
				_("Cannot edit Standard Notification. To edit, please disable this and duplicate it")
			)

	def validate_condition(self):
		temp_pg = jingrow.new_pg(self.document_type)
		if self.condition:
			try:
				jingrow.safe_eval(self.condition, None, get_context(temp_pg.as_dict()))
			except Exception:
				jingrow.throw(_("The Condition '{0}' is invalid").format(self.condition))

	def validate_forbidden_document_types(self):
		if self.document_type in FORBIDDEN_DOCUMENT_TYPES or (
			jingrow.get_meta(self.document_type).istable and self.event not in DATE_BASED_EVENTS
		):
			# only date based events are allowed for child tables
			jingrow.throw(
				_("Cannot set Notification with event {0} on Page Type {1}").format(
					_(self.event), _(self.document_type)
				)
			)

	def get_documents_for_today(self):
		"""get list of documents that will be triggered today"""
		pgs = []

		diff_days = self.days_in_advance
		if self.event == "Days After":
			diff_days = -diff_days

		reference_date = add_to_date(nowdate(), days=diff_days)
		reference_date_start = reference_date + " 00:00:00.000000"
		reference_date_end = reference_date + " 23:59:59.000000"

		pg_list = jingrow.get_all(
			self.document_type,
			fields="name",
			filters=[
				{self.date_changed: (">=", reference_date_start)},
				{self.date_changed: ("<=", reference_date_end)},
			],
		)

		for d in pg_list:
			pg = jingrow.get_pg(self.document_type, d.name)

			if self.condition and not jingrow.safe_eval(self.condition, None, get_context(pg)):
				continue

			pgs.append(pg)

		return pgs

	def send(self, pg):
		"""Build recipients and send Notification"""

		context = get_context(pg)
		context = {"pg": pg, "alert": self, "comments": None}
		if pg.get("_comments"):
			context["comments"] = json.loads(pg.get("_comments"))

		if self.is_standard:
			self.load_standard_properties(context)
		try:
			if self.channel == "Email":
				self.send_an_email(pg, context)

			if self.channel == "Slack":
				self.send_a_slack_msg(pg, context)

			if self.channel == "SMS":
				self.send_sms(pg, context)

			if self.channel == "System Notification" or self.send_system_notification:
				self.create_system_notification(pg, context)

		except Exception:
			self.log_error("Failed to send Notification")

		if self.set_property_after_alert:
			allow_update = True
			if (
				pg.pagestatus.is_submitted()
				and not pg.meta.get_field(self.set_property_after_alert).allow_on_submit
			):
				allow_update = False
			try:
				if allow_update and not pg.flags.in_notification_update:
					fieldname = self.set_property_after_alert
					value = self.property_value
					if pg.meta.get_field(fieldname).fieldtype in jingrow.model.numeric_fieldtypes:
						value = jingrow.utils.cint(value)

					pg.reload()
					pg.set(fieldname, value)
					pg.flags.updater_reference = {
						"pagetype": self.pagetype,
						"pagename": self.name,
						"label": _("via Notification"),
					}
					pg.flags.in_notification_update = True
					pg.save(ignore_permissions=True)
					pg.flags.in_notification_update = False
			except Exception:
				self.log_error("Page update failed")

	def create_system_notification(self, pg, context):
		subject = self.subject
		if "{" in subject:
			subject = jingrow.render_template(self.subject, context)

		attachments = self.get_attachment(pg)

		recipients, cc, bcc = self.get_list_of_recipients(pg, context)

		users = recipients + cc + bcc

		if not users:
			return

		notification_pg = {
			"type": "Alert",
			"document_type": get_reference_pagetype(pg),
			"document_name": get_reference_name(pg),
			"subject": subject,
			"from_user": pg.modified_by or pg.owner,
			"email_content": jingrow.render_template(self.message, context),
			"attached_file": attachments and json.dumps(attachments[0]),
		}
		enqueue_create_notification(users, notification_pg)

	def send_an_email(self, pg, context):
		from email.utils import formataddr

		from jingrow.core.pagetype.email.email_helpers import _make as make_email

		subject = self.subject
		if "{" in subject:
			subject = jingrow.render_template(self.subject, context)

		attachments = self.get_attachment(pg)
		recipients, cc, bcc = self.get_list_of_recipients(pg, context)
		if not (recipients or cc or bcc):
			return

		sender = None
		message = jingrow.render_template(self.message, context)
		if self.sender and self.sender_email:
			sender = formataddr((self.sender, self.sender_email))

		email = None
		# Add mail notification to email list
		# No need to add if it is already an email.
		if pg.pagetype != "Email":
			email = make_email(
				pagetype=get_reference_pagetype(pg),
				name=get_reference_name(pg),
				content=message,
				subject=subject,
				sender=sender,
				recipients=recipients,
				send_email=False,
				attachments=attachments,
				cc=cc,
				bcc=bcc,
			).get("name")

		jingrow.sendmail(
			recipients=recipients,
			subject=subject,
			sender=sender,
			cc=cc,
			bcc=bcc,
			message=message,
			reference_pagetype=get_reference_pagetype(pg),
			reference_name=get_reference_name(pg),
			attachments=attachments,
			expose_recipients="header",
			print_letterhead=((attachments and attachments[0].get("print_letterhead")) or False),
			email=email,
		)

	def send_a_slack_msg(self, pg, context):
		send_slack_message(
			webhook_url=self.slack_webhook_url,
			message=jingrow.render_template(self.message, context),
			reference_pagetype=get_reference_pagetype(pg),
			reference_name=get_reference_name(pg),
		)

	def send_sms(self, pg, context):
		send_sms(
			receiver_list=self.get_receiver_list(pg, context),
			msg=jingrow.render_template(self.message, context),
		)

	def get_list_of_recipients(self, pg, context):
		recipients = []
		cc = []
		bcc = []
		for recipient in self.recipients:
			if recipient.condition:
				if not jingrow.safe_eval(recipient.condition, None, context):
					continue
			if recipient.receiver_by_document_field:
				fields = recipient.receiver_by_document_field.split(",")
				# fields from child table
				if len(fields) > 1:
					for d in pg.get(fields[1]):
						email_id = d.get(fields[0])
						if validate_email_address(email_id):
							recipients.append(email_id)
				# field from parent pg
				else:
					email_ids_value = pg.get(fields[0])
					if validate_email_address(email_ids_value):
						email_ids = email_ids_value.replace(",", "\n")
						recipients = recipients + email_ids.split("\n")

			cc.extend(get_emails_from_template(recipient.cc, context))
			bcc.extend(get_emails_from_template(recipient.bcc, context))

			# For sending emails to specified role
			if recipient.receiver_by_role:
				emails = get_info_based_on_role(recipient.receiver_by_role, "email", ignore_permissions=True)

				for email in emails:
					recipients = recipients + email.split("\n")

		if self.send_to_all_assignees:
			recipients = recipients + get_assignees(pg)

		return list(set(recipients)), list(set(cc)), list(set(bcc))

	def get_receiver_list(self, pg, context):
		"""return receiver list based on the pg field and role specified"""
		receiver_list = []
		for recipient in self.recipients:
			if recipient.condition:
				if not jingrow.safe_eval(recipient.condition, None, context):
					continue

			# For sending messages to the owner's mobile phone number
			if recipient.receiver_by_document_field == "owner":
				receiver_list += get_user_info([dict(user_name=pg.get("owner"))], "mobile_no")
			# For sending messages to the number specified in the receiver field
			elif recipient.receiver_by_document_field:
				receiver_list.append(pg.get(recipient.receiver_by_document_field))

			# For sending messages to specified role
			if recipient.receiver_by_role:
				receiver_list += get_info_based_on_role(
					recipient.receiver_by_role, "mobile_no", ignore_permissions=True
				)

		return receiver_list

	def get_attachment(self, pg):
		"""check print settings are attach the pdf"""
		if not self.attach_print:
			return None

		print_settings = jingrow.get_pg("Print Settings", "Print Settings")
		if (pg.pagestatus == 0 and not print_settings.allow_print_for_draft) or (
			pg.pagestatus == 2 and not print_settings.allow_print_for_cancelled
		):
			# ignoring attachment as draft and cancelled documents are not allowed to print
			status = "Draft" if pg.pagestatus == 0 else "Cancelled"
			jingrow.throw(
				_(
					"""Not allowed to attach {0} document, please enable Allow Print For {0} in Print Settings"""
				).format(status),
				title=_("Error in Notification"),
			)
		else:
			return [
				{
					"print_format_attachment": 1,
					"pagetype": pg.pagetype,
					"name": pg.name,
					"print_format": self.print_format,
					"print_letterhead": print_settings.with_letterhead,
					"lang": jingrow.db.get_value("Print Format", self.print_format, "default_print_language")
					if self.print_format
					else "en",
				}
			]

	def get_template(self, md_as_html=False):
		module = get_pg_module(self.module, self.pagetype, self.name)

		path = os.path.join(os.path.dirname(module.__file__), jingrow.scrub(self.name))
		extension = FORMATS.get(self.message_type, ".md")
		file_path = path + extension

		template = ""

		if os.path.exists(file_path):
			with open(file_path) as f:
				template = f.read()

		if not template:
			return

		if extension == ".md":
			return jingrow.utils.md_to_html(template)

		return template

	def load_standard_properties(self, context):
		"""load templates and run get_context"""
		module = get_pg_module(self.module, self.pagetype, self.name)
		if module:
			if hasattr(module, "get_context"):
				out = module.get_context(context)
				if out:
					context.update(out)

		self.message = self.get_template(md_as_html=True)

	def on_trash(self):
		jingrow.cache.hdel("notifications", self.document_type)


@jingrow.whitelist()
def get_documents_for_today(notification):
	notification = jingrow.get_pg("Notification", notification)
	notification.check_permission("read")
	return [d.name for d in notification.get_documents_for_today()]


def trigger_daily_alerts():
	trigger_notifications(None, "daily")


def trigger_notifications(pg, method=None):
	if jingrow.flags.in_import or jingrow.flags.in_patch:
		# don't send notifications while syncing or patching
		return

	if method == "daily":
		pg_list = jingrow.get_all(
			"Notification", filters={"event": ("in", ("Days Before", "Days After")), "enabled": 1}
		)
		for d in pg_list:
			alert = jingrow.get_pg("Notification", d.name)

			for pg in alert.get_documents_for_today():
				evaluate_alert(pg, alert, alert.event)
				jingrow.db.commit()


def evaluate_alert(pg: Page, alert, event):
	from jinja2 import TemplateError

	try:
		if isinstance(alert, str):
			alert = jingrow.get_pg("Notification", alert)

		context = get_context(pg)

		if alert.condition:
			if not jingrow.safe_eval(alert.condition, None, context):
				return

		if event == "Value Change" and not pg.is_new():
			if not jingrow.db.has_column(pg.pagetype, alert.value_changed):
				alert.db_set("enabled", 0)
				alert.log_error(f"Notification {alert.name} has been disabled due to missing field")
				return

			pg_before_save = pg.get_pg_before_save()
			field_value_before_save = pg_before_save.get(alert.value_changed) if pg_before_save else None

			fieldtype = pg.meta.get_field(alert.value_changed).fieldtype
			if cast(fieldtype, pg.get(alert.value_changed)) == cast(fieldtype, field_value_before_save):
				# value not changed
				return

		if event != "Value Change" and not pg.is_new():
			# reload the pg for the latest values & comments,
			# except for validate type event.
			pg.reload()
		alert.send(pg)
	except TemplateError:
		message = _("Error while evaluating Notification {0}. Please fix your template.").format(
			jingrow.utils.get_link_to_form("Notification", alert.name)
		)
		jingrow.throw(message, title=_("Error in Notification"))
	except Exception as e:
		title = str(e)
		message = jingrow.get_traceback(with_context=True)
		jingrow.log_error(title=title, message=message)
		msg = f"<details><summary>{title}</summary>{message}</details>"
		jingrow.throw(msg, title=_("Error in Notification"))


def get_context(pg):
	Jingrow = namedtuple("jingrow", ["utils"])
	return {
		"pg": pg,
		"nowdate": nowdate,
		"jingrow": Jingrow(utils=get_safe_globals().get("jingrow").get("utils")),
	}


def get_assignees(pg):
	assignees = []
	assignees = jingrow.get_all(
		"ToDo",
		filters={"status": "Open", "reference_name": pg.name, "reference_type": pg.pagetype},
		fields=["allocated_to"],
	)

	return [d.allocated_to for d in assignees]


def get_emails_from_template(template, context):
	if not template:
		return ()

	emails = jingrow.render_template(template, context) if "{" in template else template
	return filter(None, emails.replace(",", "\n").split("\n"))


def get_reference_pagetype(pg):
	return pg.parenttype if pg.meta.istable else pg.pagetype


def get_reference_name(pg):
	return pg.parent if pg.meta.istable else pg.name
