from nawah.enums import NAWAH_VALUES
from nawah.config import Config

from bson import ObjectId
from typing import (
	Dict,
	Union,
	Type,
	List,
	Sequence,
	Any,
	Literal,
	Callable,
	Optional,
	ForwardRef,
	TYPE_CHECKING,
	Protocol,
)

import datetime, logging, re, inspect

from ._package import SYS_DOC
from ._exceptions import (
	InvalidAttrTypeException,
	InvalidAttrTypeArgException,
	InvalidAttrTypeArgsException,
)

if TYPE_CHECKING:
	from ._module import EXTN
	from ._types import NAWAH_EVENTS, NAWAH_ENV, NAWAH_QUERY, NAWAH_DOC, NAWAH_DOC
	from ._query import Query

logger = logging.getLogger('nawah')

ATTRS_TYPES_ARGS: Dict[str, Dict[str, Union[Type, str]]] = {
	'ANY': {},
	'ACCESS': {},
	'COUNTER': {'pattern': str},
	'ID': {},
	'STR': {'pattern': str},
	'INT': {'ranges': List[List[int]]},
	'FLOAT': {'ranges': List[List[float]]},
	'BOOL': {},
	'LOCALE': {},
	'LOCALES': {},
	'EMAIL': {
		'allowed_domains': List[str],
		'disallowed_domains': List[str],
		'strict_matching': bool,
	},
	'PHONE': {'codes': List[str]},
	'IP': {},
	'URI_WEB': {
		'allowed_domains': List[str],
		'disallowed_domains': List[str],
		'strict_matching': bool,
	},
	'DATETIME': {'ranges': List[List[datetime.datetime]]},
	'DATE': {'ranges': List[List[datetime.date]]},
	'DYNAMIC_ATTR': {'types': List[str]},
	'DYNAMIC_VAL': {'dynamic_attr': str},
	'TIME': {'ranges': List[List[datetime.time]]},
	'FILE': {
		'types': List[str],
		'max_ratio': List[int],
		'min_ratio': List[int],
		'max_dims': List[int],
		'min_dims': List[int],
		'max_size': int,
	},
	'GEO': {},
	'LIST': {'list': List['ATTR'], 'min': int, 'max': int},
	'KV_DICT': {
		'key': 'ATTR',
		'val': 'ATTR',
		'min': int,
		'max': int,
		'req': List[str],
	},
	'TYPED_DICT': {'dict': Dict[str, 'ATTR']},
	'LITERAL': {'literal': List[Union[str, int, float, bool]]},
	'UNION': {'union': List['ATTR']},
	'TYPE': {'type': str},
}

SPECIAL_ATTRS = Literal[
	'$search', '$sort', '$skip', '$limit', '$extn', '$soft', '$attrs', '$group', '$geo_near'
]


class ATTR_TYPE_CALLABLE_TYPE(Protocol):
	def __call__(
		*,
		self,
		mode: Literal['create', 'create_draft', 'update'],
		attr_name: str,
		attr_type: 'ATTR',
		attr_val: Any,
		skip_events: 'NAWAH_EVENTS',
		env: 'NAWAH_ENV',
		query: Union['NAWAH_QUERY', 'Query'],
		doc: 'NAWAH_DOC',
		scope: Optional['NAWAH_DOC'],
	) -> Any:
		...


ATTRS_TYPES_TYPE = Literal[
	'ANY',
	'ACCESS',
	'COUNTER',
	'ID',
	'STR',
	'INT',
	'FLOAT',
	'BOOL',
	'LOCALE',
	'LOCALES',
	'EMAIL',
	'PHONE',
	'IP',
	'URI_WEB',
	'DATETIME',
	'DATE',
	'DYNAMIC_ATTR',
	'DYNAMIC_VAL',
	'TIME',
	'FILE',
	'GEO',
	'LIST',
	'KV_DICT',
	'TYPED_DICT',
	'LITERAL',
	'UNION',
	'TYPE',
]


class ATTR:
	_nawah_attr: bool = True
	_type: ATTRS_TYPES_TYPE
	_desc: Optional[str]
	_args: Dict[str, Any]
	_valid: bool = False
	_extn: Optional[Union['EXTN', 'ATTR']] = None

	__default = NAWAH_VALUES.NONE_VALUE

	@property
	def _default(self):
		if self.__default == '$__datetime':
			return datetime.datetime.utcnow().isoformat()
		elif self.__default == '$__date':
			return datetime.date.today().isoformat()
		elif self.__default == '$__time':
			return datetime.datetime.now().time().isoformat()
		else:
			return self.__default

	@_default.setter
	def _default(self, value):
		self.__default = value

	def __repr__(self):
		return f'<ATTR:{self._type},{self._args}>'

	def __init__(self, *, attr_type: ATTRS_TYPES_TYPE, desc: str = None, **kwargs: Any):
		self._type = attr_type
		self._desc = desc
		self._args = kwargs
		ATTR.validate_type(attr_type=self, skip_type=True)

	@classmethod
	def ANY(cls, *, desc: str = None):
		return ATTR(attr_type='ANY', desc=desc)

	@classmethod
	def ACCESS(cls, *, desc: str = None):
		return ATTR(attr_type='ACCESS', desc=desc)

	@classmethod
	def COUNTER(cls, *, desc: str = None, pattern: str, values: List[Callable] = None):
		return ATTR(attr_type='COUNTER', desc=desc, pattern=pattern, values=values)

	@classmethod
	def ID(cls, *, desc: str = None):
		return ATTR(attr_type='ID', desc=desc)

	@classmethod
	def STR(cls, *, desc: str = None, pattern: str = None):
		return ATTR(attr_type='STR', desc=desc, pattern=pattern)

	@classmethod
	def INT(cls, *, desc: str = None, ranges: List[List[int]] = None):
		return ATTR(attr_type='INT', desc=desc, ranges=ranges)

	@classmethod
	def FLOAT(cls, *, desc: str = None, ranges: List[List[int]] = None):
		return ATTR(attr_type='FLOAT', desc=desc, ranges=ranges)

	@classmethod
	def BOOL(cls, *, desc: str = None):
		return ATTR(attr_type='BOOL', desc=desc)

	@classmethod
	def LOCALE(cls, *, desc: str = None):
		return ATTR(attr_type='LOCALE', desc=desc)

	@classmethod
	def LOCALES(cls, *, desc: str = None):
		return ATTR(attr_type='LOCALES', desc=desc)

	@classmethod
	def EMAIL(
		cls,
		*,
		desc: str = None,
		allowed_domains: List[str] = None,
		disallowed_domains: List[str] = None,
		strict: bool = False,
	):
		return ATTR(
			attr_type='EMAIL',
			desc=desc,
			allowed_domains=allowed_domains,
			disallowed_domains=disallowed_domains,
			strict=strict,
		)

	@classmethod
	def PHONE(cls, *, desc: str = None, codes: List[str] = None):
		return ATTR(attr_type='PHONE', desc=desc, codes=codes)

	@classmethod
	def IP(cls, *, desc: str = None):
		return ATTR(attr_type='IP', desc=desc)

	@classmethod
	def URI_WEB(
		cls,
		*,
		desc: str = None,
		allowed_domains: List[str] = None,
		disallowed_domains: List[str] = None,
		strict: bool = False,
	):
		return ATTR(
			attr_type='URI_WEB',
			desc=desc,
			allowed_domains=allowed_domains,
			disallowed_domains=disallowed_domains,
			strict=strict,
		)

	@classmethod
	def DATETIME(cls, *, desc: str = None, ranges: List[List[str]] = None):
		return ATTR(attr_type='DATETIME', desc=desc, ranges=ranges)

	@classmethod
	def DATE(cls, *, desc: str = None, ranges: List[List[str]] = None):
		return ATTR(attr_type='DATE', desc=desc, ranges=ranges)

	@classmethod
	def DYNAMIC_ATTR(cls, *, desc: str = None, types: List[str] = None):
		return ATTR(attr_type='DYNAMIC_ATTR', desc=desc, types=types)

	@classmethod
	def DYNAMIC_VAL(cls, *, desc: str = None, dynamic_attr: str):
		return ATTR(attr_type='DYNAMIC_VAL', desc=desc, dynamic_attr=dynamic_attr)

	@classmethod
	def TIME(cls, *, desc: str = None, ranges: List[List[str]] = None):
		return ATTR(attr_type='TIME', desc=desc, ranges=ranges)

	@classmethod
	def FILE(
		cls,
		*,
		desc: str = None,
		types: List[str] = None,
		max_ratio: List[int] = None,
		min_ratio: List[int] = None,
		max_dims: List[int] = None,
		min_dims: List[int] = None,
		max_size: int = None,
	):
		return ATTR(
			attr_type='FILE',
			desc=desc,
			types=types,
			max_ratio=max_ratio,
			min_ratio=min_ratio,
			max_dims=max_dims,
			min_dims=min_dims,
			max_size=max_size,
		)

	@classmethod
	def GEO(cls, *, desc: str = None):
		return ATTR(attr_type='GEO', desc=desc)

	@classmethod
	def LIST(
		cls,
		*,
		desc: str = None,
		list: List['ATTR'],
		min: int = None,
		max: int = None,
	):
		return ATTR(attr_type='LIST', desc=desc, list=list, min=min, max=max)

	@classmethod
	def KV_DICT(
		cls,
		*,
		desc: str = None,
		key: 'ATTR',
		val: 'ATTR',
		min: int = None,
		max: int = None,
		req: List[str] = None,
	):
		return ATTR(
			attr_type='KV_DICT', desc=desc, key=key, val=val, min=min, max=max, req=req
		)

	@classmethod
	def TYPED_DICT(cls, *, desc: str = None, dict: Dict[str, 'ATTR']):
		return ATTR(attr_type='TYPED_DICT', desc=desc, dict=dict)

	@classmethod
	def LITERAL(cls, *, desc: str = None, literal: Sequence[Union[str, int, float, bool]]):
		return ATTR(attr_type='LITERAL', desc=desc, literal=literal)

	@classmethod
	def UNION(cls, *, desc: str = None, union: List['ATTR']):
		return ATTR(attr_type='UNION', desc=desc, union=union)

	@classmethod
	def TYPE(cls, *, desc: str = None, type: Union[str, ATTR_TYPE_CALLABLE_TYPE]):
		return ATTR(attr_type='TYPE', desc=desc, type=type)

	@classmethod
	def validate_type(cls, *, attr_type: 'ATTR', skip_type: bool = False):
		# [DOC] Skip validating Attr Type if it is already validated, unless we want to validate TYPE, which could require deep (nested) validation
		if skip_type and attr_type._valid:
			return

		if attr_type._type not in ATTRS_TYPES_ARGS.keys():
			raise InvalidAttrTypeException(attr_type=attr_type)
		elif not skip_type and attr_type._type == 'TYPE':
			# [DOC] Complex condition is unreadblae in single condition set, break into multiple conditions as try..except block
			try:
				if type(attr_type._args['type']) == str:
					if attr_type._args['type'] not in Config.types.keys():
						raise Exception()
					if not inspect.iscoroutinefunction(Config.types[attr_type._args['type']]):
						raise Exception()
					# [DOC] Assign new Attr Type Arg for shorthand calling the TYPE function
					attr_type._args['func'] = Config.types[attr_type._args['type']]
				else:
					if not inspect.iscoroutinefunction(attr_type._args['type']):
						raise Exception()
					# [DOC] Assign new Attr Type Arg for shorthand calling the TYPE function
					attr_type._args['func'] = attr_type._args['type']
			except:
				raise InvalidAttrTypeException(attr_type=attr_type)
		elif attr_type._type != 'TYPE':
			for arg in ATTRS_TYPES_ARGS[attr_type._type].keys():
				if (
					arg in ['list', 'dict', 'literal', 'union', 'type']
					and arg not in attr_type._args.keys()
				):
					raise InvalidAttrTypeArgException(
						arg_name=arg,
						arg_type=ATTRS_TYPES_ARGS[attr_type._type][arg],
						arg_val=attr_type._args[arg],
					)
				elif arg in attr_type._args.keys() and attr_type._args[arg] != None:
					cls.validate_arg(
						arg_name=arg,
						arg_type=ATTRS_TYPES_ARGS[attr_type._type][arg],
						arg_val=attr_type._args[arg],
						skip_type=skip_type,
					)
				else:
					attr_type._args[arg] = None
			if attr_type._type == 'COUNTER':
				if (
					'$__values.' in attr_type._args['pattern']
					or '$__counters:' in attr_type._args['pattern']
				):
					logger.error(
						'Attr Type COUNTER is using wrong format for \'$__values\', or \'$__counters\'.'
					)
					raise InvalidAttrTypeException(attr_type=attr_type)
				counter_groups = re.findall(
					r'(\$__(?:values:[0-9]+|counters\.[a-z0-9_]+))',
					attr_type._args['pattern'],
				)
				if len(counter_groups) == 0:
					logger.error(
						'Attr Type COUNTER is not having any \'$__values\', or \'$__counters\'.'
					)
					raise InvalidAttrTypeException(attr_type=attr_type)
				if '$__counters.' not in attr_type._args['pattern']:
					logger.warning('Attr Type COUNTER is defined with not \'$__counters\'.')
				for group in counter_groups:
					if group.startswith('$__counters.'):
						Config.docs.append(
							SYS_DOC(
								module='setting',
								key='var',
								doc={
									'user': ObjectId('f00000000000000000000010'),
									'var': '__counter:' + group.replace('$__counters.', ''),
									'val_type': {'type': 'INT', 'args': {}, 'allow_none': False, 'default': None},
									'val': 0,
									'type': 'global',
								},
							)
						)
			attr_type._valid = True

	@classmethod
	def validate_arg(cls, *, arg_name: str, arg_type: Any, arg_val: Any, skip_type: bool):
		if arg_type == str:
			if type(arg_val) != str:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			return
		elif arg_type == int:
			if type(arg_val) != int:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			return
		elif arg_type == float:
			if type(arg_val) not in [float, int]:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			return
		elif arg_type == bool:
			if type(arg_val) != bool:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			return
		elif type(arg_type) == ForwardRef or (type(arg_type) == str and arg_type == 'ATTR'):
			if type(arg_val) != ATTR:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			if arg_name == 'key':
				if arg_val._type not in ['STR', 'LITERAL']:
					raise InvalidAttrTypeArgException(
						arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
					)
			return
		elif arg_name == 'literal':
			if type(arg_val) != list:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			for arg_val_child in arg_val:
				if type(arg_val_child) not in arg_type.__args__[0].__args__:
					raise InvalidAttrTypeArgException(
						arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
					)
			return
		elif arg_name == 'union':
			if type(arg_val) != list:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			for arg_val_child in arg_val:
				if type(arg_val_child) != ATTR:
					raise InvalidAttrTypeArgException(
						arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
					)
				ATTR.validate_type(attr_type=arg_val_child, skip_type=skip_type)
			return
		elif arg_type == datetime.date:
			if not re.match(r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$', arg_val) and not re.match(
				r'^[\-\+][0-9]+[dsmhw]$', arg_val
			):
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			return
		elif arg_type == datetime.datetime:
			if (
				not re.match(
					r'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\.[0-9]{6})?)?$',
					arg_val,
				)
				and not re.match(r'^[\-\+][0-9]+[dsmhw]$', arg_val)
			):
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			return
		elif arg_type == datetime.time:
			if not re.match(
				r'^[0-9]{2}:[0-9]{2}(:[0-9]{2}(\.[0-9]{6})?)?$', arg_val
			) and not re.match(r'^[\-\+][0-9]+[dsmhw]$', arg_val):
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			return
		elif arg_type._name == 'List':
			if type(arg_val) != list:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			for arg_val_child in arg_val:
				cls.validate_arg(
					arg_name=arg_name,
					arg_type=arg_type.__args__[0],
					arg_val=arg_val_child,
					skip_type=skip_type,
				)

				# [DOC] In Addition to validating as list arg, check for ATTR and validate
				if type(arg_val_child) == ATTR:
					ATTR.validate_type(attr_type=arg_val_child, skip_type=skip_type)

			return
		elif arg_type._name == 'Dict':
			if type(arg_val) != dict:
				raise InvalidAttrTypeArgException(
					arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
				)
			for arg_val_child in arg_val.keys():
				if type(arg_val[arg_val_child]) != ATTR:
					raise InvalidAttrTypeArgException(
						arg_name=arg_name, arg_type=arg_type, arg_val=arg_val[arg_val_child]
					)
				ATTR.validate_type(attr_type=arg_val[arg_val_child], skip_type=skip_type)
			return

		raise InvalidAttrTypeArgException(
			arg_name=arg_name, arg_type=arg_type, arg_val=arg_val
		)
