# Copyright (c) 2015, JINGROW and Contributors
# License: MIT. See LICENSE
import json
import os
from typing import TYPE_CHECKING

import jingrow
import jingrow.model
import jingrow.utils
from jingrow import _
from jingrow.desk.reportview import validate_args
from jingrow.model.db_query import check_parent_permission
from jingrow.model.utils import is_virtual_pagetype
from jingrow.utils import get_safe_filters
from jingrow.utils.deprecations import deprecated

if TYPE_CHECKING:
	from jingrow.model.page import Page

"""
Handle RESTful requests that are mapped to the `/api/data` route.

Requests via JingrowClient are also handled here.
"""


@jingrow.whitelist()
def get_list(
	pagetype,
	fields=None,
	filters=None,
	group_by=None,
	order_by=None,
	limit_start=None,
	limit_page_length=20,
	parent=None,
	debug: bool = False,
	as_dict: bool = True,
	or_filters=None,
):
	"""Returns a list of records by filters, fields, ordering and limit

	:param pagetype: PageType of the data to be queried
	:param fields: fields to be returned. Default is `name`
	:param filters: filter list by this dict
	:param order_by: Order by this fieldname
	:param limit_start: Start at this index
	:param limit_page_length: Number of records to be returned (default 20)"""
	if jingrow.is_table(pagetype):
		check_parent_permission(parent, pagetype)

	args = jingrow._dict(
		pagetype=pagetype,
		parent_pagetype=parent,
		fields=fields,
		filters=filters,
		or_filters=or_filters,
		group_by=group_by,
		order_by=order_by,
		limit_start=limit_start,
		limit_page_length=limit_page_length,
		debug=debug,
		as_list=not as_dict,
	)

	validate_args(args)
	return jingrow.get_list(**args)


@jingrow.whitelist()
def get_count(pagetype, filters=None, debug=False, cache=False):
	return jingrow.db.count(pagetype, get_safe_filters(filters), debug, cache)


@jingrow.whitelist()
def get(pagetype, name=None, filters=None, parent=None):
	"""Returns a document by name or filters

	:param pagetype: PageType of the document to be returned
	:param name: return document of this `name`
	:param filters: If name is not set, filter by these values and return the first match"""
	if jingrow.is_table(pagetype):
		check_parent_permission(parent, pagetype)

	if name:
		pg = jingrow.get_pg(pagetype, name)
	elif filters or filters == {}:
		pg = jingrow.get_pg(pagetype, jingrow.parse_json(filters))
	else:
		pg = jingrow.get_pg(pagetype)  # single

	pg.check_permission()
	pg.apply_fieldlevel_read_permissions()

	return pg.as_dict()


@jingrow.whitelist()
def get_quick_entry_fields(pagetype):
	"""Returns fields for quick entry form of given pagetype"""
	meta = jingrow.get_meta(pagetype)
	fields = []
	for df in meta.fields:
		if (
			(df.reqd or df.allow_in_quick_entry)
			and not df.read_only
			and not df.is_virtual
			and df.fieldtype not in ("Tab Break", "Section Break", "Column Break")
		):
			fields.append({
				"fieldname": df.fieldname,
				"label": df.label,
				"fieldtype": df.fieldtype,
				"reqd": df.reqd or 0,
				"options": df.options or "",
				"default": df.default or "",
				"description": df.description or "",
			})
	return fields


@jingrow.whitelist()
def get_value(pagetype, fieldname, filters=None, as_dict=True, debug=False, parent=None):
	"""Returns a value form a document

	:param pagetype: PageType to be queried
	:param fieldname: Field to be returned (default `name`)
	:param filters: dict or string for identifying the record"""
	if jingrow.is_table(pagetype):
		check_parent_permission(parent, pagetype)

	if not jingrow.has_permission(pagetype, parent_pagetype=parent):
		jingrow.throw(_("No permission for {0}").format(_(pagetype)), jingrow.PermissionError)

	filters = get_safe_filters(filters)
	if isinstance(filters, str):
		filters = {"name": filters}

	try:
		fields = jingrow.parse_json(fieldname)
	except (TypeError, ValueError):
		# name passed, not json
		fields = [fieldname]

	# check whether the used filters were really parseable and usable
	# and did not just result in an empty string or dict
	if not filters:
		filters = None

	if jingrow.get_meta(pagetype).issingle:
		value = jingrow.db.get_values_from_single(fields, filters, pagetype, as_dict=as_dict, debug=debug)
	else:
		value = get_list(
			pagetype,
			filters=filters,
			fields=fields,
			debug=debug,
			limit_page_length=1,
			parent=parent,
			as_dict=as_dict,
		)

	if as_dict:
		return value[0] if value else {}

	if not value:
		return

	return value[0] if len(fields) > 1 else value[0][0]

@jingrow.whitelist()
def get_single(pagetype):
	"""Return a `jingrow.model.page.Page` object of the given Single pagetype."""
	return jingrow.get_pg(pagetype, pagetype)

@jingrow.whitelist()
def get_single_value(pagetype, field):
	if not jingrow.has_permission(pagetype):
		jingrow.throw(_("No permission for {0}").format(_(pagetype)), jingrow.PermissionError)

	return jingrow.db.get_single_value(pagetype, field)


@jingrow.whitelist(methods=["POST", "PUT"])
def set_value(pagetype, name, fieldname, value=None):
	"""Set a value using get_pg, group of values

	:param pagetype: PageType of the document
	:param name: name of the document
	:param fieldname: fieldname string or JSON / dict with key value pair
	:param value: value if fieldname is JSON / dict"""

	if fieldname in (jingrow.model.default_fields + jingrow.model.child_table_fields):
		jingrow.throw(_("Cannot edit standard fields"))

	if not value:
		values = fieldname
		if isinstance(fieldname, str):
			try:
				values = json.loads(fieldname)
			except ValueError:
				values = {fieldname: ""}
	else:
		values = {fieldname: value}

	# check for child table pagetype
	if not jingrow.get_meta(pagetype).istable:
		pg = jingrow.get_pg(pagetype, name)
		pg.update(values)
	else:
		pg = jingrow.db.get_value(pagetype, name, ["parenttype", "parent"], as_dict=True)
		pg = jingrow.get_pg(pg.parenttype, pg.parent)
		child = pg.getone({"pagetype": pagetype, "name": name})
		child.update(values)

	pg.save()

	return pg.as_dict()


@jingrow.whitelist(methods=["POST", "PUT"])
def insert(pg=None):
	"""Insert a document

	:param pg: JSON or dict object to be inserted"""
	if isinstance(pg, str):
		pg = json.loads(pg)

	return insert_pg(pg).as_dict()


@jingrow.whitelist(methods=["POST", "PUT"])
def insert_many(pgs=None):
	"""Insert multiple documents

	:param pgs: JSON or list of dict objects to be inserted in one request"""
	if isinstance(pgs, str):
		pgs = json.loads(pgs)

	if len(pgs) > 200:
		jingrow.throw(_("Only 200 inserts allowed in one request"))

	return [insert_pg(pg).name for pg in pgs]


@jingrow.whitelist(methods=["POST", "PUT"])
def save(pg):
	"""Update (save) an existing document

	:param pg: JSON or dict object with the properties of the document to be updated"""
	if isinstance(pg, str):
		pg = json.loads(pg)

	pg = jingrow.get_pg(pg)
	pg.save()

	return pg.as_dict()


@jingrow.whitelist(methods=["POST", "PUT"])
def rename_pg(pagetype, old_name, new_name, merge=False):
	"""Rename document

	:param pagetype: PageType of the document to be renamed
	:param old_name: Current `name` of the document to be renamed
	:param new_name: New `name` to be set"""
	new_name = jingrow.rename_pg(pagetype, old_name, new_name, merge=merge)
	return new_name


@jingrow.whitelist(methods=["POST", "PUT"])
def submit(pg):
	"""Submit a document

	:param pg: JSON or dict object to be submitted remotely"""
	if isinstance(pg, str):
		pg = json.loads(pg)

	pg = jingrow.get_pg(pg)
	pg.submit()

	return pg.as_dict()


@jingrow.whitelist(methods=["POST", "PUT"])
def cancel(pagetype, name):
	"""Cancel a document

	:param pagetype: PageType of the document to be cancelled
	:param name: name of the document to be cancelled"""
	wrapper = jingrow.get_pg(pagetype, name)
	wrapper.cancel()

	return wrapper.as_dict()


@jingrow.whitelist(methods=["DELETE", "POST"])
def delete(pagetype, name):
	"""Delete a remote document

	:param pagetype: PageType of the document to be deleted
	:param name: name of the document to be deleted"""
	delete_pg(pagetype, name)


@jingrow.whitelist(methods=["POST", "PUT"])
def bulk_update(pgs):
	"""Bulk update documents

	:param pgs: JSON list of documents to be updated remotely. Each document must have `pagename` property"""
	pgs = json.loads(pgs)
	failed_pgs = []
	for pg in pgs:
		pg.pop("flags", None)
		try:
			existing_pg = jingrow.get_pg(pg["pagetype"], pg["pagename"])
			existing_pg.update(pg)
			existing_pg.save()
		except Exception:
			failed_pgs.append({"pg": pg, "exc": jingrow.utils.get_traceback()})

	return {"failed_pgs": failed_pgs}


@jingrow.whitelist()
def has_permission(pagetype, pagename, perm_type="read"):
	"""Returns a JSON with data whether the document has the requested permission

	:param pagetype: PageType of the document to be checked
	:param pagename: `name` of the document to be checked
	:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`"""
	# perm_type can be one of read, write, create, submit, cancel, report
	return {"has_permission": jingrow.has_permission(pagetype, perm_type.lower(), pagename)}


@jingrow.whitelist()
def get_pg_permissions(pagetype, pagename):
	"""Returns an evaluated document permissions dict like `{"read":1, "write":1}`

	:param pagetype: PageType of the document to be evaluated
	:param pagename: `name` of the document to be evaluated
	"""
	pg = jingrow.get_pg(pagetype, pagename)
	return {"permissions": jingrow.permissions.get_pg_permissions(pg)}


@jingrow.whitelist()
def get_password(pagetype, name, fieldname):
	"""Return a password type property. Only applicable for System Managers

	:param pagetype: PageType of the document that holds the password
	:param name: `name` of the document that holds the password
	:param fieldname: `fieldname` of the password property
	"""
	jingrow.only_for("System Manager")
	return jingrow.get_pg(pagetype, name).get_password(fieldname)


@jingrow.whitelist()
@deprecated
def get_js(items):
	"""Load JS code files.  Will also append translations
	and extend `jingrow._messages`

	:param items: JSON list of paths of the js files to be loaded."""
	items = json.loads(items)
	out = []
	for src in items:
		src = src.strip("/").split("/")

		if ".." in src or src[0] != "assets":
			jingrow.throw(_("Invalid file path: {0}").format("/".join(src)))

		contentpath = os.path.join(jingrow.local.sites_path, *src)
		with open(contentpath) as srcfile:
			code = jingrow.utils.cstr(srcfile.read())

		out.append(code)

	return out


@jingrow.whitelist(allow_guest=True)
def get_time_zone():
	"""Returns default time zone"""
	return {"time_zone": jingrow.defaults.get_defaults().get("time_zone")}


@jingrow.whitelist(allow_guest=True)
def get_local_lang():
	"""Returns system language (from System Settings). Defaults to 'en'."""
	# Prefer thread-local language if available (no cache/DB access)
	lang = getattr(jingrow.local, "lang", None)
	if lang:
		return {"language": lang}

	# Fallbacks: system defaults (cached) then db default, then 'en'
	try:
		lang = jingrow.defaults.get_defaults().get("language")
	except Exception:
		lang = None

	if not lang:
		try:
			lang = jingrow.db.get_default("lang")
		except Exception:
			lang = None

	return {"language": lang or "en"}


@jingrow.whitelist(methods=["POST", "PUT"])
def attach_file(
	filename=None,
	filedata=None,
	pagetype=None,
	pagename=None,
	folder=None,
	decode_base64=False,
	is_private=None,
	pagefield=None,
):
	"""Attach a file to Page

	:param filename: filename e.g. test-file.txt
	:param filedata: base64 encode filedata which must be urlencoded
	:param pagetype: Reference PageType to attach file to
	:param pagename: Reference PageName to attach file to
	:param folder: Folder to add File into
	:param decode_base64: decode filedata from base64 encode, default is False
	:param is_private: Attach file as private file (1 or 0)
	:param pagefield: file to attach to (optional)"""

	pg = jingrow.get_pg(pagetype, pagename)
	pg.check_permission()

	file = jingrow.get_pg(
		{
			"pagetype": "File",
			"file_name": filename,
			"attached_to_pagetype": pagetype,
			"attached_to_name": pagename,
			"attached_to_field": pagefield,
			"folder": folder,
			"is_private": is_private,
			"content": filedata,
			"decode": decode_base64,
		}
	).save()

	if pagefield and pagetype:
		pg.set(pagefield, file.file_url)
		pg.save()

	return file


@jingrow.whitelist()
def is_document_amended(pagetype, pagename):
	if jingrow.permissions.has_permission(pagetype):
		try:
			return jingrow.db.exists(pagetype, {"amended_from": pagename})
		except jingrow.db.InternalError:
			pass

	return False


@jingrow.whitelist()
def validate_link(pagetype: str, pagename: str, fields=None):
	if not isinstance(pagetype, str):
		jingrow.throw(_("PageType must be a string"))

	if not isinstance(pagename, str):
		jingrow.throw(_("Page Name must be a string"))

	if pagetype != "PageType":
		parent_pagetype = None
		if jingrow.get_meta(pagetype).istable:  # needed for links to child rows
			parent_pagetype = jingrow.db.get_value(pagetype, pagename, "parenttype")
		if not (
			jingrow.has_permission(pagetype, "select", parent_pagetype=parent_pagetype)
			or jingrow.has_permission(pagetype, "read", parent_pagetype=parent_pagetype)
		):
			jingrow.throw(
				_("You do not have Read or Select Permissions for {}").format(jingrow.bold(pagetype)),
				jingrow.PermissionError,
			)

	values = jingrow._dict()

	if is_virtual_pagetype(pagetype):
		try:
			jingrow.get_pg(pagetype, pagename)
			values.name = pagename
		except jingrow.DoesNotExistError:
			jingrow.clear_last_message()
			jingrow.msgprint(
				_("Page {0} {1} does not exist").format(jingrow.bold(pagetype), jingrow.bold(pagename)),
			)
		return values

	values.name = jingrow.db.get_value(pagetype, pagename, cache=True)

	fields = jingrow.parse_json(fields)
	if not values.name or not fields:
		return values

	try:
		values.update(get_value(pagetype, fields, pagename))
	except jingrow.PermissionError:
		jingrow.clear_last_message()
		jingrow.msgprint(
			_("You need {0} permission to fetch values from {1} {2}").format(
				jingrow.bold(_("Read")), jingrow.bold(pagetype), jingrow.bold(pagename)
			),
			title=_("Cannot Fetch Values"),
			indicator="orange",
		)

	return values


def insert_pg(pg) -> "Page":
	"""Inserts document and returns parent document object with appended child document
	if `pg` is child document else returns the inserted document object

	:param pg: pg to insert (dict)"""

	pg = jingrow._dict(pg)
	if jingrow.is_table(pg.pagetype):
		if not (pg.parenttype and pg.parent and pg.parentfield):
			jingrow.throw(_("Parenttype, Parent and Parentfield are required to insert a child record"))

		# inserting a child record
		parent = jingrow.get_pg(pg.parenttype, pg.parent)
		parent.append(pg.parentfield, pg)
		parent.save()
		return parent

	return jingrow.get_pg(pg).insert()


def delete_pg(pagetype, name):
	"""Deletes document
	if pagetype is a child table, then deletes the child record using the parent pg
	so that the parent pg's `on_update` is called
	"""

	if jingrow.is_table(pagetype):
		values = jingrow.db.get_value(pagetype, name, ["parenttype", "parent", "parentfield"])
		if not values:
			raise jingrow.DoesNotExistError(pagetype=pagetype)

		parenttype, parent, parentfield = values
		parent = jingrow.get_pg(parenttype, parent)
		if not parent.has_permission("write"):
			raise jingrow.DoesNotExistError(pagetype=pagetype)

		for row in parent.get(parentfield):
			if row.name == name:
				parent.remove(row)
				parent.save()
				break
	else:
		jingrow.delete_pg(pagetype, name, ignore_missing=False)
