from twisted.plugin import IPlugin
from txircd.config import ConfigValidationError
from txircd.module_interface import IModuleData, ModuleData
from txircd.utils import durationToSeconds, now
from zope.interface import implementer
from typing import Any, Callable, Dict, List, Tuple, TYPE_CHECKING
import re

if TYPE_CHECKING:
	from txircd.user import IRCUser

def applyFilter(message: str, filter: re.Pattern, filterDefinition: Dict[str, Any]) -> bool:
	if filterDefinition["caseinsensitive"]:
		filterMatch = filter.search(message, re.IGNORECASE)
	else:
		filterMatch = filter.search(message)
	if filterMatch:
		return True
	return False

def createXLineMask(user: IRCUser, maskDefinition: str) -> str:
	maskFormat = {
		"nick": user.nick,
		"user": user.ident,
		"host": user.host,
		"ip": user.ip
	}
	return maskDefinition.format(**maskFormat)

@implementer(IPlugin, IModuleData)
class Filter(ModuleData):
	name = "Filter"
	filters = None
	
	def actions(self) -> List[Tuple[str, int, Callable]]:
		return [
			("commandmodify-PRIVMSG", 1, self.checkMessage),
			("commandmodify-NOTICE", 1, self.checkMessage),
			("servernoticetype", 1, self.checkSnoType)
		]

	def load(self) -> None:
		self.filters = self.parseConfig(self.ircd.config)
	
	def rehash(self) -> None:
		self.filters = self.parseConfig(self.ircd.config)
	
	def verifyConfig(self, config: Dict[str, Any]) -> None:
		if "filters" in config:
			if not isinstance(config["filters"], dict):
				raise ConfigValidationError("filters", "filters must be a dict")
			for filterPattern, filterData in config["filters"].items():
				if not isinstance(filterPattern, str):
					raise ConfigValidationError("filters", "Filter pattern must be a regex string")
				if "action" not in filterData:
					raise ConfigValidationError("filters", "Missing action for filter")
				if filterData["action"] not in ("block", "block-notify", "notify", "kill", "xline"):
					action = filterData["action"]
					raise ConfigValidationError("filters", f"Invalid action for filter ({action}); must be one of \"block\", \"block-notify\", \"kill\", \"xline\"")
				if filterData["action"] == "xline" and "type" not in filterData:
					raise ConfigValidationError("filters", f"An x:line type must be specified for filters with the xline action")
				if filterData["action"] == "xline" and "mask" not in filterData:
					raise ConfigValidationError("filters", "A mask format must be specified for xline filters")
				if filterData["action"] == "xline" and "duration" not in filterData:
					filterData["duration"] = "0"
				if (filterData["action"] == "kill" or filterData["action"] == "xline") and "message" not in filterData:
					raise ConfigValidationError("filters", "Kill and xline filters must have a message")
				if "caseinsensitive" not in filterData:
					filterData["caseinsensitive"] = False
	
	def parseConfig(self, config: Dict[str, Any]) -> Dict[re.Pattern, Dict[str, Any]]:
		if "filters" not in config:
			return {}
		filters = {}
		for filterPattern, filterData in config["filters"].items():
			pattern = None
			try:
				pattern = re.compile(filterPattern)
			except re.error as error:
				self.ircd.log.error("Failed to load filter: invalid regex pattern \"{pattern}\" ({error})", pattern=filterPattern, error=error)
				continue
			if "duration" in filterData:
				filterData["duration"] = durationToSeconds(filterData["duration"])
			filters[pattern] = filterData
		return filters
	
	def checkSnoType(self, user: IRCUser, typename: str) -> bool:
		return typename == "filter"

	def checkMessage(self, user: IRCUser, data: Dict[Any, Any]) -> None:
		if self.ircd.runActionUntilValue("userhasoperpermission", user, "filter-override", users=[user]):
			return None
		stopChannels = []
		stopUsers = []
		addedXLines = {}
		
		if "targetchans" in data:
			for channel, message in data["targetchans"].items():
				for filter, filterData in self.filters.items():
					if applyFilter(message, filter, filterData):
						action = filterData["action"]
						if action == "block":
							stopChannels.append(channel)
						elif action == "block-notify":
							stopChannels.append(channel)
							self.ircd.runActionStandard("sendservernotice", "filter", f"Blocked message from user {user.nick} to channel {channel.name} which matched pattern {filter.pattern}: {message}")
						elif action == "notify":
							self.ircd.runActionStandard("sendservernotice", "filter", f"Message sent from user {user.nick} to channel {channel.name} which matched pattern {filter.pattern}: {message}")
						elif action == "kill":
							killMessage = filterData["message"]
							user.disconnect(f"Killed ({killMessage})")
						elif action == "xline":
							mask = createXLineMask(user, filterData["mask"])
							if filterData["type"] not in addedXLines:
								addedXLines[filterData["type"]] = set()
							if mask not in addedXLines[filterData["type"]]:
								self.ircd.runActionUntilTrue("addxline", filterData["type"], mask, now(), filterData["duration"], self.ircd.name, filterData["message"])
								addedXLines[filterData["type"]].add(mask)
						break
		
		if "targetusers" in data:
			for targetUser, message in data["targetusers"].items():
				for filter, filterData in self.filters.items():
					if applyFilter(message, filter, filterData):
						action = filterData["action"]
						if action == "block":
							stopUsers.append(user)
						elif action == "block-notify":
							stopUsers.append(user)
							self.ircd.runActionStandard("sendservernotice", "filter", f"Blocked message from user {user.nick} to user {targetUser.nick} which matched pattern {filter.pattern}: {message}")
						elif action == "notify":
							self.ircd.runActionStandard("sendservernotice", "filter", f"Message sent from user {user.nick} to user {targetUser.nick} which matched pattern {filter.pattern}: {message}")
						elif action == "kill":
							killMessage = filterData["message"]
							user.disconnect(f"Killed ({killMessage})")
						elif action == "xline":
							mask = createXLineMask(user, filterData["mask"])
							if filterData["type"] not in addedXLines:
								addedXLines[filterData["type"]] = set()
							if mask not in addedXLines[filterData["type"]]:
								self.ircd.runActionUntilTrue("addxline", filterData["type"], mask, now(), filterData["duration"], self.ircd.name, filterData["message"])
								addedXLines[filterData["type"]].add(mask)
						break
		
		for channel in stopChannels:
			del data["targetchans"][channel]
		for targetUser in stopUsers:
			del data["targetusers"][targetUser]
		
		return None

filter = Filter()
@@ -305,60 +305,8 @@
 re%0A%0A
-if TYPE_CHECKING:%0A%09from txircd.user import IRCUser%0A%0A
 def 
@@ -607,23 +607,25 @@
 k(user: 
+%22
 IRCUser
+%22
 , maskDe
@@ -3412,23 +3412,25 @@
 , user: 
+%22
 IRCUser
+%22
 , typena
@@ -3508,23 +3508,25 @@
 , user: 
+%22
 IRCUser
+%22
 , data: 

Fix a shared misunderstanding of how Python's stupid type checking works