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

from collections.abc import Iterable

import jingrow
from jingrow import _
from jingrow.cache_manager import clear_pagetype_map, get_pagetype_map
from jingrow.desk.form import assign_to
from jingrow.model import log_types
from jingrow.model.page import Page
from jingrow.utils.data import comma_and


class AssignmentRule(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.automation.pagetype.assignment_rule_day.assignment_rule_day import AssignmentRuleDay
		from jingrow.automation.pagetype.assignment_rule_user.assignment_rule_user import AssignmentRuleUser
		from jingrow.types import DF

		assign_condition: DF.Code
		assignment_days: DF.Table[AssignmentRuleDay]
		close_condition: DF.Code | None
		description: DF.SmallText
		disabled: DF.Check
		document_type: DF.Link
		due_date_based_on: DF.Literal[None]
		field: DF.Literal[None]
		last_user: DF.Link | None
		priority: DF.Int
		rule: DF.Literal["Round Robin", "Load Balancing", "Based on Field"]
		unassign_condition: DF.Code | None
		users: DF.TableMultiSelect[AssignmentRuleUser]
	# end: auto-generated types
	def validate(self):
		self.validate_document_types()
		self.validate_assignment_days()

	def clear_cache(self):
		super().clear_cache()
		clear_pagetype_map(self.pagetype, self.document_type)
		clear_pagetype_map(self.pagetype, f"due_date_rules_for_{self.document_type}")

	def validate_document_types(self):
		if self.document_type == "ToDo":
			jingrow.throw(
				_("Assignment Rule is not allowed on document type {0}").format(jingrow.bold(_("ToDo")))
			)

	def validate_assignment_days(self):
		assignment_days = self.get_assignment_days()
		if len(set(assignment_days)) != len(assignment_days):
			jingrow.throw(
				_("The following Assignment Days have been repeated: {0}").format(
					comma_and([_(day) for day in get_repeated(assignment_days)], add_quotes=False)
				)
			)

	def apply_unassign(self, pg, assignments):
		if self.unassign_condition and self.name in [d.assignment_rule for d in assignments]:
			return self.clear_assignment(pg)

		return False

	def apply_assign(self, pg):
		if self.safe_eval("assign_condition", pg):
			return self.do_assignment(pg)

	def do_assignment(self, pg):
		# clear existing assignment, to reassign
		assign_to.clear(pg.get("pagetype"), pg.get("name"), ignore_permissions=True)

		user = self.get_user(pg)

		if user:
			assign_to.add(
				dict(
					assign_to=[user],
					pagetype=pg.get("pagetype"),
					name=pg.get("name"),
					description=jingrow.render_template(self.description, pg),
					assignment_rule=self.name,
					notify=True,
					date=pg.get(self.due_date_based_on) if self.due_date_based_on else None,
				),
				ignore_permissions=True,
			)

			# set for reference in round robin
			self.db_set("last_user", user)
			return True

		return False

	def clear_assignment(self, pg):
		"""Clear assignments"""
		if self.safe_eval("unassign_condition", pg):
			return assign_to.clear(pg.get("pagetype"), pg.get("name"), ignore_permissions=True)

	def close_assignments(self, pg):
		"""Close assignments"""
		if self.safe_eval("close_condition", pg):
			return assign_to.close_all_assignments(
				pg.get("pagetype"), pg.get("name"), ignore_permissions=True
			)

	def get_user(self, pg):
		"""
		Get the next user for assignment
		"""
		if self.rule == "Round Robin":
			return self.get_user_round_robin()
		elif self.rule == "Load Balancing":
			return self.get_user_load_balancing()
		elif self.rule == "Based on Field":
			return self.get_user_based_on_field(pg)

	def get_user_round_robin(self):
		"""
		Get next user based on round robin
		"""

		# first time, or last in list, pick the first
		if not self.last_user or self.last_user == self.users[-1].user:
			return self.users[0].user

		# find out the next user in the list
		for i, d in enumerate(self.users):
			if self.last_user == d.user:
				return self.users[i + 1].user

		# bad last user, assign to the first one
		return self.users[0].user

	def get_user_load_balancing(self):
		"""Assign to the user with least number of open assignments"""
		counts = [
			dict(
				user=d.user,
				count=jingrow.db.count(
					"ToDo",
					dict(
						reference_type=self.document_type,
						allocated_to=d.user,
						status="Open",
					),
				),
			)
			for d in self.users
		]
		# sort by dict value
		sorted_counts = sorted(counts, key=lambda k: k["count"])

		# pick the first user
		return sorted_counts[0].get("user")

	def get_user_based_on_field(self, pg):
		val = pg.get(self.field)
		if jingrow.db.exists("User", val):
			return val

	def safe_eval(self, fieldname, pg):
		try:
			if self.get(fieldname):
				return jingrow.safe_eval(self.get(fieldname), None, pg)
		except Exception as e:
			# when assignment fails, don't block the document as it may be
			# a part of the email pulling
			jingrow.msgprint(jingrow._("Auto assignment failed: {0}").format(str(e)), indicator="orange")

		return False

	def get_assignment_days(self):
		return [d.day for d in self.get("assignment_days", [])]

	def is_rule_not_applicable_today(self):
		today = jingrow.flags.assignment_day or jingrow.utils.get_weekday()
		assignment_days = self.get_assignment_days()
		return assignment_days and today not in assignment_days


def get_assignments(pg) -> list[dict]:
	return jingrow.get_all(
		"ToDo",
		fields=["name", "assignment_rule"],
		filters=dict(
			reference_type=pg.get("pagetype"), reference_name=pg.get("name"), status=("!=", "Cancelled")
		),
		limit=5,
	)


@jingrow.whitelist()
def bulk_apply(pagetype, pagenames):
	pagenames = jingrow.parse_json(pagenames)
	background = len(pagenames) > 5

	for name in pagenames:
		if background:
			jingrow.enqueue(
				"jingrow.automation.pagetype.assignment_rule.assignment_rule.apply",
				pg=None,
				pagetype=pagetype,
				name=name,
			)
		else:
			apply(pagetype=pagetype, name=name)


def reopen_closed_assignment(pg):
	todo_list = jingrow.get_all(
		"ToDo",
		filters={
			"reference_type": pg.pagetype,
			"reference_name": pg.name,
			"status": "Closed",
		},
		pluck="name",
	)

	for todo in todo_list:
		todo_pg = jingrow.get_pg("ToDo", todo)
		todo_pg.status = "Open"
		todo_pg.save(ignore_permissions=True)

	return bool(todo_list)


def apply(pg=None, method=None, pagetype=None, name=None):
	pagetype = pagetype or pg.pagetype

	skip_assignment_rules = (
		jingrow.flags.in_patch
		or jingrow.flags.in_install
		or jingrow.flags.in_setup_wizard
		or pagetype in log_types
	)

	if skip_assignment_rules:
		return

	if not pg and pagetype and name:
		pg = jingrow.get_pg(pagetype, name)

	assignment_rules = get_pagetype_map(
		"Assignment Rule",
		pg.pagetype,
		filters={"document_type": pg.pagetype, "disabled": 0},
		order_by="priority desc",
	)

	# multiple auto assigns
	assignment_rule_pgs: list[AssignmentRule] = [
		jingrow.get_cached_pg("Assignment Rule", d.get("name")) for d in assignment_rules
	]

	if not assignment_rule_pgs:
		return

	pg = pg.as_dict()
	assignments = get_assignments(pg)

	clear = True  # are all assignments cleared
	new_apply = False  # are new assignments applied

	if assignments:
		# first unassign
		# use case, there are separate groups to be assigned for say L1 and L2,
		# so when the value switches from L1 to L2, L1 team must be unassigned, then L2 can be assigned.
		clear = False
		for assignment_rule in assignment_rule_pgs:
			if assignment_rule.is_rule_not_applicable_today():
				continue

			clear = assignment_rule.apply_unassign(pg, assignments)
			if clear:
				break

	# apply rule only if there are no existing assignments
	if clear:
		for assignment_rule in assignment_rule_pgs:
			if assignment_rule.is_rule_not_applicable_today():
				continue

			new_apply = assignment_rule.apply_assign(pg)
			if new_apply:
				break

	# apply close rule only if assignments exists
	assignments = get_assignments(pg)

	if assignments:
		for assignment_rule in assignment_rule_pgs:
			if assignment_rule.is_rule_not_applicable_today():
				continue

			if not new_apply:
				# only reopen if close condition is not satisfied
				to_close_todos = assignment_rule.safe_eval("close_condition", pg)

				if to_close_todos:
					# close todo status
					todos_to_close = jingrow.get_all(
						"ToDo",
						filters={
							"reference_type": pg.pagetype,
							"reference_name": pg.name,
						},
						pluck="name",
					)

					for todo in todos_to_close:
						_todo = jingrow.get_pg("ToDo", todo)
						_todo.status = "Closed"
						_todo.save(ignore_permissions=True)
					break

				else:
					reopened = reopen_closed_assignment(pg)
					if reopened:
						break

			assignment_rule.close_assignments(pg)


def update_due_date(pg, state=None):
	"""Run on_update on every Page (via hooks.py)"""
	skip_document_update = (
		jingrow.flags.in_migrate
		or jingrow.flags.in_patch
		or jingrow.flags.in_import
		or jingrow.flags.in_setup_wizard
		or jingrow.flags.in_install
	)

	if skip_document_update:
		return

	assignment_rules = get_pagetype_map(
		pagetype="Assignment Rule",
		name=f"due_date_rules_for_{pg.pagetype}",
		filters={
			"due_date_based_on": ["is", "set"],
			"document_type": pg.pagetype,
			"disabled": 0,
		},
	)

	for rule in assignment_rules:
		rule_pg = jingrow.get_cached_pg("Assignment Rule", rule.get("name"))
		due_date_field = rule_pg.due_date_based_on
		field_updated = (
			pg.meta.has_field(due_date_field) and pg.has_value_changed(due_date_field) and rule.get("name")
		)

		if field_updated:
			assignment_todos = jingrow.get_all(
				"ToDo",
				filters={
					"assignment_rule": rule.get("name"),
					"reference_type": pg.pagetype,
					"reference_name": pg.name,
					"status": "Open",
				},
				pluck="name",
			)

			for todo in assignment_todos:
				todo_pg = jingrow.get_pg("ToDo", todo)
				todo_pg.date = pg.get(due_date_field)
				todo_pg.flags.updater_reference = {
					"pagetype": "Assignment Rule",
					"pagename": rule.get("name"),
					"label": _("via Assignment Rule"),
				}
				todo_pg.save(ignore_permissions=True)


def get_assignment_rules() -> list[str]:
	return jingrow.get_all("Assignment Rule", filters={"disabled": 0}, pluck="document_type")


def get_repeated(values: Iterable) -> list:
	unique = set()
	repeated = set()

	for value in values:
		if value in unique:
			repeated.add(value)
		else:
			unique.add(value)

	return [str(x) for x in repeated]
