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

import json
import os

import jingrow
from jingrow import _, scrub
from jingrow.core.api.file import get_max_file_size
from jingrow.core.pagetype.file.utils import remove_file_by_url
from jingrow.desk.form.meta import get_code_files_via_hooks
from jingrow.modules.utils import export_module_json, get_pg_module
from jingrow.permissions import check_pagetype_permission
from jingrow.rate_limiter import rate_limit
from jingrow.utils import dict_with_keys, strip_html
from jingrow.utils.caching import redis_cache
from jingrow.website.utils import get_boot_data, get_comment_list, get_sidebar_items
from jingrow.website.website_generator import WebsiteGenerator


class WebForm(WebsiteGenerator):
	# 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.types import DF
		from jingrow.website.pagetype.web_form_field.web_form_field import WebFormField
		from jingrow.website.pagetype.web_form_list_column.web_form_list_column import WebFormListColumn

		allow_comments: DF.Check
		allow_delete: DF.Check
		allow_edit: DF.Check
		allow_incomplete: DF.Check
		allow_multiple: DF.Check
		allow_print: DF.Check
		allowed_embedding_domains: DF.SmallText | None
		anonymous: DF.Check
		apply_document_permissions: DF.Check
		banner_image: DF.AttachImage | None
		breadcrumbs: DF.Code | None
		button_label: DF.Data | None
		client_script: DF.Code | None
		condition: DF.Code | None
		custom_css: DF.Code | None
		pg_type: DF.Link
		introduction_text: DF.TextEditor | None
		is_standard: DF.Check
		list_columns: DF.Table[WebFormListColumn]
		list_title: DF.Data | None
		login_required: DF.Check
		max_attachment_size: DF.Int
		meta_description: DF.SmallText | None
		meta_image: DF.AttachImage | None
		meta_title: DF.Data | None
		module: DF.Link | None
		print_format: DF.Link | None
		published: DF.Check
		route: DF.Data | None
		show_attachments: DF.Check
		show_list: DF.Check
		show_sidebar: DF.Check
		success_message: DF.Text | None
		success_title: DF.Data | None
		success_url: DF.Data | None
		title: DF.Data
		web_form_fields: DF.Table[WebFormField]
		website_sidebar: DF.Link | None
	# end: auto-generated types
	website = jingrow._dict(no_cache=1)

	def validate(self):
		super().validate()

		if not self.module:
			self.module = jingrow.db.get_value("PageType", self.pg_type, "module")

		in_user_env = not (
			jingrow.flags.in_install
			or jingrow.flags.in_patch
			or jingrow.flags.in_test
			or jingrow.flags.in_fixtures
		)
		if in_user_env and self.is_standard and not jingrow.conf.developer_mode:
			# only published can be changed for standard web forms
			if self.has_value_changed("published"):
				published_value = self.published
				self.reload()
				self.published = published_value
			else:
				jingrow.throw(_("You need to be in developer mode to edit a Standard Web Form"))

		if not jingrow.flags.in_import:
			self.validate_fields()

	def validate_fields(self):
		"""Validate all fields are present"""
		from jingrow.model import no_value_fields

		meta = jingrow.get_meta(self.pg_type)
		missing = [
			df.fieldname
			for df in self.web_form_fields
			if df.fieldname and (df.fieldtype not in no_value_fields and not meta.has_field(df.fieldname))
		]

		if missing:
			jingrow.throw(_("Following fields are missing:") + "<br>" + "<br>".join(missing))

	def reset_field_parent(self):
		"""Convert link fields to select with names as options"""
		for df in self.web_form_fields:
			df.parent = self.pg_type

	# export
	def on_update(self):
		"""
		Writes the .txt for this page and if write_content is checked,
		it will write out a .html file
		"""
		path = export_module_json(self, self.is_standard, self.module)

		if path:
			# js
			if not os.path.exists(path + ".js"):
				with open(path + ".js", "w") as f:
					f.write(
						"""jingrow.ready(function() {
	// bind events here
})"""
					)

			# py
			if not os.path.exists(path + ".py"):
				with open(path + ".py", "w") as f:
					f.write(
						"""import jingrow

def get_context(context):
	# do your magic here
	pass
"""
					)

	def get_context(self, context):
		"""Build context to render the `web_form.html` template"""
		context.in_edit_mode = False
		context.in_view_mode = False

		if jingrow.form_dict.is_list:
			context.template = "website/pagetype/web_form/templates/web_list.html"
		else:
			context.template = "website/pagetype/web_form/templates/web_form.html"

		# check permissions
		if jingrow.form_dict.name:
			assert isinstance(jingrow.form_dict.name, str | int)

			if jingrow.session.user == "Guest":
				jingrow.throw(
					_("You need to be logged in to access this {0}.").format(self.pg_type),
					jingrow.PermissionError,
				)

			if not jingrow.db.exists(self.pg_type, jingrow.form_dict.name):
				check_pagetype_permission(self.pg_type)
				raise jingrow.PageDoesNotExistError()

			if not self.has_web_form_permission(self.pg_type, jingrow.form_dict.name):
				check_pagetype_permission(self.pg_type)
				jingrow.throw(
					_("You don't have the permissions to access this document"), jingrow.PermissionError
				)

		if jingrow.local.path == self.route:
			path = f"/{self.route}/list" if self.show_list else f"/{self.route}/new"
			jingrow.redirect(path)

		if jingrow.form_dict.is_list and not self.show_list:
			jingrow.redirect(f"/{self.route}/new")

		if jingrow.form_dict.is_edit and not self.allow_edit:
			context.in_view_mode = True
			jingrow.redirect(f"/{self.route}/{jingrow.form_dict.name}")

		if jingrow.form_dict.is_edit:
			context.in_edit_mode = True

		if jingrow.form_dict.is_read:
			context.in_view_mode = True

		if (
			not jingrow.form_dict.is_edit
			and not jingrow.form_dict.is_read
			and self.allow_edit
			and jingrow.form_dict.name
		):
			context.in_edit_mode = True
			jingrow.redirect(f"/{jingrow.local.path}/edit")

		if (
			jingrow.session.user != "Guest"
			and self.login_required
			and not self.allow_multiple
			and not jingrow.form_dict.name
			and not jingrow.form_dict.is_list
		):
			condition_json = json.loads(self.condition_json) if self.condition_json else []
			condition_json.append(["owner", "=", jingrow.session.user])
			names = jingrow.get_all(self.pg_type, filters=condition_json, pluck="name")
			if names:
				context.in_view_mode = True
				jingrow.redirect(f"/{self.route}/{names[0]}")

		# Show new form when
		# - User is Guest
		# - Login not required
		route_to_new = jingrow.session.user == "Guest" or not self.login_required
		if not jingrow.form_dict.is_new and route_to_new:
			jingrow.redirect(f"/{self.route}/new")

		self.reset_field_parent()

		# add keys from form_dict to context
		context.update(dict_with_keys(jingrow.form_dict, ["is_list", "is_new", "is_edit", "is_read"]))

		for df in self.web_form_fields:
			if df.fieldtype == "Column Break":
				context.has_column_break = True
				break

		# load web form pg
		context.web_form_pg = self.as_dict(no_nulls=True)
		context.web_form_pg.update(
			dict_with_keys(context, ["is_list", "is_new", "in_edit_mode", "in_view_mode"])
		)

		if self.show_sidebar and self.website_sidebar:
			context.sidebar_items = get_sidebar_items(self.website_sidebar)

		if jingrow.form_dict.is_list:
			self.load_list_data(context)
		else:
			self.load_form_data(context)

		self.add_custom_context_and_script(context)
		self.load_translations(context)
		self.add_metatags(context)

		context.boot = get_boot_data()
		context.boot["link_title_pagetypes"] = jingrow.boot.get_link_title_pagetypes()

		context.webform_banner_image = self.banner_image
		context.pop("banner_image", None)

	def add_metatags(self, context):
		description = self.meta_description

		if not description and self.introduction_text:
			description = self.introduction_text[:140]

		context.metatags = {
			"title": self.meta_title or self.title,
			"description": description,
			"image": self.meta_image,
		}

	def load_translations(self, context):
		messages = [
			"Sr",
			"Attach",
			"Next",
			"Previous",
			"Discard?",
			"Cancel",
			"Discard:Button in web form",
			"Edit:Button in web form",
			"See previous responses:Button in web form",
			"Edit your response:Button in web form",
			"Are you sure you want to discard the changes?",
			"Mandatory fields required::Error message in web form",
			"Invalid values for fields::Error message in web form",
			"Error:Title of error message in web form",
			"Page {0} of {1}",
			"Couldn't save, please check the data you have entered",
			"Validation Error",
			self.title,
			self.introduction_text,
			self.success_title,
			self.success_message,
			self.list_title,
			self.button_label,
			self.meta_title,
			self.meta_description,
		]

		for field in self.web_form_fields:
			messages.extend([field.label, field.description])
			if field.fieldtype == "Select" and field.options:
				messages.extend(field.options.split("\n"))

		# When at least one field in self.web_form_fields has fieldtype "Table" then add "No data" to messages
		if any(field.fieldtype == "Table" for field in self.web_form_fields):
			messages.append("Move")
			messages.append("Insert Above")
			messages.append("Insert Below")
			messages.append("Duplicate")
			messages.append("Shortcuts")
			messages.append("Ctrl + Up")
			messages.append("Ctrl + Down")
			messages.append("ESC")
			messages.append("Editing Row")
			messages.append("Add / Remove Columns")
			messages.append("Fieldname")
			messages.append("Column Width")
			messages.append("Configure Columns")
			messages.append("Select Fields")
			messages.append("Select All")
			messages.append("Update")
			messages.append("Reset to default")
			messages.append("No Data")
			messages.append("Delete")
			messages.append("Delete All")
			messages.append("Add Row")
			messages.append("Add Multiple")
			messages.append("Download")
			messages.append("of")
			messages.append("Upload")
			messages.append("Last")
			messages.append("First")
			messages.append("No.:Title of the 'row number' column")

		# Phone Picker
		if any(field.fieldtype == "Phone" for field in self.web_form_fields):
			messages.append("Search for countries...")

		# Dates
		if any(field.fieldtype == "Date" for field in self.web_form_fields):
			messages.append("Now")
			messages.append("Today")
			messages.append("Date {0} must be in format: {1}")
			messages.append("{0} to {1}")

		# Time
		if any(field.fieldtype == "Time" for field in self.web_form_fields):
			messages.append("Now")

		messages.extend(col.get("label") if col else "" for col in self.list_columns)

		context.translated_messages = jingrow.as_json({message: _(message) for message in messages if message})

	def load_list_data(self, context):
		if not self.list_columns:
			self.list_columns = get_in_list_view_fields(self.pg_type)
			context.web_form_pg.list_columns = self.list_columns

	def load_form_data(self, context):
		"""Load document `pg` and `layout` properties for template"""
		context.parents = []
		if self.show_list:
			context.parents.append(
				{
					"label": _(self.title),
					"route": f"{self.route}/list",
				}
			)

		context.parents = self.get_parents(context)

		if self.breadcrumbs:
			context.parents = jingrow.safe_eval(self.breadcrumbs, {"_": _})

		if self.show_list and jingrow.form_dict.is_new:
			context.title = _("New {0}").format(context.title)

		context.has_header = (jingrow.form_dict.name or jingrow.form_dict.is_new) and (
			jingrow.session.user != "Guest" or not self.login_required
		)

		if context.success_message:
			context.success_message = jingrow.db.escape(context.success_message.replace("\n", "<br>")).strip(
				"'"
			)

		if not context.max_attachment_size:
			context.max_attachment_size = get_max_file_size() / 1024 / 1024

		# For Table fields, server-side processing for meta
		for field in context.web_form_pg.web_form_fields:
			if field.fieldtype == "Table":
				field.fields = get_in_list_view_fields(field.options)

			if field.fieldtype == "Link":
				field.fieldtype = "Autocomplete"
				field.options = get_link_options(
					self.name, field.options, field.allow_read_on_all_link_options
				)

		context.reference_pg = {}

		# load reference pg
		if jingrow.form_dict.name:
			context.pg_name = jingrow.form_dict.name
			context.reference_pg = jingrow.get_pg(self.pg_type, context.pg_name)
			context.web_form_title = context.title
			context.title = (
				strip_html(context.reference_pg.get(context.reference_pg.meta.get_title_field()))
				or context.pg_name
			)
			context.reference_pg.add_seen()
			context.reference_pagetype = context.reference_pg.pagetype
			context.reference_name = context.reference_pg.name

			if self.show_attachments:
				context.attachments = jingrow.get_all(
					"File",
					filters={
						"attached_to_name": context.reference_name,
						"attached_to_pagetype": context.reference_pagetype,
						"is_private": 0,
					},
					fields=["file_name", "file_url", "file_size"],
				)

			if self.allow_comments:
				context.comment_list = get_comment_list(
					context.reference_pg.pagetype, context.reference_pg.name
				)

			context.reference_pg = context.reference_pg.as_dict(no_nulls=True)

	def add_custom_context_and_script(self, context):
		"""Update context from module if standard and append script"""
		if self.is_standard:
			web_form_module = get_web_form_module(self)
			new_context = web_form_module.get_context(context)

			if new_context:
				context.update(new_context)

			js_path = os.path.join(os.path.dirname(web_form_module.__file__), scrub(self.name) + ".js")
			if os.path.exists(js_path):
				script = jingrow.render_template(open(js_path).read(), context)

				for path in get_code_files_via_hooks("webform_include_js", context.pg_type):
					custom_js = jingrow.render_template(open(path).read(), context)
					script = "\n\n".join([script, custom_js])

				context.script = script

			css_path = os.path.join(os.path.dirname(web_form_module.__file__), scrub(self.name) + ".css")
			if os.path.exists(css_path):
				style = open(css_path).read()

				for path in get_code_files_via_hooks("webform_include_css", context.pg_type):
					custom_css = open(path).read()
					style = "\n\n".join([style, custom_css])

				context.style = style

	def get_parents(self, context):
		parents = None

		if context.is_list and not context.parents:
			parents = [{"title": _("My Account"), "name": "me"}]
		elif context.parents:
			parents = context.parents

		return parents

	def validate_mandatory(self, pg):
		"""Validate mandatory web form fields"""
		missing = [f for f in self.web_form_fields if f.reqd and pg.get(f.fieldname) in (None, [], "")]
		if missing:
			jingrow.throw(
				_("Mandatory Information missing:")
				+ "<br><br>"
				+ "<br>".join(f"{d.label} ({d.fieldtype})" for d in missing)
			)

	def allow_website_search_indexing(self):
		return False

	def has_web_form_permission(self, pagetype, name, ptype="read"):
		if jingrow.session.user == "Guest":
			return False

		if self.apply_document_permissions:
			return jingrow.get_pg(pagetype, name).has_permission(permtype=ptype)

		# owner matches
		elif jingrow.db.get_value(pagetype, name, "owner") == jingrow.session.user:
			return True

		elif jingrow.has_website_permission(name, ptype=ptype, pagetype=pagetype):
			return True

		elif check_webform_perm(pagetype, name):
			return True

		else:
			return False


def get_web_form_module(pg):
	if pg.is_standard:
		return get_pg_module(pg.module, pg.pagetype, pg.name)


@jingrow.whitelist(allow_guest=True)
@rate_limit(key="web_form", limit=10, seconds=60)
def accept(web_form, data):
	"""Save the web form"""
	data = jingrow._dict(json.loads(data))

	files = []
	files_to_delete = []

	web_form = jingrow.get_pg("Web Form", web_form)
	pagetype = web_form.pg_type
	user = jingrow.session.user

	if web_form.anonymous and jingrow.session.user != "Guest":
		jingrow.session.user = "Guest"

	if data.name and not web_form.allow_edit:
		jingrow.throw(_("You are not allowed to update this Web Form Page"))

	jingrow.flags.in_web_form = True
	meta = jingrow.get_meta(pagetype)

	if data.name:
		# update
		pg = jingrow.get_pg(pagetype, data.name)
	else:
		# insert
		pg = jingrow.new_pg(pagetype)

	# set values
	for field in web_form.web_form_fields:
		fieldname = field.fieldname
		df = meta.get_field(fieldname)
		value = data.get(fieldname, "")

		if df and df.fieldtype in ("Attach", "Attach Image"):
			if value and "data:" and "base64" in value:
				files.append((fieldname, value))
				if not pg.name:
					pg.set(fieldname, "")
				continue

			elif not value and pg.get(fieldname):
				files_to_delete.append(pg.get(fieldname))

		pg.set(fieldname, value)

	if pg.name:
		if web_form.has_web_form_permission(pagetype, pg.name, "write"):
			pg.save(ignore_permissions=True)
		else:
			# only if permissions are present
			pg.save()

	else:
		# insert
		if web_form.login_required and jingrow.session.user == "Guest":
			jingrow.throw(_("You must login to submit this form"))

		ignore_mandatory = True if files else False

		pg.insert(ignore_permissions=True, ignore_mandatory=ignore_mandatory)

	# add files
	if files:
		for f in files:
			fieldname, filedata = f

			# remove earlier attached file (if exists)
			if pg.get(fieldname):
				remove_file_by_url(pg.get(fieldname), pagetype=pagetype, name=pg.name)

			# save new file
			filename, dataurl = filedata.split(",", 1)
			_file = jingrow.get_pg(
				{
					"pagetype": "File",
					"file_name": filename,
					"attached_to_pagetype": pagetype,
					"attached_to_name": pg.name,
					"content": dataurl,
					"decode": True,
				}
			)
			_file.save()

			# update values
			pg.set(fieldname, _file.file_url)

		pg.save(ignore_permissions=True)

	if files_to_delete:
		for f in files_to_delete:
			if f:
				remove_file_by_url(f, pagetype=pagetype, name=pg.name)

	if web_form.anonymous and jingrow.session.user == "Guest" and user:
		jingrow.session.user = user

	jingrow.flags.web_form_pg = pg
	return pg


@jingrow.whitelist()
def delete(web_form_name: str, pagename: str | int):
	web_form = jingrow.get_pg("Web Form", web_form_name)

	owner = jingrow.db.get_value(web_form.pg_type, pagename, "owner")
	if jingrow.session.user == owner and web_form.allow_delete:
		jingrow.delete_pg(web_form.pg_type, pagename, ignore_permissions=True)
	else:
		raise jingrow.PermissionError("Not Allowed")


@jingrow.whitelist()
def delete_multiple(web_form_name: str, pagenames):
	web_form = jingrow.get_pg("Web Form", web_form_name)

	pagenames = json.loads(pagenames)

	allowed_pagenames = []
	restricted_pagenames = []

	for pagename in pagenames:
		assert isinstance(pagename, str | int)

		owner = jingrow.db.get_value(web_form.pg_type, pagename, "owner")
		if jingrow.session.user == owner and web_form.allow_delete:
			allowed_pagenames.append(pagename)
		else:
			restricted_pagenames.append(pagename)

	for pagename in allowed_pagenames:
		jingrow.delete_pg(web_form.pg_type, pagename, ignore_permissions=True)

	if restricted_pagenames:
		raise jingrow.PermissionError(
			"You do not have permisssion to delete " + ", ".join(restricted_pagenames)
		)


def check_webform_perm(pagetype, name):
	pg = jingrow.get_pg(pagetype, name)
	if hasattr(pg, "has_webform_permission"):
		if pg.has_webform_permission():
			return True


@jingrow.whitelist(allow_guest=True)
def get_web_form_filters(web_form_name):
	web_form = jingrow.get_pg("Web Form", web_form_name)
	return [field for field in web_form.web_form_fields if field.show_in_filter]


@jingrow.whitelist(allow_guest=True)
def get_form_data(pagetype, pagename=None, web_form_name=None):
	web_form = jingrow.get_pg("Web Form", web_form_name)

	if web_form.login_required and jingrow.session.user == "Guest":
		jingrow.throw(_("Not Permitted"), jingrow.PermissionError)

	out = jingrow._dict()
	out.web_form = web_form

	if jingrow.session.user != "Guest" and not pagename and not web_form.allow_multiple:
		pagename = jingrow.db.get_value(pagetype, {"owner": jingrow.session.user}, "name")

	if pagename:
		pg = jingrow.get_pg(pagetype, pagename)
		if web_form.has_web_form_permission(pagetype, pagename, ptype="read"):
			out.pg = pg
		else:
			jingrow.throw(_("Not permitted"), jingrow.PermissionError)

	# For Table fields, server-side processing for meta
	for field in out.web_form.web_form_fields:
		if field.fieldtype == "Table":
			field.fields = get_in_list_view_fields(field.options)
			out.update({field.fieldname: field.fields})

		if field.fieldtype == "Link":
			field.fieldtype = "Autocomplete"
			field.options = get_link_options(
				web_form_name, field.options, field.allow_read_on_all_link_options
			)

	return out


@jingrow.whitelist()
def get_in_list_view_fields(pagetype):
	meta = jingrow.get_meta(pagetype)
	fields = []

	if meta.title_field:
		fields.append(meta.title_field)
	else:
		fields.append("name")

	if meta.has_field("status"):
		fields.append("status")

	fields += [df.fieldname for df in meta.fields if df.in_list_view and df.fieldname not in fields]

	def get_field_df(fieldname):
		if fieldname == "name":
			return {"label": "Name", "fieldname": "name", "fieldtype": "Data"}
		return meta.get_field(fieldname).as_dict()

	return [get_field_df(f) for f in fields]


def get_link_options(web_form_name, pagetype, allow_read_on_all_link_options=False):
	web_form: WebForm = jingrow.get_pg("Web Form", web_form_name)

	if web_form.login_required and jingrow.session.user == "Guest":
		jingrow.throw(_("You must be logged in to use this form."), jingrow.PermissionError)

	if not web_form.published or not any(f for f in web_form.web_form_fields if f.options == pagetype):
		jingrow.throw(
			_("You don't have permission to access the {0} PageType.").format(pagetype), jingrow.PermissionError
		)

	link_options, filters = [], {}
	if web_form.login_required and not allow_read_on_all_link_options:
		filters = {"owner": jingrow.session.user}

	fields = ["name as value"]

	meta = jingrow.get_meta(pagetype)
	show_title_field = meta.title_field and meta.show_title_field_in_link

	if show_title_field:
		fields.append(f"{meta.title_field} as label")

	link_options = jingrow.get_all(pagetype, filters, fields)

	if show_title_field:
		if meta.translated_pagetype:
			# Translate the labels if "Translate Link Fields" is enabled
			link_options = [{"value": row.value, "label": _(row.label)} for row in link_options]

		return json.dumps(link_options, default=str)
	else:
		if meta.translated_pagetype:
			# Add `label` as the translated name if "Translate Link Fields" is enabled
			return [{"value": row.value, "label": _(row.value)} for row in link_options]

		# Use the actual names as options without labels
		return "\n".join([str(pg.value) for pg in link_options])


@redis_cache(ttl=60 * 60)
def get_published_web_forms() -> dict[str, str]:
	return jingrow.get_all("Web Form", ["name", "route", "modified"], {"published": 1})
