// The Construct
//
// Copyright (C) The Construct Developers, Authors & Contributors
// Copyright (C) 2016-2020 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.

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

decltype(ircd::m::push::pusher::type_prefix)
ircd::m::push::pusher::type_prefix
{
	"ircd.push.pusher"
};

decltype(ircd::m::push::rule::type_prefix)
ircd::m::push::rule::type_prefix
{
	"ircd.push.rule"
};

//
// request
//

template<>
decltype(ircd::util::instance_list<ircd::m::push::request>::allocator)
ircd::util::instance_list<ircd::m::push::request>::allocator
{};

template<>
decltype(ircd::util::instance_list<ircd::m::push::request>::list)
ircd::util::instance_list<ircd::m::push::request>::list
{
	allocator
};

decltype(ircd::m::push::request::enable)
ircd::m::push::request::enable
{
	{ "name",     "ircd.m.push.request.enable" },
	{ "default",  true                         },
};

decltype(ircd::m::push::request::timeout)
ircd::m::push::request::timeout
{
	{ "name",     "ircd.m.push.request.timeout" },
	{ "default",  8L                            },
};

decltype(ircd::m::push::request::mutex)
ircd::m::push::request::mutex;

decltype(ircd::m::push::request::dock)
ircd::m::push::request::dock;

decltype(ircd::m::push::request::id_ctr)
ircd::m::push::request::id_ctr;

//
// match
//

namespace ircd::m::push
{
	static bool unknown_condition_kind(const event &, const cond &, const match::opts &);
	static bool sender_notification_permission(const event &, const cond &, const match::opts &);
	static bool contains_display_name(const event &, const cond &, const match::opts &);
	static bool state_key_user_mxid(const event &, const cond &, const match::opts &);
	static bool contains_user_mxid(const event &, const cond &, const match::opts &);
	static bool room_member_count(const event &, const cond &, const match::opts &);
	static bool event_property_contains(const event &, const cond &, const match::opts &);
	static bool event_property_is(const event &, const cond &, const match::opts &);
	static bool event_match(const event &, const cond &, const match::opts &);
}

decltype(ircd::m::push::match::cond_kind)
ircd::m::push::match::cond_kind
{
	event_match,
	event_property_is,
	event_property_contains,
	room_member_count,
	contains_user_mxid,
	state_key_user_mxid,
	contains_display_name,
	sender_notification_permission,
	unknown_condition_kind,
};

decltype(ircd::m::push::match::cond_kind_name)
ircd::m::push::match::cond_kind_name
{
	"event_match",
	"event_property_is",
	"event_property_contains",
	"room_member_count",
	"contains_user_mxid",
	"state_key_user_mxid",
	"contains_display_name",
	"sender_notification_permission",
};

//
// match::match
//

ircd::m::push::match::match(const event &event,
                            const rule &rule,
                            const match::opts &opts)
:returns<bool>{[&event, &rule, &opts]
{
	if(json::get<"pattern"_>(rule))
	{
		const push::cond cond
		{
			{ "kind",     "event_match"               },
			{ "key",      "content.body"              },
			{ "pattern",  json::get<"pattern"_>(rule) },
		};

		if(!match(event, cond, opts))
			return false;
	}

	for(const json::object cond : json::get<"conditions"_>(rule))
		if(!match(event, push::cond(cond), opts))
			return false;

	return true;
}}
{
}

ircd::m::push::match::match(const event &event,
                            const cond &cond,
                            const match::opts &opts)
:returns<bool>{[&event, &cond, &opts]
{
	const string_view &kind
	{
		json::get<"kind"_>(cond)
	};

	const auto pos
	{
		indexof(kind, string_views(cond_kind_name))
	};

	assert(pos <= sizeof(cond_kind_name) / sizeof(string_view));
	const auto &func
	{
		cond_kind[pos]
	};

	return func(event, cond, opts);
}}
{
}

//
// push::match condition functors (internal)
//

namespace ircd::m::push
{
	static string_view value_extract(const event &, const cond &);
}

bool
ircd::m::push::event_match(const event &event,
                           const cond &cond,
                           const match::opts &opts)
try
{
	assert(json::get<"kind"_>(cond) == "event_match");

	const auto value
	{
		value_extract(event, cond)
	};

	 //TODO: XXX spec leading/trailing; not imatch
	const globular_imatch pattern
	{
		json::get<"pattern"_>(cond)
	};

	return pattern(value);
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Push condition 'event_match' %s :%s",
		string_view{event.event_id},
		e.what(),
	};

	return false;
}

bool
ircd::m::push::event_property_is(const event &event,
                                 const cond &cond,
                                 const match::opts &opts)
try
{
	assert(json::get<"kind"_>(cond) == "event_property_is");

	const auto value
	{
		value_extract(event, cond)
	};

	const json::value a{value}, b
	{
		json::get<"value"_>(cond)
	};

	return a == b;
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Push condition 'event_property_is' %s :%s",
		string_view{event.event_id},
		e.what(),
	};

	return false;
}

bool
ircd::m::push::event_property_contains(const event &event,
                                       const cond &cond,
                                       const match::opts &opts)
try
{
	assert(json::get<"kind"_>(cond) == "event_property_contains");

	const json::array array
	{
		value_extract(event, cond)
	};

	if(!json::type(array, json::ARRAY))
		return false;

	const json::value a
	{
		at<"value"_>(cond)
	};

	for(const json::value b : array)
		if(type(a) == type(b) && a == b)
			return true;

	return false;
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Push condition 'event_property_contains' %s :%s",
		string_view{event.event_id},
		e.what(),
	};

	return false;
}

bool
ircd::m::push::contains_user_mxid(const event &event,
                                  const cond &cond,
                                  const match::opts &opts)
try
{
	assert(json::get<"kind"_>(cond) == "contains_user_mxid");

	assert(opts.user_id);
	if(unlikely(!opts.user_id))
		return false;

	const json::object &content
	{
		json::get<"content"_>(event)
	};

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

	if(has(body, opts.user_id))
		return true;

	const json::string &formatted_body
	{
		content["formatted_body"]
	};

	if(has(formatted_body, opts.user_id))
		return true;

	return false;
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Push condition 'contains_user_mxid' %s :%s",
		string_view{event.event_id},
		e.what(),
	};

	return false;
}

bool
ircd::m::push::state_key_user_mxid(const event &event,
                                   const cond &cond,
                                   const match::opts &opts)
try
{
	assert(json::get<"kind"_>(cond) == "state_key_user_mxid");

	const auto &state_key
	{
		json::get<"state_key"_>(event)
	};

	assert(opts.user_id);
	return state_key == opts.user_id;
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Push condition 'state_key_user_mxid' %s :%s",
		string_view{event.event_id},
		e.what(),
	};

	return false;
}

bool
ircd::m::push::contains_display_name(const event &event,
                                     const cond &cond,
                                     const match::opts &opts)
try
{
	assert(json::get<"kind"_>(cond) == "contains_display_name");

	const json::object &content
	{
		json::get<"content"_>(event)
	};

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

	if(!body)
		return false;

	assert(opts.user_id);
	if(unlikely(!opts.user_id))
		return false;

	const m::user::profile profile
	{
		opts.user_id
	};

	bool ret{false};
	profile.get(std::nothrow, "displayname", [&ret, &body]
	(const string_view &, const json::string &displayname)
	{
		ret = displayname && has(body, displayname);
	});

	return ret;
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Push condition 'contains_display_name' %s :%s",
		string_view{event.event_id},
		e.what(),
	};

	return false;
}

bool
ircd::m::push::sender_notification_permission(const event &event,
                                              const cond &cond,
                                              const match::opts &opts)
try
{
	assert(json::get<"kind"_>(cond) == "sender_notification_permission");

	const auto &key
	{
		json::get<"key"_>(cond)
	};

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

	const room::power power
	{
		room
	};

	const auto &user_level
	{
		power.level_user(at<"sender"_>(event))
	};

	int64_t required_level
	{
		room::power::default_power_level
	};

	power.for_each("notifications", [&required_level, &key]
	(const auto &name, const auto &level) noexcept
	{
		if(name == key)
		{
			required_level = level;
			return false;
		}
		else return true;
	});

	const bool ret
	{
		user_level >= required_level
	};

	if(!ret)
		log::dwarning
		{
			log, "Insufficient power level %ld for %s to notify '%s' to %s (require:%ld).",
			user_level,
			string_view{json::get<"sender"_>(event)},
			key,
			string_view{room.room_id},
			required_level
		};

	return ret;
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Push condition 'sender_notification_permission' %s :%s",
		string_view{event.event_id},
		e.what(),
	};

	return false;
}

bool
ircd::m::push::room_member_count(const event &event,
                                 const cond &cond,
                                 const match::opts &opts)
try
{
	assert(json::get<"kind"_>(cond) == "room_member_count");

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

	const m::room::members members
	{
		room
	};

	const auto &is
	{
		json::get<"is"_>(cond)
	};

	string_view valstr(is);
	while(valstr && !all_of<std::isdigit>(valstr))
		valstr.pop_front();

	const ulong val
	{
		lex_cast<ulong>(valstr)
	};

	const string_view op
	{
		begin(is), begin(valstr)
	};

	switch(hash(op))
	{
		default:
		case "=="_:
			return
				val?
					members.count("join") == val:
					members.empty("join");

		case ">="_:
			return
				val?
					members.count("join") >= val:
					true;

		case "<="_:
			return
				val?
					members.count("join") <= val:
					members.empty("join");

		case ">"_:
			return
				val?
					members.count("join") > val:
					!members.empty("join");

		case "<"_:
			return
				val > 1?
					members.count("join") < val:
				val == 1?
					members.empty("join"):
					false;
	};
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "Push condition 'room_member_count' %s :%s",
		string_view{event.event_id},
		e.what(),
	};

	return false;
}

bool
ircd::m::push::unknown_condition_kind(const event &event,
                                      const cond &cond,
                                      const match::opts &opts)
{
	const string_view &kind
	{
		json::get<"kind"_>(cond)
	};

	log::derror
	{
		log, "Push condition for %s by %s :unknown kind '%s' rule always fails...",
		string_view{event.event_id},
		string_view{opts.user_id},
		kind,
	};

	return false;
}

//
// match tools (internal)
//

ircd::string_view
ircd::m::push::value_extract(const event &event,
                             const cond &cond)
{
	const auto &[top, path]
	{
		split(json::get<"key"_>(cond), '.')
	};

	string_view value
	{
		json::get(event, top, json::object{})
	};

	tokens(path, '.', [&value]
	(const string_view &key)
	{
		if(!json::type(value, json::OBJECT))
			return false;

		value = json::object(value)[key];
		if(likely(!json::type(value, json::STRING)))
			return true;

		value = json::string(value);
		return false;
	});

	return value;
}

//
// rule
//

bool
ircd::m::push::rule::for_each(const path &path,
                              const closure_bool &closure)
{
	using json::at;

	char typebuf[event::TYPE_MAX_SIZE];
	const string_view &type
	{
		make_type(typebuf, path)
	};

	return events::type::for_each_in(type, [&closure]
	(const string_view &type, const event::idx &event_idx)
	{
		static const event::fetch::opts fopts
		{
			event::keys::include
			{
				"content",
				"room_id",
				"sender",
				"state_key",
			}
		};

		if(!m::room::state::present(event_idx))
			return true;

		const m::event::fetch event
		{
			std::nothrow, event_idx, fopts
		};

		if(!event.valid)
			return true;

		const m::user::id &sender
		{
			at<"sender"_>(event)
		};

		const m::room::id &room_id
		{
			at<"room_id"_>(event)
		};

		if(!my(sender) || !m::user::room::is(room_id, sender))
			return true;

		const push::path path
		{
			make_path(type, at<"state_key"_>(event))
		};

		return closure(at<"sender"_>(event), path, at<"content"_>(event));
	});
}

bool
ircd::m::push::notifying(const rule &rule)
{
	const json::array &actions
	{
		json::get<"actions"_>(rule)
	};

	for(const string_view &action : actions)
	{
		if(!json::type(action, json::STRING))
			continue;

		const json::string &string
		{
			action
		};

		if(string == "notify")
			return true;

		if(string == "coalesce")
			return true;
	}

	return false;
}

bool
ircd::m::push::highlighting(const rule &rule)
{
	const json::array &actions
	{
		json::get<"actions"_>(rule)
	};

	for(const string_view &action : actions)
	{
		if(!json::type(action, json::OBJECT))
			continue;

		const json::object &object
		{
			action
		};

		const json::string set_tweak
		{
			object["set_tweak"]
		};

		if(set_tweak != "highlight")
			continue;

		const auto &value
		{
			object["value"]
		};

		// Spec sez: If a highlight tweak is given with no value, its value is
		// defined to be true. If no highlight tweak is given at all then the
		// value of highlight is defined to be false.
		return !value || value == "true";
	}

	return false;
}

//
// path
//

ircd::m::push::path
ircd::m::push::make_path(const event &event)
{
	return make_path(at<"type"_>(event), at<"state_key"_>(event));
}

ircd::m::push::path
ircd::m::push::make_path(const string_view &type,
                         const string_view &state_key)
{
	if(unlikely(!startswith(type, rule::type_prefix)))
		throw NOT_A_RULE
		{
			"Type '%s' does not start with prefix '%s'",
			type,
			rule::type_prefix,
		};

	const auto unprefixed
	{
		lstrip(lstrip(type, rule::type_prefix), '.')
	};

	const auto &[scope, kind]
	{
		split(unprefixed, '.')
	};

	return path
	{
		scope, kind, state_key
	};
}

ircd::string_view
ircd::m::push::make_type(const mutable_buffer &buf,
                         const path &path)
{
	const auto &[scope, kind, ruleid]
	{
		path
	};

	if(!scope)
		return fmt::sprintf
		{
			buf, "%s.",
			rule::type_prefix,
		};

	else if(!kind)
		return fmt::sprintf
		{
			buf, "%s.%s.",
			rule::type_prefix,
			scope,
		};

	return fmt::sprintf
	{
		buf, "%s.%s.%s",
		rule::type_prefix,
		scope,
		kind,
	};
}

//
// default ruleset
//

decltype(ircd::m::push::rules::defaults)
ircd::m::push::rules::defaults{R"(
{
	"override":
	[
		{
			"rule_id": ".m.rule.master",
			"default": true,
			"enabled": false,
			"conditions": [],
			"actions":
			[
				"dont_notify"
			]
		},
		{
			"rule_id": ".m.rule.suppress_notices",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "event_match",
					"key": "content.msgtype",
					"pattern": "m.notice"
				}
			],
			"actions":
			[
				"dont_notify"
			]
		},
		{
			"rule_id": ".m.rule.invite_for_me",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"key": "type",
					"kind": "event_match",
					"pattern": "m.room.member"
				},
				{
					"key": "content.membership",
					"kind": "event_match",
					"pattern": "invite"
				},
				{
					"kind": "state_key_user_mxid"
				}
			],
			"actions":
			[
				"notify",
				{
					"set_tweak": "sound",
					"value": "default"
				},
				{
					"set_tweak": "highlight",
					"value": false
				}
			]
		},
		{
			"rule_id": ".m.rule.member_event",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"key": "type",
					"kind": "event_match",
					"pattern": "m.room.member"
				}
			],
			"actions":
			[
				"dont_notify"
			]
		},
		{
			"rule_id": ".m.rule.contains_display_name",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "contains_display_name"
				}
			],
			"actions":
			[
				"notify",
				{
					"set_tweak": "sound",
					"value": "default"
				},
				{
					"set_tweak": "highlight"
				}
			]
		},
		{
			"rule_id": ".m.rule.tombstone",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "event_match",
					"key": "type",
					"pattern": "m.room.tombstone"
				},
				{
					"kind": "event_match",
					"key": "state_key",
					"pattern": ""
				}
			],
			"actions":
			[
				"notify",
				{
					"set_tweak": "highlight",
					"value": true
				}
			]
		},
		{
			"rule_id": ".m.rule.roomnotif",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "event_match",
					"key": "content.body",
					"pattern": "@room"
				},
				{
					"kind": "sender_notification_permission",
					"key": "room"
				}
			],
			"actions":
			[
				"notify",
				{
					"set_tweak": "highlight",
					"value": true
				}
			]
		},
		{
			"rule_id": ".m.rule.reaction",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "event_match",
					"key": "type",
					"pattern": "m.reaction"
				}
			],
			"actions":
			[
				"dont_notify"
			]
		}
	],
	"content":
	[
		{
			"rule_id": ".m.rule.contains_user_name",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "contains_user_mxid"
				}
			],
			"actions":
			[
				"notify",
				{
					"set_tweak": "sound",
					"value": "default"
				},
				{
					"set_tweak": "highlight",
					"value": true
				}
			]
		}
	],
	"underride":
	[
		{
			"rule_id": ".m.rule.call",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"key": "type",
					"kind": "event_match",
					"pattern": "m.call.invite"
				}
			],
			"actions":
			[
				"notify",
				{
					"set_tweak": "sound",
					"value": "ring"
				},
				{
					"set_tweak": "highlight",
					"value": false
				}
			]
		},
		{
			"rule_id": ".m.rule.encrypted_room_one_to_one",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "event_match",
					"key": "type",
					"pattern": "m.room.encrypted"
				},
				{
					"kind": "room_member_count",
					"is": "2"
				}
			],
			"actions":
			[
				"notify",
				{
					"set_tweak": "sound",
					"value": "default"
				},
				{
					"set_tweak": "highlight",
					"value": false
				}
			]
		},
		{
			"rule_id": ".m.rule.room_one_to_one",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "event_match",
					"key": "type",
					"pattern": "m.room.message"
				},
				{
					"kind": "room_member_count",
					"is": "2"
				}
			],
			"actions":
			[
				"notify",
				{
					"set_tweak": "sound",
					"value": "default"
				},
				{
					"set_tweak": "highlight",
					"value": false
				}
			]
		},
		{
			"rule_id": ".m.rule.message",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "event_match",
					"key": "type",
					"pattern": "m.room.message"
				}
			],
			"actions":
			[
				"dont_notify",
				{
					"set_tweak": "highlight",
					"value": false
				}
			]
		},
		{
			"rule_id": ".m.rule.encrypted",
			"default": true,
			"enabled": true,
			"conditions":
			[
				{
					"kind": "event_match",
					"key": "type",
					"pattern": "m.room.encrypted"
				}
			],
			"actions":
			[
				"dont_notify",
				{
					"set_tweak": "highlight",
					"value": false
				}
			]
		}
	]
}
)"};
