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

from typing import Protocol, runtime_checkable

import jingrow
from jingrow import _
from jingrow.model.base_page import get_controller
from jingrow.model.page import Page
from jingrow.utils import cint
from jingrow.utils.caching import site_cache


@runtime_checkable
class LogType(Protocol):
	"""Interface requirement for pagetypes that can be cleared using log settings."""

	@staticmethod
	def clear_old_logs(days: int) -> None:
		...


@site_cache
def _supports_log_clearing(pagetype: str) -> bool:
	try:
		controller = get_controller(pagetype)
		return issubclass(controller, LogType)
	except Exception:
		return False


class LogSettings(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.core.pagetype.logs_to_clear.logs_to_clear import LogsToClear
		from jingrow.types import DF

		logs_to_clear: DF.Table[LogsToClear]

	# end: auto-generated types
	def validate(self):
		self.remove_unsupported_pagetypes()
		self._deduplicate_entries()
		self.add_default_logtypes()

	def remove_unsupported_pagetypes(self):
		for entry in list(self.logs_to_clear):
			if _supports_log_clearing(entry.ref_pagetype):
				continue

			msg = _("{} does not support automated log clearing.").format(jingrow.bold(entry.ref_pagetype))
			if jingrow.conf.developer_mode:
				msg += "<br>" + _("Implement `clear_old_logs` method to enable auto error clearing.")
			jingrow.msgprint(msg, title=_("PageType not supported by Log Settings."))
			self.remove(entry)

	def _deduplicate_entries(self):
		seen = set()
		for entry in list(self.logs_to_clear):
			if entry.ref_pagetype in seen:
				self.remove(entry)
			seen.add(entry.ref_pagetype)

	def add_default_logtypes(self):
		existing_logtypes = {d.ref_pagetype for d in self.logs_to_clear}
		added_logtypes = set()
		default_logtypes_retention = jingrow.get_hooks("default_log_clearing_pagetypes", {})

		for logtype, retentions in default_logtypes_retention.items():
			if logtype not in existing_logtypes and _supports_log_clearing(logtype):
				if not jingrow.db.exists("PageType", logtype):
					continue

				self.append("logs_to_clear", {"ref_pagetype": logtype, "days": cint(retentions[-1])})
				added_logtypes.add(logtype)

		if added_logtypes:
			jingrow.msgprint(_("Added default log pagetypes: {}").format(",".join(added_logtypes)), alert=True)

	def clear_logs(self):
		"""
		Log settings can clear any log type that's registered to it and provides a method to delete old logs.

		Check `LogPagetype` above for interface that pagetypes need to implement.
		"""

		for entry in self.logs_to_clear:
			controller: LogType = get_controller(entry.ref_pagetype)
			func = controller.clear_old_logs

			# Only pass what the method can handle, this is considering any
			# future addition that might happen to the required interface.
			kwargs = jingrow.get_newargs(func, {"days": entry.days})
			func(**kwargs)
			jingrow.db.commit()

	def register_pagetype(self, pagetype: str, days=30):
		existing_logtypes = {d.ref_pagetype for d in self.logs_to_clear}

		if pagetype not in existing_logtypes and _supports_log_clearing(pagetype):
			self.append("logs_to_clear", {"ref_pagetype": pagetype, "days": cint(days)})
		else:
			for entry in self.logs_to_clear:
				if entry.ref_pagetype == pagetype:
					entry.days = days
					break


def run_log_clean_up():
	pg = jingrow.get_pg("Log Settings")
	pg.remove_unsupported_pagetypes()
	pg.add_default_logtypes()
	pg.save()
	pg.clear_logs()


@jingrow.whitelist()
def has_unseen_error_log():
	if jingrow.get_all("Error Log", filters={"seen": 0}, limit=1):
		return {
			"show_alert": True,
			"message": _("You have unseen {0}").format(
				'<a href="/app/List/Error%20Log/List"> Error Logs </a>'
			),
		}


@jingrow.whitelist()
@jingrow.validate_and_sanitize_search_inputs
def get_log_pagetypes(pagetype, txt, searchfield, start, page_len, filters):
	filters = filters or {}

	filters.extend(
		[
			["istable", "=", 0],
			["issingle", "=", 0],
			["name", "like", f"%%{txt}%%"],
		]
	)
	pagetypes = jingrow.get_list("PageType", filters=filters, pluck="name")

	supported_pagetypes = [(d,) for d in pagetypes if _supports_log_clearing(d)]

	return supported_pagetypes[start:page_len]


LOG_PAGETYPES = [
	"Scheduled Job Log",
	"System Log",
	"Route History",
	"Email Queue",
	"Email Queue Recipient",
	"Error Log",
]


def clear_log_table(pagetype, days=90):
	"""If any logtype table grows too large then clearing it with DELETE query
	is not feasible in reasonable time. This command copies recent data to new
	table and replaces current table with new smaller table.

	ref: https://mariadb.com/kb/en/big-deletes/#deleting-more-than-half-a-table
	"""
	from jingrow.utils import get_table_name

	if pagetype not in LOG_PAGETYPES:
		raise jingrow.ValidationError(f"Unsupported logging PageType: {pagetype}")

	original = get_table_name(pagetype)
	temporary = f"{original} temp_table"
	backup = f"{original} backup_table"

	try:
		jingrow.db.sql_ddl(f"CREATE TABLE `{temporary}` LIKE `{original}`")

		# Copy all recent data to new table
		jingrow.db.sql(
			f"""INSERT INTO `{temporary}`
				SELECT * FROM `{original}`
				WHERE `{original}`.`modified` > NOW() - INTERVAL '{days}' DAY"""
		)
		jingrow.db.sql_ddl(f"RENAME TABLE `{original}` TO `{backup}`, `{temporary}` TO `{original}`")
	except Exception:
		jingrow.db.rollback()
		jingrow.db.sql_ddl(f"DROP TABLE IF EXISTS `{temporary}`")
		raise
	else:
		jingrow.db.sql_ddl(f"DROP TABLE `{backup}`")
