# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt

from __future__ import unicode_literals

import frappe
import re
import redis
from frappe.utils import cint, strip_html_tags
from frappe.model.base_document import get_controller
from six import text_type


def setup_global_search_table():
	"""
	Creates __global_seach table
	:return:
	"""
	if not '__global_search' in frappe.db.get_tables():
		frappe.db.sql('''create table __global_search(
			doctype varchar(100),
			name varchar(140),
			title varchar(140),
			content text,
			fulltext(content),
			route varchar(140),
			published int(1) not null default 0,
			unique `doctype_name` (doctype, name))
			COLLATE=utf8mb4_unicode_ci
			ENGINE=MyISAM
			CHARACTER SET=utf8mb4''')


def reset():
	"""
	Deletes all data in __global_search
	:return:
	"""
	frappe.db.sql('delete from __global_search')


def get_doctypes_with_global_search(with_child_tables=True):
	"""
	Return doctypes with global search fields
	:param with_child_tables:
	:return:
	"""
	def _get():
		global_search_doctypes = []
		filters = {}
		if not with_child_tables:
			filters = {"istable": ["!=", 1], "issingle": ["!=", 1]}
		for d in frappe.get_all('DocType', fields=['name', 'module'], filters=filters):
			meta = frappe.get_meta(d.name)
			if len(meta.get_global_search_fields()) > 0:
				global_search_doctypes.append(d)

		installed_apps = frappe.get_installed_apps()
		module_app = frappe.local.module_app

		doctypes = [
			d.name for d in global_search_doctypes
			if module_app.get(frappe.scrub(d.module))
			and module_app[frappe.scrub(d.module)] in installed_apps
		]

		return doctypes

	return frappe.cache().get_value('doctypes_with_global_search', _get)


def rebuild_for_doctype(doctype):
	"""
	Rebuild entries of doctype's documents in __global_search on change of
	searchable fields
	:param doctype: Doctype
	"""

	def _get_filters():
		filters = frappe._dict({ "docstatus": ["!=", 2] })
		if meta.has_field("enabled"):
			filters.enabled = 1
		if meta.has_field("disabled"):
			filters.disabled = 0

		return filters

	meta = frappe.get_meta(doctype)
	if cint(meta.istable) == 1:
		parent_doctypes = frappe.get_all("DocField", fields="parent", filters={
			"fieldtype": "Table",
			"options": doctype
		})
		for p in parent_doctypes:
			rebuild_for_doctype(p.parent)

		return

	# Delete records
	delete_global_search_records_for_doctype(doctype)

	parent_search_fields = meta.get_global_search_fields()
	fieldnames = get_selected_fields(meta, parent_search_fields)

	# Get all records from parent doctype table
	all_records = frappe.get_all(doctype, fields=fieldnames, filters=_get_filters())

	# Children data
	all_children, child_search_fields = get_children_data(doctype, meta)
	all_contents = []

	for doc in all_records:
		content = []
		for field in parent_search_fields:
			value = doc.get(field.fieldname)
			if value:
				content.append(get_formatted_value(value, field))

		# get children data
		for child_doctype, records in all_children.get(doc.name, {}).items():
			for field in child_search_fields.get(child_doctype):
				for r in records:
					if r.get(field.fieldname):
						content.append(get_formatted_value(r.get(field.fieldname), field))

		if content:
			# if doctype published in website, push title, route etc.
			published = 0
			title, route = "", ""
			try:
				if hasattr(get_controller(doctype), "is_website_published") and meta.allow_guest_to_view:
					d = frappe.get_doc(doctype, doc.name)
					published = 1 if d.is_website_published() else 0
					title = d.get_title()
					route = d.get("route")
			except ImportError:
				# some doctypes has been deleted via future patch, hence controller does not exists
				pass

			all_contents.append({
				"doctype": frappe.db.escape(doctype),
				"name": frappe.db.escape(doc.name),
				"content": frappe.db.escape(' ||| '.join(content or '')),
				"published": published,
				"title": frappe.db.escape(title or ''),
				"route": frappe.db.escape(route or '')
			})
	if all_contents:
		insert_values_for_multiple_docs(all_contents)


def delete_global_search_records_for_doctype(doctype):
	frappe.db.sql('''
		delete
			from __global_search
		where
			doctype = %s''', doctype, as_dict=True)


def get_selected_fields(meta, global_search_fields):
	fieldnames = [df.fieldname for df in global_search_fields]
	if meta.istable==1:
		fieldnames.append("parent")
	elif "name" not in fieldnames:
		fieldnames.append("name")

	if meta.has_field("is_website_published"):
		fieldnames.append("is_website_published")

	return fieldnames


def get_children_data(doctype, meta):
	"""
		Get all records from all the child tables of a doctype

		all_children = {
			"parent1": {
				"child_doctype1": [
					{
						"field1": val1,
						"field2": val2
					}
				]
			}
		}

	"""
	all_children = frappe._dict()
	child_search_fields = frappe._dict()

	for child in meta.get_table_fields():
		child_meta = frappe.get_meta(child.options)
		search_fields = child_meta.get_global_search_fields()
		if search_fields:
			child_search_fields.setdefault(child.options, search_fields)
			child_fieldnames = get_selected_fields(child_meta, search_fields)
			child_records = frappe.get_all(child.options, fields=child_fieldnames, filters={
				"docstatus": ["!=", 1],
				"parenttype": doctype
			})

			for record in child_records:
				all_children.setdefault(record.parent, frappe._dict())\
					.setdefault(child.options, []).append(record)

	return all_children, child_search_fields


def insert_values_for_multiple_docs(all_contents):
	values = []
	for content in all_contents:
		values.append("( '{doctype}', '{name}', '{content}', '{published}', '{title}', '{route}')"
			.format(**content))

	batch_size = 50000
	for i in range(0, len(values), batch_size):
		batch_values = values[i:i + batch_size]
		# ignoring duplicate keys for doctype_name
		frappe.db.sql('''
			insert ignore into __global_search
				(doctype, name, content, published, title, route)
			values
				{0}
			'''.format(", ".join(batch_values)))


def update_global_search(doc):
	"""
	Add values marked with `in_global_search` to
	`frappe.flags.update_global_search` from given doc
	:param doc: Document to be added to global search
	"""

	if doc.docstatus > 1 or (doc.meta.has_field("enabled") and not doc.get("enabled")) \
		or doc.get("disabled"):
			return

	if frappe.flags.update_global_search==None:
		frappe.flags.update_global_search = []

	content = []
	for field in doc.meta.get_global_search_fields():
		if doc.get(field.fieldname) and field.fieldtype != "Table":
			content.append(get_formatted_value(doc.get(field.fieldname), field))

	# Get children
	for child in doc.meta.get_table_fields():
		for d in doc.get(child.fieldname):
			if d.parent == doc.name:
				for field in d.meta.get_global_search_fields():
					if d.get(field.fieldname):
						content.append(get_formatted_value(d.get(field.fieldname), field))

	if content:
		published = 0
		if hasattr(doc, 'is_website_published') and doc.meta.allow_guest_to_view:
			published = 1 if doc.is_website_published() else 0

		frappe.flags.update_global_search.append(
			dict(doctype=doc.doctype, name=doc.name, content=' ||| '.join(content or ''),
				published=published, title=doc.get_title(), route=doc.get('route')))
		enqueue_global_search()


def enqueue_global_search():
	if frappe.flags.update_global_search:
		try:
			frappe.enqueue('frappe.utils.global_search.sync_global_search',
				now=frappe.flags.in_test or frappe.flags.in_install or frappe.flags.in_migrate,
				flags=frappe.flags.update_global_search, enqueue_after_commit=True)
		except redis.exceptions.ConnectionError:
			sync_global_search()

		frappe.flags.update_global_search = []


def get_formatted_value(value, field):
	"""
	Prepare field from raw data
	:param value:
	:param field:
	:return:
	"""

	from six.moves.html_parser import HTMLParser

	if getattr(field, 'fieldtype', None) in ["Text", "Text Editor"]:
		h = HTMLParser()
		value = h.unescape(value)
		value = (re.subn(r'<[\s]*(script|style).*?</\1>(?s)', '', text_type(value))[0])
		value = ' '.join(value.split())
	return field.label + " : " + strip_html_tags(text_type(value))


def sync_global_search(flags=None):
	"""
	Add values from `flags` (frappe.flags.update_global_search) to __global_search.
	This is called internally at the end of the request.
	:param flags:
	:return:
	"""

	if not flags:
		flags = frappe.flags.update_global_search

	# Can pass flags manually as frappe.flags.update_global_search isn't reliable at a later time,
	# when syncing is enqueued
	for value in flags:
		frappe.db.sql('''
			insert into __global_search
				(doctype, name, content, published, title, route)
			values
				(%(doctype)s, %(name)s, %(content)s, %(published)s, %(title)s, %(route)s)
			on duplicate key update
				content = %(content)s''', value)

	frappe.flags.update_global_search = []


def delete_for_document(doc):
	"""
	Delete the __global_search entry of a document that has
	been deleted
	:param doc: Deleted document
	"""

	frappe.db.sql('''
		delete
			from __global_search
		where
			doctype = %s and
			name = %s''', (doc.doctype, doc.name), as_dict=True)


@frappe.whitelist()
def search(text, start=0, limit=20, doctype=""):
	"""
	Search for given text in __global_search
	:param text: phrase to be searched
	:param start: start results at, default 0
	:param limit: number of results to return, default 20
	:return: Array of result objects
	"""

	text = "+" + text + "*"
	if not doctype:
		results = frappe.db.sql('''
			select
				doctype, name, content
			from
				__global_search
			where
				match(content) against (%s IN BOOLEAN MODE)
			limit {start}, {limit}'''.format(start=start, limit=limit), text+"*", as_dict=True)
	else:
		results = frappe.db.sql('''
			select
				doctype, name, content
			from
				__global_search
			where
				doctype = %s AND
				match(content) against (%s IN BOOLEAN MODE)
			limit {start}, {limit}'''.format(start=start, limit=limit), (doctype, text), as_dict=True)

	for r in results:
		try:
			if frappe.get_meta(r.doctype).image_field:
				r.image = frappe.db.get_value(r.doctype, r.name, frappe.get_meta(r.doctype).image_field)
		except Exception:
			frappe.clear_messages()

	return results


@frappe.whitelist(allow_guest=True)
def web_search(text, start=0, limit=20):
	"""
	Search for given text in __global_search where published = 1
	:param text: phrase to be searched
	:param start: start results at, default 0
	:param limit: number of results to return, default 20
	:return: Array of result objects
	"""

	text = "+" + text + "*"
	results = frappe.db.sql('''
		select
			doctype, name, content, title, route
		from
			__global_search
		where
			published = 1 and
			match(content) against (%s IN BOOLEAN MODE)
		limit {start}, {limit}'''.format(start=start, limit=limit),
		text, as_dict=True)
	return results
