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

from collections import Counter
from email.utils import getaddresses
from urllib.parse import unquote

from bs4 import BeautifulSoup

import jingrow
from jingrow import _
from jingrow.automation.pagetype.assignment_rule.assignment_rule import (
	apply as apply_assignment_rule,
)
from jingrow.contacts.pagetype.contact.contact import get_contact_name
from jingrow.core.pagetype.comment.comment import update_comment_in_pg
from jingrow.core.pagetype.email.email_helpers import validate_email
from jingrow.core.pagetype.email.mixins import EmailMixin
from jingrow.core.utils import get_parent_pg
from jingrow.model.page import Page
from jingrow.utils import (
	cstr,
	parse_addr,
	split_emails,
	strip_html,
	time_diff_in_seconds,
	validate_email_address,
)
from jingrow.utils.user import is_system_user

exclude_from_linked_with = True


class Email(Page, EmailMixin):
	# 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.email_link.email_link import EmailLink
		from jingrow.types import DF

		_user_tags: DF.Data | None
		bcc: DF.Code | None
		cc: DF.Code | None
		delivery_status: DF.Literal["", "Sent", "Bounced", "Opened", "Marked As Spam", "Rejected", "Delayed", "Soft-Bounced", "Clicked", "Recipient Unsubscribed", "Error", "Expired", "Sending", "Read", "Scheduled"]
		email_account: DF.Link | None
		email_status: DF.Literal["Open", "Spam", "Trash", "Archived"]
		email_template: DF.Link | None
		has_attachment: DF.Check
		imap_folder: DF.Data | None
		in_reply_to: DF.Link | None
		message_id: DF.SmallText | None
		read_by_recipient: DF.Check
		read_by_recipient_on: DF.Datetime | None
		read_receipt: DF.Check
		recipients: DF.Code | None
		reference_name: DF.DynamicLink | None
		reference_owner: DF.ReadOnly | None
		reference_pagetype: DF.Link | None
		seen: DF.Check
		send_after: DF.Datetime | None
		sender: DF.Data | None
		sender_full_name: DF.Data | None
		sent_or_received: DF.Literal["Sent", "Received"]
		status: DF.Literal["Open", "Replied", "Closed", "Linked"]
		subject: DF.SmallText
		text_content: DF.Code | None
		timeline_links: DF.Table[EmailLink]
		uid: DF.Int
		unread_notification_sent: DF.Check
		user: DF.Link | None
	# end: auto-generated types
	"""Email represents an external email."""

	no_feed_on_delete = True
	PAGETYPE = "Email"

	def onload(self):
		"""create email flag queue"""
		if (
			self.sent_or_received == "Received"
			and self.uid
			and self.uid != -1
		):
			email_flag_queue = jingrow.db.get_value(
				"Email Flag Queue", {"email": self.name, "is_completed": 0}
			)
			if email_flag_queue:
				return

			jingrow.get_pg(
				{
					"pagetype": "Email Flag Queue",
					"action": "Read",
					"email": self.name,
					"uid": self.uid,
					"email_account": self.email_account,
				}
			).insert(ignore_permissions=True)
			jingrow.db.commit()

	def validate(self):
		self.validate_reference()

		if not self.user:
			self.user = jingrow.session.user

		if not self.subject:
			self.subject = strip_html((self.content or "")[:141])

		if not self.sent_or_received:
			self.seen = 1
			self.sent_or_received = "Sent"

		if not self.send_after:  # Handle empty string, always set NULL
			self.send_after = None

		validate_email(self)

		self.parse_email_for_timeline_links()
		self.set_timeline_links()
		self.deduplicate_timeline_links()

		self.set_sender_full_name()

		if self.is_new():
			self.set_status()
			self.mark_email_as_spam()

	def validate_reference(self):
		if self.reference_pagetype and self.reference_name:
			if not self.reference_owner:
				self.reference_owner = jingrow.db.get_value(
					self.reference_pagetype, self.reference_name, "owner"
				)

			# prevent email against a child table
			if jingrow.get_meta(self.reference_pagetype).istable:
				jingrow.throw(
					_("Cannot create a {0} against a child document: {1}").format(
						_("Email"), _(self.reference_pagetype)
					)
				)

			# Prevent circular linking of Email PageTypes
			if self.reference_pagetype == "Email":
				circular_linking = False
				pg = get_parent_pg(self)
				while pg.reference_pagetype == "Email":
					if get_parent_pg(pg).name == self.name:
						circular_linking = True
						break
					pg = get_parent_pg(pg)

				if circular_linking:
					jingrow.throw(
						_("Please make sure the Reference Email Pgs are not circularly linked."),
						jingrow.CircularLinkingError,
					)

	def after_insert(self):
		if not (self.reference_pagetype and self.reference_name):
			return

		if self.reference_pagetype == "Email" and self.sent_or_received == "Sent":
			jingrow.db.set_value("Email", self.reference_name, "status", "Replied")

		self.notify_change("add")

	def set_signature_in_email_content(self):
		"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email"""
		if not self.content:
			return

		soup = BeautifulSoup(self.content, "html.parser")
		email_body = soup.find("div", {"class": "ql-editor read-mode"})

		if not email_body:
			return

		user_email_signature = (
			jingrow.db.get_value(
				"User",
				self.sender,
				"email_signature",
			)
			if self.sender
			else None
		)

		signature = user_email_signature or jingrow.db.get_value(
			"Email Account",
			{"default_outgoing": 1, "add_signature": 1},
			"signature",
		)

		if not signature:
			return

		soup = BeautifulSoup(signature, "html.parser")
		html_signature = soup.find("div", {"class": "ql-editor read-mode"})
		_signature = None
		if html_signature:
			_signature = html_signature.renderContents()

		if (cstr(_signature) or signature) not in self.content:
			self.content = f'{self.content}</p><br><p class="signature">{signature}'

	def before_save(self):
		if not self.flags.skip_add_signature:
			self.set_signature_in_email_content()

	def on_update(self):
		# add to _comment property of the pagetype, so it shows up in
		# comments count for the list view
		update_comment_in_pg(self)

		parent = get_parent_pg(self)
		if (method := getattr(parent, "on_email_update", None)) and callable(method):
			parent.on_email_update(self)
			return

		update_parent_document_on_email(self)

	def on_trash(self):
		self.notify_change("delete")

	@property
	def sender_mailid(self):
		return parse_addr(self.sender)[1] if self.sender else ""

	@staticmethod
	def _get_emails_list(emails=None, exclude_displayname=False):
		"""Returns list of emails from given email string.

		* Removes duplicate mailids
		* Removes display name from email address if exclude_displayname is True
		"""
		emails = split_emails(emails) if isinstance(emails, str) else (emails or [])
		if exclude_displayname:
			return [email.lower() for email in {parse_addr(email)[1] for email in emails} if email]
		return [email for email in set(emails) if email]

	def to_list(self, exclude_displayname=True):
		"""Returns to list."""
		return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname)

	def cc_list(self, exclude_displayname=True):
		"""Returns cc list."""
		return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname)

	def bcc_list(self, exclude_displayname=True):
		"""Returns bcc list."""
		return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname)

	def get_attachments(self):
		return jingrow.get_all(
			"File",
			fields=["name", "file_name", "file_url", "is_private"],
			filters={
				"attached_to_name": self.name,
				"attached_to_pagetype": self.PAGETYPE,
			},
		)

	def notify_change(self, action):
		jingrow.publish_realtime(
			"pageinfo_update",
			{"pg": self.as_dict(), "key": "emails", "action": action},
			pagetype=self.reference_pagetype,
			pagename=self.reference_name,
			after_commit=True,
		)

	def set_status(self):
		if self.reference_pagetype and self.reference_name:
			self.status = "Linked"
		else:
			self.status = "Open"

		if self.send_after and self.is_new():
			self.delivery_status = "Scheduled"

	def mark_email_as_spam(self):
		if (
			self.sent_or_received == "Received"
			and jingrow.db.exists("Email Rule", {"email_id": self.sender, "is_spam": 1})
		):
			self.email_status = "Spam"

	@classmethod
	def find(cls, name, ignore_error=False):
		try:
			return jingrow.get_pg(cls.PAGETYPE, name)
		except jingrow.DoesNotExistError:
			if ignore_error:
				return
			raise

	@classmethod
	def find_one_by_filters(cls, *, order_by=None, **kwargs):
		name = jingrow.db.get_value(cls.PAGETYPE, kwargs, order_by=order_by)
		return cls.find(name) if name else None

	def update_db(self, **kwargs):
		jingrow.db.set_value(self.PAGETYPE, self.name, kwargs)

	def set_sender_full_name(self):
		if not self.sender_full_name and self.sender:
			if self.sender == "Administrator":
				self.sender_full_name = jingrow.db.get_value("User", "Administrator", "full_name")
				self.sender = jingrow.db.get_value("User", "Administrator", "email")
			elif self.sender == "Guest":
				self.sender_full_name = self.sender
				self.sender = None
			else:
				if self.sent_or_received == "Sent":
					validate_email_address(self.sender, throw=True)
				sender_name, sender_email = parse_addr(self.sender)
				if sender_name == sender_email:
					sender_name = None

				self.sender = sender_email
				self.sender_full_name = sender_name

				if not self.sender_full_name:
					self.sender_full_name = jingrow.db.get_value("User", self.sender, "full_name")

				if not self.sender_full_name:
					first_name, last_name = jingrow.db.get_value(
						"Contact", filters={"email_id": sender_email}, fieldname=["first_name", "last_name"]
					) or [None, None]
					self.sender_full_name = (first_name or "") + (last_name or "")

				if not self.sender_full_name:
					self.sender_full_name = sender_email

	def set_delivery_status(self, commit=False):
		"""Look into the status of Email Queue linked to this Email and set the Delivery Status of this Email"""
		delivery_status = None
		status_counts = Counter(
			jingrow.get_all("Email Queue", pluck="status", filters={"email": self.name})
		)
		if self.sent_or_received == "Received":
			return

		if status_counts.get("Not Sent") or status_counts.get("Sending"):
			delivery_status = "Sending"

		elif status_counts.get("Error"):
			delivery_status = "Error"

		elif status_counts.get("Expired"):
			delivery_status = "Expired"

		elif status_counts.get("Sent"):
			delivery_status = "Sent"

		if delivery_status:
			self.db_set("delivery_status", delivery_status)
			self.notify_change("update")

			# for list views and forms
			self.notify_update()

			if commit:
				jingrow.db.commit()

	def parse_email_for_timeline_links(self):
		if not jingrow.db.get_value("Email Account", filters={"enable_automatic_linking": 1}):
			return

		for pagetype, pagename in parse_email([self.recipients, self.cc, self.bcc]):
			if not jingrow.db.get_value(pagetype, pagename, ignore=True):
				continue

			self.add_link(pagetype, pagename)

			if not self.reference_pagetype:
				self.reference_pagetype = pagetype
				self.reference_name = pagename

	# Timeline Links
	def set_timeline_links(self):
		contacts = []
		create_contact_enabled = self.email_account and jingrow.db.get_value(
			"Email Account", self.email_account, "create_contact"
		)
		contacts = get_contacts(
			[self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled
		)

		for contact_name in contacts:
			self.add_link("Contact", contact_name)

			# link contact's dynamic links to email
			add_contact_links_to_email(self, contact_name)

	def deduplicate_timeline_links(self):
		if not self.timeline_links:
			return

		unique_links = {(link.link_pagetype, link.link_name) for link in self.timeline_links}
		self.timeline_links = []
		for pagetype, name in unique_links:
			self.add_link(pagetype, name)

	def add_link(self, link_pagetype, link_name, autosave=False):
		self.append("timeline_links", {"link_pagetype": link_pagetype, "link_name": link_name})

		if autosave:
			self.save(ignore_permissions=True)

	def get_links(self):
		return self.timeline_links

	def remove_link(self, link_pagetype, link_name, autosave=False, ignore_permissions=True):
		for l in list(self.timeline_links):
			if l.link_pagetype == link_pagetype and l.link_name == link_name:
				self.timeline_links.remove(l)

		if autosave:
			self.save(ignore_permissions=ignore_permissions)


def on_pagetype_update():
	"""Add indexes in `tabEmail`"""
	jingrow.db.add_index("Email", ["reference_pagetype", "reference_name"])
	jingrow.db.add_index("Email", ["status"])
	jingrow.db.add_index("Email", ["message_id(140)"])


def has_permission(pg, ptype, user=None, debug=False):
	if ptype == "read":
		if pg.reference_pagetype == "Email" and pg.reference_name == pg.name:
			return

		if pg.reference_pagetype and pg.reference_name:
			return jingrow.has_permission(
				pg.reference_pagetype, ptype="read", pg=pg.reference_name, user=user, debug=debug
			)


def get_permission_query_conditions_for_email(user):
	if not user:
		user = jingrow.session.user

	roles = jingrow.get_roles(user)

	if "Super Email User" in roles or "System Manager" in roles:
		return None
	else:
		accounts = jingrow.get_all(
			"User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx"
		)

		if not accounts:
			return "1=0"

		email_accounts = ['"%s"' % account.get("email_account") for account in accounts]
		return """`tabEmail`.email_account in ({email_accounts})""".format(
			email_accounts=",".join(email_accounts)
		)


def get_contacts(email_strings: list[str], auto_create_contact=False) -> list[str]:
	email_addrs = get_emails(email_strings)
	contacts = []
	for email in email_addrs:
		email = get_email_without_link(email)
		contact_name = get_contact_name(email)

		if not contact_name and email and auto_create_contact:
			email_parts = email.split("@")
			first_name = jingrow.unscrub(email_parts[0])

			try:
				contact_name = f"{first_name}-{email_parts[1]}" if first_name == "Contact" else first_name
				contact = jingrow.get_pg(
					{"pagetype": "Contact", "first_name": contact_name, "name": contact_name}
				)
				contact.add_email(email_id=email, is_primary=True)
				contact.insert(ignore_permissions=True)
				contact_name = contact.name
			except Exception:
				contact_name = None
				contact.log_error("Unable to add contact")

		if contact_name:
			contacts.append(contact_name)

	return contacts


def get_emails(email_strings: list[str]) -> list[str]:
	email_addrs = []

	for email_string in email_strings:
		if email_string:
			result = getaddresses([email_string])
			email_addrs.extend(email[1] for email in result)
	return email_addrs


def add_contact_links_to_email(email, contact_name):
	contact_links = jingrow.get_all(
		"Dynamic Link",
		filters={"parenttype": "Contact", "parent": contact_name},
		fields=["link_pagetype", "link_name"],
	)

	if contact_links:
		for contact_link in contact_links:
			email.add_link(contact_link.link_pagetype, contact_link.link_name)


def parse_email(email_strings):
	"""
	Parse email to add timeline links.
	When automatic email linking is enabled, an email from email_strings can contain
	a pagetype and pagename ie in the format `admin+pagetype+pagename@example.com` or `admin+pagetype=pagename@example.com`,
	the email is parsed and pagetype and pagename is extracted.
	"""
	for email_string in email_strings:
		if not email_string:
			continue

		for email in email_string.split(","):
			email_username = email.split("@", 1)[0]
			email_local_parts = email_username.split("+")
			pagename = pagetype = None
			if len(email_local_parts) == 3:
				pagetype = unquote(email_local_parts[1])
				pagename = unquote(email_local_parts[2])

			elif len(email_local_parts) == 2:
				document_parts = email_local_parts[1].split("=", 1)
				if len(document_parts) != 2:
					continue

				pagetype = unquote(document_parts[0])
				pagename = unquote(document_parts[1])

			if pagetype and pagename:
				yield pagetype, pagename


def get_email_without_link(email):
	"""
	returns email address without pagetype links
	returns admin@example.com for email admin+pagetype+pagename@example.com
	"""
	if not jingrow.get_all("Email Account", filters={"enable_automatic_linking": 1}):
		return email

	try:
		_email = email.split("@")
		email_id = _email[0].split("+", 1)[0]
		email_host = _email[1]
	except IndexError:
		return email

	return f"{email_id}@{email_host}"


def update_parent_document_on_email(pg):
	"""Update mins_to_first_email of parent document based on who is replying."""

	parent = get_parent_pg(pg)
	if not parent:
		return

	status_field = parent.meta.get_field("status")
	if status_field:
		options = (status_field.options or "").splitlines()

		# if status has a "Open" option and status is "Replied", then update the status for received email
		if (
			("Open" in options)
			and parent.status == "Replied"
			and pg.sent_or_received == "Received"
			or (
				parent.pagetype == "Issue" and ("Open" in options) and pg.sent_or_received == "Received"
			)  # For 'Issue', current status is not considered.
		):
			parent.db_set("status", "Open")
			parent.run_method("handle_hold_time", "Replied")
			apply_assignment_rule(parent)

	update_first_response_time(parent, pg)
	set_avg_response_time(parent, pg)
	parent.run_method("notify_email", pg)
	parent.notify_update()


def update_first_response_time(parent, email):
	if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
		if (
			is_system_user(email.sender)
			or jingrow.get_cached_value("User", jingrow.session.user, "user_type") == "System User"
		):
			if email.sent_or_received == "Sent":
				first_responded_on = email.creation
				if parent.meta.has_field("first_responded_on"):
					parent.db_set("first_responded_on", first_responded_on)
				first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2)
				parent.db_set("first_response_time", first_response_time)


def set_avg_response_time(parent, email):
	if parent.meta.has_field("avg_response_time") and email.sent_or_received == "Sent":
		# avg response time for all the responses
		emails = jingrow.get_list(
			"Email",
			filters={"reference_pagetype": parent.pagetype, "reference_name": parent.name},
			fields=["sent_or_received", "name", "creation"],
			order_by="creation",
		)

		if len(emails):
			response_times = []
			for i in range(len(emails)):
				if (
					emails[i].sent_or_received == "Sent"
					and emails[i - 1].sent_or_received == "Received"
				):
					response_time = round(
						time_diff_in_seconds(emails[i].creation, emails[i - 1].creation), 2
					)
					if response_time > 0:
						response_times.append(response_time)
			if response_times:
				avg_response_time = sum(response_times) / len(response_times)
				parent.db_set("avg_response_time", avg_response_time)
