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

import copy
import json
import os
import re
from typing import TYPE_CHECKING, Optional

import jingrow
from jingrow import _, cstr, get_module_path
from jingrow.core.pagetype.access_log.access_log import make_access_log
from jingrow.core.pagetype.page_share_key.page_share_key import is_expired
from jingrow.utils import cint, escape_html, strip_html
from jingrow.utils.jinja_globals import is_rtl

if TYPE_CHECKING:
	from jingrow.model.page import Page
	from jingrow.printing.pagetype.print_format.print_format import PrintFormat

no_cache = 1

standard_format = "templates/print_formats/standard.html"


def get_context(context):
	"""Build context for print"""
	if not ((jingrow.form_dict.pagetype and jingrow.form_dict.name) or jingrow.form_dict.pg):
		return {
			"body": f"""
				<h1>Error</h1>
				<p>Parameters pagetype and name required</p>
				<pre>{escape_html(jingrow.as_json(jingrow.form_dict, indent=2))}</pre>
				"""
		}

	if jingrow.form_dict.pg:
		pg = jingrow.form_dict.pg
	else:
		pg = jingrow.get_pg(jingrow.form_dict.pagetype, jingrow.form_dict.name)

	set_link_titles(pg)

	settings = jingrow.parse_json(jingrow.form_dict.settings)

	letterhead = jingrow.form_dict.letterhead or None

	meta = jingrow.get_meta(pg.pagetype)

	print_format = get_print_format_pg(None, meta=meta)

	body = get_rendered_template(
		pg,
		print_format=print_format,
		meta=meta,
		trigger_print=jingrow.form_dict.trigger_print,
		no_letterhead=jingrow.form_dict.no_letterhead,
		letterhead=letterhead,
		settings=settings,
	)

	make_access_log(
		pagetype=jingrow.form_dict.pagetype, document=jingrow.form_dict.name, file_type="PDF", method="Print"
	)

	return {
		"body": body,
		"print_style": get_print_style(jingrow.form_dict.style, print_format),
		"comment": jingrow.session.user,
		"title": jingrow.utils.strip_html(cstr(pg.get_title() or pg.name)),
		"lang": jingrow.local.lang,
		"layout_direction": "rtl" if is_rtl() else "ltr",
		"pagetype": jingrow.form_dict.pagetype,
		"name": jingrow.form_dict.name,
		"key": jingrow.form_dict.get("key"),
		"print_format": getattr(print_format, "name", None),
		"letterhead": letterhead,
		"no_letterhead": jingrow.form_dict.no_letterhead,
		"pdf_generator": jingrow.form_dict.get("pdf_generator", "wkhtmltopdf"),
	}


def get_print_format_pg(print_format_name, meta):
	"""Returns print format document"""
	if not print_format_name:
		print_format_name = jingrow.form_dict.format or meta.default_print_format or "Standard"

	if print_format_name == "Standard":
		return None
	else:
		try:
			return jingrow.get_pg("Print Format", print_format_name)
		except jingrow.DoesNotExistError:
			# if old name, return standard!
			return None


def get_rendered_template(
	pg: "Page",
	print_format: str | None = None,
	meta=None,
	no_letterhead: bool | None = None,
	letterhead: str | None = None,
	trigger_print: bool = False,
	settings: dict | None = None,
) -> str:
	if not jingrow.flags.ignore_print_permissions:
		validate_print_permission(pg)

	print_settings = jingrow.get_single("Print Settings").as_dict()
	print_settings.update(settings or {})

	if isinstance(no_letterhead, str):
		no_letterhead = cint(no_letterhead)

	elif no_letterhead is None:
		no_letterhead = not cint(print_settings.with_letterhead)

	pg.flags.in_print = True
	pg.flags.print_settings = print_settings

	if pg.meta.is_submittable:
		if pg.pagestatus.is_draft() and not cint(print_settings.allow_print_for_draft):
			jingrow.throw(_("Not allowed to print draft documents"), jingrow.PermissionError)

		if pg.pagestatus.is_cancelled() and not cint(print_settings.allow_print_for_cancelled):
			jingrow.throw(_("Not allowed to print cancelled documents"), jingrow.PermissionError)

	pg.run_method("before_print", print_settings)

	if not hasattr(pg, "print_heading"):
		pg.print_heading = None
	if not hasattr(pg, "sub_heading"):
		pg.sub_heading = None

	if not meta:
		meta = jingrow.get_meta(pg.pagetype)

	jenv = jingrow.get_jenv()
	format_data, format_data_map = [], {}

	# determine template
	if print_format:
		pg.print_section_headings = print_format.show_section_headings
		pg.print_line_breaks = print_format.line_breaks
		pg.align_labels_right = print_format.align_labels_right
		pg.absolute_value = print_format.absolute_value

		def get_template_from_string():
			return jenv.from_string(get_print_format(pg.pagetype, print_format))

		template = None
		if hook_func := jingrow.get_hooks("get_print_format_template"):
			template = jingrow.call(hook_func[-1], jenv=jenv, print_format=print_format)

		if template:
			pass
		elif print_format.custom_format:
			template = get_template_from_string()

		elif print_format.format_data:
			# set format data
			format_data = json.loads(print_format.format_data)
			for df in format_data:
				format_data_map[df.get("fieldname")] = df
				if "visible_columns" in df:
					for _df in df.get("visible_columns"):
						format_data_map[_df.get("fieldname")] = _df

			pg.format_data_map = format_data_map

			template = "standard"

		elif print_format.standard == "Yes":
			template = get_template_from_string()

		else:
			# fallback
			template = "standard"

	else:
		template = "standard"

	if template == "standard":
		template = jenv.get_template(standard_format)

	letter_head = jingrow._dict(get_letter_head(pg, no_letterhead, letterhead) or {})

	if letter_head.content:
		letter_head.content = jingrow.utils.jinja.render_template(letter_head.content, {"pg": pg.as_dict()})
		if letter_head.header_script:
			letter_head.content += f"""
				<script>
					{ letter_head.header_script }
				</script>
			"""

	if letter_head.footer:
		letter_head.footer = jingrow.utils.jinja.render_template(letter_head.footer, {"pg": pg.as_dict()})
		if letter_head.footer_script:
			letter_head.footer += f"""
				<script>
					{ letter_head.footer_script }
				</script>
			"""

	convert_markdown(pg)

	args = {}
	# extract `print_heading_template` from the first field and remove it
	if format_data and format_data[0].get("fieldname") == "print_heading_template":
		args["print_heading_template"] = format_data.pop(0).get("options")

	args.update(
		{
			"pg": pg,
			"meta": jingrow.get_meta(pg.pagetype),
			"layout": make_layout(pg, meta, format_data),
			"no_letterhead": no_letterhead,
			"trigger_print": cint(trigger_print),
			"letter_head": letter_head.content,
			"footer": letter_head.footer,
			"print_settings": print_settings,
		}
	)
	hook_func = jingrow.get_hooks("pdf_body_html")
	html = jingrow.get_attr(hook_func[-1])(jenv=jenv, template=template, print_format=print_format, args=args)

	if cint(trigger_print):
		html += trigger_print_script

	return html


def set_link_titles(pg):
	# Adds name with title of link field pagetype to __link_titles
	if not pg.get("__link_titles"):
		setattr(pg, "__link_titles", {})

	meta = jingrow.get_meta(pg.pagetype)
	set_title_values_for_link_and_dynamic_link_fields(meta, pg)
	set_title_values_for_table_and_multiselect_fields(meta, pg)


def set_title_values_for_link_and_dynamic_link_fields(meta, pg, parent_pg=None):
	if parent_pg and not parent_pg.get("__link_titles"):
		setattr(parent_pg, "__link_titles", {})
	elif pg and not pg.get("__link_titles"):
		setattr(pg, "__link_titles", {})

	for field in meta.get_link_fields() + meta.get_dynamic_link_fields():
		if not pg.get(field.fieldname):
			continue

		# If link field, then get pagetype from options
		# If dynamic link field, then get pagetype from dependent field
		pagetype = field.options if field.fieldtype == "Link" else pg.get(field.options)

		meta = jingrow.get_meta(pagetype)
		if not meta or not (meta.title_field and meta.show_title_field_in_link):
			continue

		link_title = jingrow.get_cached_value(pagetype, pg.get(field.fieldname), meta.title_field)
		if parent_pg:
			parent_pg.__link_titles[f"{pagetype}::{pg.get(field.fieldname)}"] = link_title
		elif pg:
			pg.__link_titles[f"{pagetype}::{pg.get(field.fieldname)}"] = link_title


def set_title_values_for_table_and_multiselect_fields(meta, pg):
	for field in meta.get_table_fields():
		if not pg.get(field.fieldname):
			continue

		_meta = jingrow.get_meta(field.options)
		for value in pg.get(field.fieldname):
			set_title_values_for_link_and_dynamic_link_fields(_meta, value, pg)


def convert_markdown(pg: "Page"):
	"""Convert text field values to markdown if necessary"""
	for field in pg.meta.fields:
		if field.fieldtype == "Text Editor":
			value = pg.get(field.fieldname)
			if value and "<!-- markdown -->" in value:
				pg.set(field.fieldname, jingrow.utils.md_to_html(value))


@jingrow.whitelist()
def get_html_and_style(
	pg: str,
	name: str | None = None,
	print_format: str | None = None,
	no_letterhead: bool | None = None,
	letterhead: str | None = None,
	trigger_print: bool = False,
	style: str | None = None,
	settings: str | None = None,
):
	"""Returns `html` and `style` of print format, used in PDF etc"""

	if isinstance(name, str):
		document = jingrow.get_pg(pg, name)
	else:
		document = jingrow.get_pg(json.loads(pg))

	document.check_permission()

	print_format = get_print_format_pg(print_format, meta=document.meta)
	set_link_titles(document)

	try:
		html = get_rendered_template(
			pg=document,
			print_format=print_format,
			meta=document.meta,
			no_letterhead=no_letterhead,
			letterhead=letterhead,
			trigger_print=trigger_print,
			settings=jingrow.parse_json(settings),
		)
	except jingrow.TemplateNotFoundError:
		jingrow.clear_last_message()
		html = None

	return {"html": html, "style": get_print_style(style=style, print_format=print_format)}


@jingrow.whitelist()
def get_rendered_raw_commands(pg: str, name: str | None = None, print_format: str | None = None):
	"""Returns Rendered Raw Commands of print format, used to send directly to printer"""

	if isinstance(name, str):
		document = jingrow.get_pg(pg, name)
	else:
		document = jingrow.get_pg(json.loads(pg))

	document.check_permission()

	print_format = get_print_format_pg(print_format, meta=document.meta)

	if not print_format or (print_format and not print_format.raw_printing):
		jingrow.throw(
			_("{0} is not a raw printing format.").format(print_format), jingrow.TemplateNotFoundError
		)

	return {
		"raw_commands": get_rendered_template(pg=document, print_format=print_format, meta=document.meta)
	}


def validate_print_permission(pg):
	for ptype in ("read", "print"):
		if jingrow.has_permission(pg.pagetype, ptype, pg):
			return

	if jingrow.has_website_permission(pg):
		return

	if (key := jingrow.form_dict.key) and isinstance(key, str) and validate_key(key, pg) is not False:
		return

	pg._handle_permission_failure("print")


def validate_key(key, pg):
	document_key_expiry = jingrow.get_cached_value(
		"Page Share Key",
		{"reference_pagetype": pg.pagetype, "reference_pagename": pg.name, "key": key},
		["expires_on"],
	)
	if document_key_expiry is not None:
		if is_expired(document_key_expiry[0]):
			raise jingrow.exceptions.LinkExpired
		else:
			return

	# TODO: Deprecate this! kept it for backward compatibility
	if jingrow.get_system_settings("allow_older_web_view_links") and key == pg.get_signature():
		return

	return False


def get_letter_head(pg: "Page", no_letterhead: bool, letterhead: str | None = None):
	if no_letterhead:
		return {}

	letterhead_name = letterhead or pg.get("letter_head")
	if letterhead_name:
		return jingrow.db.get_value(
			"Letter Head",
			letterhead_name,
			["content", "footer", "header_script", "footer_script"],
			as_dict=True,
		)
	else:
		return (
			jingrow.db.get_value(
				"Letter Head",
				{"is_default": 1},
				["content", "footer", "header_script", "footer_script"],
				as_dict=True,
			)
			or {}
		)


def get_print_format(pagetype, print_format):
	if print_format.disabled:
		jingrow.throw(_("Print Format {0} is disabled").format(print_format.name), jingrow.DoesNotExistError)

	# server, find template
	module = print_format.module or jingrow.db.get_value("PageType", pagetype, "module")

	is_custom_module = jingrow.get_cached_value("Module Def", module, "custom")

	if not is_custom_module:
		path = os.path.join(
			get_module_path(module, "Print Format", print_format.name),
			jingrow.scrub(print_format.name) + ".html",
		)
		if os.path.exists(path):
			with open(path) as pffile:
				return pffile.read()

	if print_format.raw_printing:
		return print_format.raw_commands
	if print_format.html:
		return print_format.html

	jingrow.throw(_("No template found at path: {0}").format(path), jingrow.TemplateNotFoundError)


def make_layout(pg, meta, format_data=None):
	"""Builds a hierarchical layout object from the fields list to be rendered
	by `standard.html`

	:param pg: Page to be rendered.
	:param meta: Page meta object (pagetype).
	:param format_data: Fields sequence and properties defined by Print Format Builder."""
	layout, page = [], []
	layout.append(page)

	def get_new_section():
		return {"columns": [], "has_data": False}

	def append_empty_field_dict_to_page_column(page):
		"""append empty columns dict to page layout"""
		if not page[-1]["columns"]:
			page[-1]["columns"].append({"fields": []})

	for df in format_data or meta.fields:
		if format_data:
			# embellish df with original properties
			df = jingrow._dict(df)
			if df.fieldname:
				original = meta.get_field(df.fieldname)
				if original:
					newdf = original.as_dict()
					newdf.hide_in_print_layout = original.get("hide_in_print_layout")
					newdf.update(df)
					df = newdf

			df.print_hide = 0

		if df.fieldtype == "Section Break" or page == []:
			if len(page) > 1:
				if not page[-1]["has_data"]:
					# truncate last section if empty
					del page[-1]

			section = get_new_section()
			if df.fieldtype == "Section Break" and df.label:
				section["label"] = df.label

			page.append(section)

		elif df.fieldtype == "Column Break":
			# if last column break and last column is not empty
			page[-1]["columns"].append({"fields": []})

		else:
			# add a column if not yet added
			append_empty_field_dict_to_page_column(page)

		if df.fieldtype == "HTML" and df.options:
			pg.set(df.fieldname, True)  # show this field

		if df.fieldtype == "Signature" and not pg.get(df.fieldname):
			placeholder_image = "/assets/jingrow/images/signature-placeholder.png"
			pg.set(df.fieldname, placeholder_image)

		if is_visible(df, pg) and has_value(df, pg):
			append_empty_field_dict_to_page_column(page)

			page[-1]["columns"][-1]["fields"].append(df)

			# section has fields
			page[-1]["has_data"] = True

			# if table, add the row info in the field
			# if a page break is found, create a new pagefield
			if df.fieldtype == "Table":
				df.rows = []
				df.start = 0
				df.end = None
				for i, row in enumerate(pg.get(df.fieldname)):
					if row.get("page_break"):
						# close the earlier row
						df.end = i

						# new page, with empty section and column
						page = [get_new_section()]
						layout.append(page)
						append_empty_field_dict_to_page_column(page)

						# continue the table in a new page
						df = copy.copy(df)
						df.start = i
						df.end = None
						page[-1]["columns"][-1]["fields"].append(df)

	return layout


def is_visible(df, pg):
	"""Returns True if pagefield is visible in print layout and does not have print_hide set."""
	if df.fieldtype in ("Section Break", "Column Break", "Button"):
		return False

	if (df.permlevel or 0) > 0 and not pg.has_permlevel_access_to(df.fieldname, df):
		return False

	return not pg.is_print_hide(df.fieldname, df)


def has_value(df, pg):
	value = pg.get(df.fieldname)
	if value in (None, ""):
		return False

	elif isinstance(value, str) and not strip_html(value).strip():
		if df.fieldtype in ["Text", "Text Editor"]:
			return True

		return False

	elif isinstance(value, list) and not len(value):
		return False

	return True


def get_print_style(
	style: str | None = None, print_format: Optional["PrintFormat"] = None, for_legacy: bool = False
):
	print_settings = jingrow.get_pg("Print Settings")

	if not style:
		style = print_settings.print_style or ""

	context = {
		"print_settings": print_settings,
		"print_style": style,
		"font": get_font(print_settings, print_format, for_legacy),
	}

	css = jingrow.get_template("templates/styles/standard.css").render(context)

	if style and jingrow.db.exists("Print Style", style):
		css = css + "\n" + jingrow.db.get_value("Print Style", style, "css")

	# move @import to top
	for at_import in list(set(re.findall(r"(@import url\([^\)]+\)[;]?)", css))):
		css = css.replace(at_import, "")

		# prepend css with at_import
		css = at_import + css

	if print_format and print_format.css:
		css += "\n\n" + print_format.css

	return css


def get_font(print_settings, print_format=None, for_legacy=False):
	default = 'Inter, "Helvetica Neue", Helvetica, Arial, "Open Sans", sans-serif'
	if for_legacy:
		return default

	font = None
	if print_format:
		if print_format.font and print_format.font != "Default":
			font = f"{print_format.font}, sans-serif"

	if not font:
		if print_settings.font and print_settings.font != "Default":
			font = f"{print_settings.font}, sans-serif"

		else:
			font = default

	return font


def get_visible_columns(data, table_meta, df):
	"""Returns list of visible columns based on print_hide and if all columns have value."""
	columns = []
	pg = data[0] or jingrow.new_pg(df.options)

	hide_in_print_layout = df.get("hide_in_print_layout") or []

	def add_column(col_df):
		if col_df.fieldname in hide_in_print_layout:
			return False
		return is_visible(col_df, pg) and column_has_value(data, col_df.get("fieldname"), col_df)

	if df.get("visible_columns"):
		# columns specified by column builder
		for col_df in df.get("visible_columns"):
			# load default pagefield properties
			pagefield = table_meta.get_field(col_df.get("fieldname"))
			if not pagefield:
				continue
			newdf = pagefield.as_dict().copy()
			newdf.update(col_df)
			if add_column(newdf):
				columns.append(newdf)
	else:
		for col_df in table_meta.fields:
			if add_column(col_df):
				columns.append(col_df)

	return columns


def column_has_value(data, fieldname, col_df):
	"""Check if at least one cell in column has non-zero and non-blank value"""
	has_value = False

	if col_df.fieldtype in ["Float", "Currency"] and not col_df.print_hide_if_no_value:
		return True

	for row in data:
		value = row.get(fieldname)
		if value:
			if isinstance(value, str):
				if strip_html(value).strip():
					has_value = True
					break
			else:
				has_value = True
				break

	return has_value


trigger_print_script = """
<script>
//allow wrapping of long tr
var elements = document.getElementsByTagName("tr");
var i = elements.length;
while (i--) {
	if(elements[i].clientHeight>300){
		elements[i].setAttribute("style", "page-break-inside: auto;");
	}
}

window.print();

// close the window after print
// NOTE: doesn't close if print is cancelled in Chrome
// Changed timeout to 5s from 1s because it blocked mobile view rendering
setTimeout(function() {
	window.close();
}, 5000);
</script>
"""
