from __future__ import annotations

import contextlib
import importlib.metadata
import logging
import threading
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Generic, TypeVar

from pygitweb.api.action import Action
from pygitweb.api.plugin import Plugin
from pygitweb.api.subpage import Subpage
from pygitweb.config import ACTIONS as BUILTIN_ACTIONS

logger = logging.getLogger(__name__)

SUBPAGE_GROUP = "pygitweb.subpages"
ACTION_GROUP = "pygitweb.actions"

_local = threading.local()

T = TypeVar("T", bound=Plugin)


def _plugin_id_stack() -> list[str]:
	stack: list[str] | None = getattr(_local, "plugin_id_stack", None)
	if stack is None:
		stack = []
		_local.plugin_id_stack = stack
	return stack


def current_plugin_id() -> str | None:
	stack = _plugin_id_stack()
	return stack[-1] if stack else None


class _PluginLogFilter(logging.Filter):
	def filter(self, record: logging.LogRecord) -> bool:
		plugin_id = current_plugin_id()
		if plugin_id is not None:
			record.msg = f"{plugin_id}: {record.msg}"
		return True


def _install_plugin_log_filter() -> None:
	logging.getLogger("pygitweb").addFilter(_PluginLogFilter())


_install_plugin_log_filter()


@contextlib.contextmanager
def plugin_load_context(plugin_id: str):
	stack = _plugin_id_stack()
	stack.append(plugin_id)
	try:
		yield
	finally:
		if stack and stack[-1] == plugin_id:
			stack.pop()
		elif plugin_id in stack:
			stack.remove(plugin_id)


def _entry_points(group: str) -> Iterable[importlib.metadata.EntryPoint]:
	return importlib.metadata.entry_points().select(group=group)


@dataclass
class PluginHandle(Generic[T]):
	plugin_id: str
	_stack: contextlib.ExitStack
	instance: T

	def unload(self) -> None:
		self._stack.close()


def _load_handle(
	ep: importlib.metadata.EntryPoint,
	expected_type: type[T],
) -> PluginHandle[T] | None:
	stack = contextlib.ExitStack()
	try:
		stack.enter_context(plugin_load_context(ep.name))
		loaded = ep.load()
	except Exception:
		stack.close()
		logger.exception("pygitweb: failed to load entry point %r", ep.name)
		return None
	if not isinstance(loaded, type) or not issubclass(loaded, expected_type):
		stack.close()
		logger.error(
			"pygitweb: entry point %r must be a %s subclass, got %r",
			ep.name,
			expected_type.__name__,
			loaded,
		)
		return None
	try:
		instance = stack.enter_context(loaded())
	except Exception:
		stack.close()
		logger.exception("pygitweb: failed to enter plugin context for %r", ep.name)
		return None
	return PluginHandle(plugin_id=ep.name, _stack=stack, instance=instance)


class PluginRegistry:
	def __init__(self) -> None:
		self._lock = threading.Lock()
		self._action_handles: dict[str, PluginHandle[Action]] = {}
		self._subpage_handles: list[PluginHandle[Subpage]] = []

	def load_all(self) -> None:
		with self._lock:
			self._unload_locked()
			self._load_actions_locked()
			self._load_subpages_locked()

	def unload_all(self) -> None:
		with self._lock:
			self._unload_locked()

	def _unload_locked(self) -> None:
		for handle in self._action_handles.values():
			handle.unload()
		for handle in self._subpage_handles:
			handle.unload()
		self._action_handles.clear()
		self._subpage_handles.clear()

	def _load_actions_locked(self) -> None:
		for ep in _entry_points(ACTION_GROUP):
			handle = _load_handle(ep, Action)
			if handle is None:
				continue
			name = handle.instance.action_name()
			if name in BUILTIN_ACTIONS:
				logger.warning(
					"pygitweb: plugin action %r conflicts with built-in; skipping entry %r",
					name,
					ep.name,
				)
				handle.unload()
				continue
			if name in self._action_handles:
				logger.warning("pygitweb: duplicate plugin action %r; skipping entry %r", name, ep.name)
				handle.unload()
				continue
			self._action_handles[name] = handle

	def _load_subpages_locked(self) -> None:
		seen_names: set[str] = set()
		for ep in _entry_points(SUBPAGE_GROUP):
			handle = _load_handle(ep, Subpage)
			if handle is None:
				continue
			key = handle.instance.subpage_name()
			if key in seen_names:
				logger.warning("pygitweb: duplicate subpage %r from %r; skipping", key, ep.name)
				handle.unload()
				continue
			seen_names.add(key)
			self._subpage_handles.append(handle)

	@property
	def actions(self) -> dict[str, Action]:
		return {name: handle.instance for name, handle in self._action_handles.items()}

	@property
	def subpages(self) -> list[Subpage]:
		return [handle.instance for handle in self._subpage_handles]

	def dispatch_actions(self) -> frozenset[str]:
		return frozenset(BUILTIN_ACTIONS) | frozenset(self._action_handles.keys())