"""Pluggy-based check plugin framework for NoiseGate."""

from collections.abc import Callable
from typing import Any

import pluggy

from noisegate.models import Check, CheckResult, Program, Report

PROJECT_NAME = "noisegate"
hookspec = pluggy.HookspecMarker(PROJECT_NAME)
hookimpl = pluggy.HookimplMarker(PROJECT_NAME)

_HANDLES_ATTR = "_noisegate_handles"


def handles(check_id: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """Decorator that marks a method as the handler for a specific check ID."""
    def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
        setattr(fn, _HANDLES_ATTR, {"check_id": check_id})
        return fn
    return decorator


class CheckSpec:
    @hookspec(firstresult=True)
    def run_check(
        self, check: Check, report: Report, program: Program,
    ) -> CheckResult | None:
        """Run a single check. Return CheckResult if handled, None to pass to next plugin."""


class CheckPlugin:
    """Base class for check plugins. Subclass this and decorate methods with @handles."""

    def __init__(self) -> None:
        self._dispatch: dict[str, Callable[..., CheckResult | None]] = {}
        for klass in type(self).__mro__:
            for name, fn in vars(klass).items():
                meta = getattr(fn, _HANDLES_ATTR, None)
                if meta is not None and meta["check_id"] not in self._dispatch:
                    self._dispatch[meta["check_id"]] = getattr(self, name)

    @hookimpl
    def run_check(self, check: Check, report: Report, program: Program) -> CheckResult | None:
        method = self._dispatch.get(check.id)
        if method is None:
            return None
        return method(check, report, program)


def create_plugin_manager(*plugins: CheckPlugin) -> pluggy.PluginManager:
    """Create a plugin manager with the given plugins registered."""
    pm = pluggy.PluginManager(PROJECT_NAME)
    pm.add_hookspecs(CheckSpec)
    for plugin in plugins:
        pm.register(plugin)
    pm.load_setuptools_entrypoints(PROJECT_NAME)
    return pm
