import asyncio
import sys
from importlib.util import spec_from_file_location, module_from_spec
from pathlib import Path
from watchfiles import awatch, Change

from aiogram import Router

from ..utils.logging import loggers
	
def extract_router(file_path: str | Path) -> Router:
	"""
	Extract `router` from file at `file_path`.

	:param file_path: Path to Python file

	:rtype: Router
	:return: Module's `router` attribute

	:raises ModuleNotFoundError: If `spec_from_file_location` fails
	:raises TypeError: If `router` isn't an instance of `Router`
	"""
	
	file_path = Path(file_path)

	spec = spec_from_file_location(file_path.stem, file_path)

	if spec is None or spec.loader is None:
		raise ModuleNotFoundError(f"Failed to create spec from file location {str(file_path)!r}")

	module = module_from_spec(spec)
	module.__name__ = spec.name
	module.__file__ = str(file_path)

	sys.modules[spec.name] = module
	
	spec.loader.exec_module(module)

	router = getattr(module, "router", None)

	if not isinstance(router, Router):
		raise TypeError(f"Expected Router, got {type(router).__name__}")

	return router

def unload_router(router: Router) -> Router:
	"""
	Remove `router` from it's `parent_router`'s sub-routers.

	:param router: Router to unload

	:rtype: Router
	:return: The unloaded router
	"""

	if router.parent_router is not None:
		router.parent_router.sub_routers = [
			sub_router
			for sub_router in router.parent_router.sub_routers
			if sub_router.name != router.name
		]

	return router

def load_router(file_path: str | Path, parent_router: Router) -> Router:
	"""
	Extract router from `file_path` and attach it to `parent_router`.

	:param file_path: File path to extract router from
	:param parent_router: Router to parent to

	:rtype: Router
	:return: The loaded router
	"""
	
	router = extract_router(file_path)
	parent_router.include_router(router)

	return router

def reload_router(router: Router, file_path: str | Path) -> Router:
	"""
	Reload router by first unloading it, then loading a new one from `file_path`.

	:param router: Router to reload
	:param file_path: File path to extract router from

	:rtype: Router
	:return: The reloaded router
	"""

	unload_router(router)
	return load_router(file_path, router.parent_router)

def handle_router_naming(router: Router, routers: dict[Path, Router]):
	"""
	Renames `router` if one of `routers` has the same name.

	:param router: Router to handle naming of
	"""

	if getattr(router, "name", None) is None:
		return
	
	if router.name in [loaded_router.name for loaded_router in routers.values()]:
		old_name = router.name
		router.name = f"{old_name}_{hex(id(router))}"

		loggers.routers.warning(f"Renamed {old_name!r} to {router.name!r} due to name conflict")

async def awatch_routers(directory: str | Path, parent_router: Router, routers: dict[Path, Router]):
	"""
	Asynchronously watch files in `directory` and reload routers on change.

	:param directory: Path to routers directory
	:param parent_router: Router to attach routers from newly added files to
	:param routers: Dictionary with file paths mapped to already loaded routers
	"""

	async for changes in awatch(directory):
		for change_type, changed_path in changes:
			changed_path = Path(changed_path).resolve()

			router = routers.get(changed_path)

			if change_type == Change.modified:
				routers.pop(changed_path)
				
				try:
					router = reload_router(router, changed_path)
				except: # noqa E722
					if getattr(router, "name", None) is None:
						loggers.routers.error(f"Exception while reloading router from file {str(changed_path)!r}", exc_info=True)
					else:
						loggers.routers.error(f"Exception while reloading router {router.name!r}", exc_info=True)
				else:
					loggers.routers.info(f"Reloaded router {router.name!r}")
				finally:
					handle_router_naming(router, routers)

					routers[changed_path] = router

			elif change_type == Change.added:
				try:
					router = load_router(changed_path, parent_router)
				except: # noqa E722
					if getattr(router, "name", None) is None:
						loggers.routers.error(f"Exception while loading router from file {str(changed_path)!r}", exc_info=True)
					else:
						loggers.routers.error(f"Exception while loading router {router.name!r}", exc_info=True)
				else:
					loggers.routers.info(f"Loaded router {router.name!r}")
				finally:
					handle_router_naming(router, routers)

					routers[changed_path] = router
			elif change_type == Change.deleted:
				unload_router(router)
				routers.pop(changed_path)

				loggers.routers.info(f"Unloaded router {router.name!r}")

def include_routers(parent_router: Router, directory: str | Path, hot_reload: bool=False):
	"""
	Include routers from directory with optional hot-reloading

	:param router: Router (or Dispatcher) to attach routers to
	:param directory: Path to routers directory
	:param hot_reload: Whether to hot reload routers on changes
	"""

	directory = Path(directory).resolve()

	routers = dict()

	for file_path in directory.iterdir():
		if file_path.suffix == ".py":
			router = extract_router(file_path)

			if router.name is None:
				router.name = file_path.stem

			routers[file_path] = load_router(file_path, parent_router)

	if hot_reload:
		asyncio.create_task(awatch_routers(directory, parent_router, routers))

__all__ = ["extract_router", "unload_router", "load_router", "reload_router", "handle_router_naming", "awatch_routers", "include_routers"]