// Matrix Construct
//
// Copyright (C) Matrix Construct Developers, Authors & Contributors
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.

namespace ircd::m::search
{
	static bool handle_result(result &, const query &);
	static bool handle_content(result &, const query &, const json::object &);
	static bool query_all_rooms(result &, const query &);
	static bool query_room(result &, const query &, const room::id &);
	static bool query_rooms(result &, const query &);
	static void handle_room_events(client &, const resource::request &, const json::object &, json::stack::object &);
	static resource::response search_post_handle(client &, const resource::request &);

	extern conf::item<size_t> limit_override;
	extern conf::item<bool> count_total;
	extern resource::method search_post;
	extern resource search_resource;
	extern log::log log;
}

ircd::mapi::header
IRCD_MODULE
{
	"Client 11.14 :Server Side Search"
};

decltype(ircd::m::search::log)
ircd::m::search::log
{
	"m.search"
};

decltype(ircd::m::search::search_resource)
ircd::m::search::search_resource
{
	"/_matrix/client/r0/search",
	{
		"(11.14.1) The search API allows clients to perform full text search"
		" across events in all rooms that the user has been in, including"
		" those that they have left. Only events that the user is allowed to"
		" see will be searched, e.g. it won't include events in rooms that"
		" happened after you left."
	}
};

decltype(ircd::m::search::search_post)
ircd::m::search::search_post
{
	search_resource, "POST", search_post_handle,
	{
		search_post.REQUIRES_AUTH |
		search_post.RATE_LIMITED,

		// Some queries can take a really long time, especially under
		// development. We don't need the default request timer getting
		// in the way for now.
		60s,
	}
};

decltype(ircd::m::search::count_total)
ircd::m::search::count_total
{
	{ "name",     "ircd.m.search.count.total" },
	{ "default",  false                       },
};

decltype(ircd::m::search::limit_override)
ircd::m::search::limit_override
{
	{ "name",     "ircd.m.search.limit.override" },
	{ "default",  1L                             },
};

ircd::m::resource::response
ircd::m::search::search_post_handle(client &client,
                                    const resource::request &request)
{
	const json::object &search_categories
	{
		request["search_categories"]
	};

	resource::response::chunked response
	{
		client, http::OK
	};

	json::stack out
	{
		response.buf, response.flusher()
	};

	json::stack::object top
	{
		out
	};

	json::stack::object result_categories
	{
		top, "search_categories"
	};

	if(search_categories.has("room_events"))
	{
		json::stack::object room_events_result
		{
			result_categories, "room_events"
		};

		handle_room_events(client, request, search_categories, room_events_result);
	}

	return response;
}

void
ircd::m::search::handle_room_events(client &client,
                                    const resource::request &request,
                                    const json::object &search_categories,
                                    json::stack::object &room_events_result)
try
{
	const m::search::room_events room_events
	{
		search_categories["room_events"]
	};

	const string_view search_input
	{
		at<"search_term"_>(room_events)
	};

	const pair<string_view> kvs
	{
		split(search_input, " :")
	};

	const auto when{[&kvs]
	(const std::initializer_list<string_view> &names, auto&& closure)
	{
		tokens(kvs.first, ' ', [&names, &closure]
		(const string_view &kv)
		{
			const auto &[key, val]
			{
				split(kv, '=')
			};

			if(std::find(begin(names), end(names), key) == end(names))
				return true;

			closure(key, val);
			return false;
		});
	}};

	uint filter_keys {0};
	m::room_event_filter room_event_filter
	{
		json::get<"filter"_>(room_events)
	};

	json::strung senders;
	when({"sender", ""}, [&](const auto &key, const auto &val)
	{
		if(!valid(m::id::USER, val))
			return;

		senders = json::get<"senders"_>(room_event_filter);
		senders = json::append(senders, val);
		json::get<"senders"_>(room_event_filter) = senders;
		filter_keys += 1;
	});

	json::strung not_senders;
	when({"!sender", "!"}, [&](const auto &key, const auto &val)
	{
		if(!valid(m::id::USER, val))
			return;

		not_senders = json::get<"not_senders"_>(room_event_filter);
		not_senders = json::append(not_senders, val);
		json::get<"not_senders"_>(room_event_filter) = not_senders;
		filter_keys += 1;
	});

	bool case_sensitive {false};
	when({"case", "ci"}, [&](const auto &key, const auto &val)
	{
		case_sensitive = key == "case";
		filter_keys += 0; // doesn't count; no case-insensitive wildcard.
	});

	// Override the limit to 1 to return a result and appease the user as
	// quickly as possible. The client can call us again for more results.
	size_t limit
	{
		limit_override?
			size_t(limit_override):
			size_t(json::get<"limit"_>(room_event_filter))
	};

	// Key command for the user to override the limit to workaround pagination
	// bugs in riot which don't properly request next batch to fill viewport.
	when({"limit"}, [&](const auto &key, const auto &val)
	{
		// limit=-1 is unlimited
		// limit=0 is unchanged
		// limit=n is overriding
		limit = lex_cast<ssize_t>(val)?: limit;
		filter_keys += 0; // doesn't count for wildcard.
	});

	// The limit value is limited to the viewport size so we can force
	// pagination rather than search the whole room with one giant result.
	limit = std::clamp
	(
		limit,
		1UL,
		size_t(m::room::events::viewport_size)
	);

	const string_view search_term
	{
		// Any string after the separator is the search term.
		kvs.second?
			kvs.second:

		// When only filter keys are given the search term is wildcard.
		filter_keys?
			string_view{}:

		// No filter keys or separator; it is the search term.
			kvs.first
	};

	const json::object &event_context
	{
		json::get<"event_context"_>(room_events)
	};

	// Spec sez default is 5. Reference client does not make any use of
	// result context if provided.
	const ushort context_default
	{
		0
	};

	const search::query query
	{
		.user_id = request.user_id,
		.batch = request.query.get<size_t>("next_batch", 0UL),
		.room_events = room_events,
		.filter = room_event_filter,
		.search_term = search_term,
		.limit = limit,
		.before_limit = event_context.get("before_limit", context_default),
		.after_limit = event_context.get("after_limit", context_default),
		.case_sensitive = case_sensitive,
	};

	log::logf
	{
		log, log::DEBUG,
		"Query '%s' by %s batch:%ld order_by:%s inc_state:%b rooms:%zu limit:%zu filter:%s",
		query.search_term,
		string_view{query.user_id},
		query.batch,
		json::get<"order_by"_>(query.room_events),
		json::get<"include_state"_>(query.room_events),
		json::get<"rooms"_>(query.filter).size(),
		query.limit,
		string_view{room_event_filter.source},
	};

	search::result result
	{
		room_events_result.s
	};

	const bool finished
	{
		query_rooms(result, query)
	};

	// Spec sez this is total results, but riot doesn't use it. Counting total
	// results is very expensive right now, so we'll just report the count we
	// have for now...
	json::stack::member
	{
		room_events_result, "count", json::value
		{
			long(result.count + !finished)
		}
	};

	//TODO: XXX
	json::stack::array
	{
		room_events_result, "highlights"
	};

	//TODO: XXX
	json::stack::object
	{
		room_events_result, "state"
	};

	if(!finished)
		json::stack::member
		{
			room_events_result, "next_batch", json::value
			{
				lex_cast(result.skipped + result.checked), json::STRING
			}
		};

	char tmbuf[48];
	log::logf
	{
		log, log::DEBUG,
		"Result '%s' by %s batch[%ld -> %ld] count:%lu append:%lu match:%lu check:%lu skip:%lu in %s",
		query.search_term,
		string_view{query.user_id},
		query.batch,
		result.event_idx,
		result.count,
		result.appends,
		result.matched,
		result.checked,
		result.skipped,
		result.elapsed.pretty(tmbuf),
	};
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "search :%s",
		e.what()
	};

	throw;
}

bool
ircd::m::search::query_rooms(result &result,
                             const query &query)
{
	const json::array rooms
	{
		json::get<"rooms"_>(query.filter)
	};

	json::stack::array results
	{
		*result.out, "results"
	};

	if(rooms.empty())
		return query_all_rooms(result, query);

	for(const json::string room_id : rooms)
		if(!query_room(result, query, room_id))
			return false;

	return true;
}

bool
ircd::m::search::query_room(result &result,
                            const query &query,
                            const room::id &room_id)
{
	const m::room room
	{
		room_id
	};

	if(!visible(room, query.user_id))
		throw m::ACCESS_DENIED
		{
			"You are not permitted to view %s",
			string_view{room_id},
		};

	const m::room::iterate content
	{
		room, "content"
	};

	return content.for_each([&result, &query]
	(const json::object &content, const auto &depth, const auto &event_idx)
	{
		result.event_idx = event_idx;
		return handle_content(result, query, content);
	});
}

bool
ircd::m::search::query_all_rooms(result &result,
                                 const query &query)
{
	if(!is_oper(query.user_id))
		throw m::ACCESS_DENIED
		{
			"You are not an operator."
		};

	return m::events::content::for_each([&result, &query]
	(const auto &event_idx, const json::object &content)
	{
		result.event_idx = event_idx;
		return handle_content(result, query, content);
	});
}

bool
ircd::m::search::handle_content(result &result,
                                const query &query,
                                const json::object &content)
try
{
	if(result.skipped < query.batch)
	{
		++result.skipped;
		return true;
	}

	const json::string body
	{
		content["body"]
	};

	const bool match_term
	{
		false
		|| (query.case_sensitive && has(body, query.search_term))
		|| (!query.case_sensitive && ihas(body, query.search_term))
	};

	const bool match
	{
		true
		&& (!query.search_term || match_term)
		&& m::match(query.filter, result.event_idx)
	};

	const bool handled
	{
		match && handle_result(result, query)
	};

	result.checked += 1;
	result.matched += match;
	result.count += handled;
	return result.count < query.limit;
}
catch(const ctx::interrupted &e)
{
	log::dwarning
	{
		log, "Query handling '%s' by '%s' event_idx:%lu :%s",
		query.search_term,
		string_view{query.user_id},
		result.event_idx,
		e.what(),
	};

	throw;
}
catch(const std::system_error &e)
{
	log::derror
	{
		log, "Query handling for '%s' by '%s' event_idx:%lu :%s",
		query.search_term,
		string_view{query.user_id},
		result.event_idx,
		e.what(),
	};

	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Query handling for '%s' by '%s' event_idx:%lu :%s",
		query.search_term,
		string_view{query.user_id},
		result.event_idx,
		e.what(),
	};

	return true;
}

bool
ircd::m::search::handle_result(result &result,
                               const query &query)
try
{
	const m::event_filter event_filter
	{
		query.filter
	};

	const m::event::fetch event
	{
		result.event_idx
	};

	assert(result.out);
	json::stack::checkpoint cp
	{
		*result.out, false
	};

	json::stack::object object
	{
		*result.out
	};

	json::stack::member
	{
		object, "rank", json::value(result.rank)
	};

	bool ret{false};
	{
		json::stack::object result_event
		{
			object, "result"
		};

		ret = event::append
		{
			result_event, event,
			{
				.event_idx = &result.event_idx,
				.user_id = &query.user_id,
				.event_filter = &event_filter,
				.query_prev_state = false,
				.query_visible = true,
			},
		};

		result.appends += ret;
		cp.committing(ret);
	}

	if(!query.before_limit && !query.after_limit)
		return ret;

	const m::room room
	{
		json::get<"room_id"_>(event)
	};

	m::room::events it
	{
		room
	};

	json::stack::object result_context
	{
		object, "context"
	};

	size_t before(0);
	if(likely(!it.seek(result.event_idx)))
	{
		json::stack::array events_before
		{
			result_context, "events_before"
		};

		for(--it; it && before < query.before_limit; ++before, --it)
		{
			const event::idx event_idx
			{
				it.event_idx()
			};

			result.appends += event::append
			{
				events_before, event,
				{
					.event_idx = &event_idx,
					.user_id = &query.user_id,
					.event_filter = &event_filter,
					.query_prev_state = false,
					.query_visible = true,
				},
			};
		}
	}

	size_t after(0);
	if(likely(it.seek(result.event_idx)))
	{
		json::stack::array events_after
		{
			result_context, "events_after"
		};

		for(++it; it && after < query.after_limit; ++after, ++it)
		{
			const event::idx event_idx
			{
				it.event_idx()
			};

			result.appends += event::append
			{
				events_after, event,
				{
					.event_idx = &event_idx,
					.user_id = &query.user_id,
					.event_filter = &event_filter,
					.query_prev_state = false,
					.query_visible = true,
				},
			};
		}
	}

	return ret;
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::system_error &e)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Result handling for '%s' by '%s' event_idx:%lu :%s",
		query.search_term,
		string_view{query.user_id},
		result.event_idx,
		e.what(),
	};

	return false;
}
