# Copyright (c) 2020, Frappe Technologies and contributors
# License: MIT. See LICENSE

from datetime import date, datetime
from typing import Any

import frappe
from frappe import _
from frappe.boot import get_allowed_report_names
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
from frappe.permissions import get_doctypes_with_read
from frappe.query_builder import Criterion
from frappe.query_builder.utils import DocType
from frappe.utils import flt
from frappe.utils.modules import get_modules_from_all_apps_for_user


class NumberCard(Document):
	# 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 frappe.types import DF

		aggregate_function_based_on: DF.Literal[None]
		background_color: DF.Color | None
		color: DF.Color | None
		currency: DF.Link | None
		document_type: DF.Link | None
		dynamic_filters_json: DF.Code | None
		filters_config: DF.Code | None
		filters_json: DF.Code | None
		function: DF.Literal["Count", "Sum", "Average", "Minimum", "Maximum"]
		is_public: DF.Check
		is_standard: DF.Check
		label: DF.Data
		method: DF.Data | None
		module: DF.Link | None
		parent_document_type: DF.Link | None
		report_field: DF.Literal[None]
		report_function: DF.Literal["Sum", "Average", "Minimum", "Maximum"]
		report_name: DF.Link | None
		show_full_number: DF.Check
		show_percentage_stats: DF.Check
		stats_time_interval: DF.Literal["Daily", "Weekly", "Monthly", "Yearly"]
		type: DF.Literal["Document Type", "Report", "Custom"]
	# end: auto-generated types

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

		if frappe.db.exists("Number Card", self.name):
			self.name = append_number_if_name_exists("Number Card", self.name)

	def validate(self):
		if self.type == "Document Type":
			if not (self.document_type and self.function):
				frappe.throw(_("Document Type and Function are required to create a number card"))

			if self.function != "Count" and not self.aggregate_function_based_on:
				frappe.throw(_("Aggregate Field is required to create a number card"))

			if frappe.get_meta(self.document_type).istable and not self.parent_document_type:
				frappe.throw(_("Parent Document Type is required to create a number card"))

		elif self.type == "Report":
			if not (self.report_name and self.report_field and self.function):
				frappe.throw(_("Report Name, Report Field and Fucntion are required to create a number card"))

		elif self.type == "Custom":
			if not self.method:
				frappe.throw(_("Method is required to create a number card"))

	def on_update(self):
		if frappe.conf.developer_mode and self.is_standard:
			export_to_files(record_list=[["Number Card", self.name]], record_module=self.module)


def get_permission_query_conditions(user=None):
	# The user param is ignored because `get_allowed_report_names` and `get_doctypes_with_read` don't support it.
	if frappe.session.user == "Administrator":
		return

	if "System Manager" in frappe.get_roles():
		return

	allowed_reports = get_allowed_report_names()
	allowed_doctypes = get_doctypes_with_read()
	allowed_modules = [module.get("module_name") for module in get_modules_from_all_apps_for_user()]

	nc = frappe.qb.DocType("Number Card")
	conditions = (
		((nc.type == "Report") & nc.report_name.isin(allowed_reports))
		| ((nc.type == "Custom") & nc.document_type.isin(allowed_doctypes))
		| ((nc.type == "Document Type") & nc.document_type.isin(allowed_doctypes))
	) & (nc.module.isin(allowed_modules) | nc.module.isnull() | nc.module == "")

	return conditions.get_sql(quote_char="`")


def has_permission(doc, ptype, user):
	# The user param is ignored because `get_allowed_report_names` and `get_doctypes_with_read` don't support it.
	if frappe.session.user == "Administrator":
		return True

	if "System Manager" in frappe.get_roles():
		return True

	if doc.type == "Report" and doc.report_name in get_allowed_report_names():
		return True

	if doc.type == "Custom" and doc.document_type in get_doctypes_with_read():
		return True

	if doc.type == "Document Type" and doc.document_type in get_doctypes_with_read():
		return True

	return False


@frappe.whitelist()
def get_result(
	doc: str | dict[str, Any] | Document,
	filters: str | list | dict[str, Any],
	to_date: str | datetime | date | None = None,
):
	doc = frappe.parse_json(doc)
	fields = []
	sql_function_map = {
		"Count": "COUNT",
		"Sum": "SUM",
		"Average": "AVG",
		"Minimum": "MIN",
		"Maximum": "MAX",
	}

	function = sql_function_map[doc.function]

	if function == "COUNT":
		arg = "*"
	else:
		arg = doc.aggregate_function_based_on

	fields = [{function: arg, "as": "result"}]

	if not filters:
		filters = []
	elif isinstance(filters, str):
		filters = frappe.parse_json(filters)

	if to_date:
		filters.append([doc.document_type, "creation", "<", to_date])

	res = frappe.get_list(
		doc.document_type, fields=fields, filters=filters, parent_doctype=doc.parent_document_type
	)
	number = res[0]["result"] if res else 0

	return flt(number)


@frappe.whitelist()
def get_percentage_difference(
	doc: str | dict[str, Any], filters: str | list | dict[str, Any], result: float | int | str
):
	doc = frappe.parse_json(doc)
	result = frappe.parse_json(result)

	doc = frappe.get_doc("Number Card", doc.name)

	if not doc.get("show_percentage_stats"):
		return

	previous_result = calculate_previous_result(doc, filters)
	if previous_result == 0:
		return None
	else:
		if result == previous_result:
			return 0
		else:
			return ((result / previous_result) - 1) * 100.0


def calculate_previous_result(doc, filters):
	from frappe.utils import add_to_date

	current_date = frappe.utils.now()
	if doc.stats_time_interval == "Daily":
		previous_date = add_to_date(current_date, days=-1)
	elif doc.stats_time_interval == "Weekly":
		previous_date = add_to_date(current_date, weeks=-1)
	elif doc.stats_time_interval == "Monthly":
		previous_date = add_to_date(current_date, months=-1)
	else:
		previous_date = add_to_date(current_date, years=-1)

	return get_result(doc, filters, previous_date)


@frappe.whitelist()
def create_number_card(args: str | dict[str, Any]):
	args = frappe.parse_json(args)
	doc = frappe.new_doc("Number Card")

	doc.update(args)
	doc.insert(ignore_permissions=True)
	return doc


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_cards_for_user(
	doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: str | list | dict[str, Any]
):
	doctype = "Number Card"
	meta = frappe.get_meta(doctype)
	searchfields = meta.get_search_fields()
	search_conditions = []

	numberCard = DocType("Number Card")

	if txt:
		search_conditions = [numberCard[field].like(f"%{txt}%") for field in searchfields]

	condition_query = frappe.qb.get_query(
		doctype,
		filters=filters,
	)

	return (
		condition_query.select(numberCard.name, numberCard.label, numberCard.document_type)
		.where((numberCard.owner == frappe.session.user) | (numberCard.is_public == 1))
		.where(Criterion.any(search_conditions))
	).run()


@frappe.whitelist()
def create_report_number_card(args: str | dict[str, Any]):
	card = create_number_card(args)
	args = frappe.parse_json(args)
	args.name = card.name
	if args.dashboard:
		add_card_to_dashboard(frappe.as_json(args))


@frappe.whitelist()
def add_card_to_dashboard(args: str | dict[str, Any]):
	args = frappe.parse_json(args)

	dashboard = frappe.get_doc("Dashboard", args.dashboard)
	dashboard_link = frappe.new_doc("Number Card Link")
	dashboard_link.card = args.name

	if args.set_standard and dashboard.is_standard:
		card = frappe.get_doc("Number Card", dashboard_link.card)
		card.is_standard = 1
		card.module = dashboard.module
		card.save()

	dashboard.append("cards", dashboard_link)
	dashboard.save()
