"""Pluggy-based check registry and triage orchestration for NoiseGate."""

import pluggy

from noisegate.exceptions import NoiseGateError
from noisegate.models import Check, CheckResult, ChecksResult, Policy, Program, Report
from noisegate.policy import derive_verdict

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

# Built-in check ID constants
ASSET_IN_SCOPE = "asset_in_scope"
FINDING_INELIGIBLE = "finding_ineligible"
REQUIRED_SECTIONS_PRESENT = "required_sections_present"
REPRODUCTION_QUALITY = "reproduction_quality"
IMPACT_QUALITY = "impact_quality"

_BUILTIN_CHECK_IDS = frozenset({
    ASSET_IN_SCOPE,
    FINDING_INELIGIBLE,
    REQUIRED_SECTIONS_PRESENT,
    REPRODUCTION_QUALITY,
    IMPACT_QUALITY,
})


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 BuiltinChecks:
    """LLM-backed implementations of the 5 built-in checks.

    All builtin checks for a given (report, program) pair are evaluated in a
    single LLM call and cached, so the pluggy loop does not incur one API call
    per check.
    """

    def __init__(self, model: str | None = None) -> None:
        self._model = model
        # (report_text, program_json) → {check_id: CheckResult}
        self._cache: dict[tuple[str, str], dict[str, CheckResult]] = {}
        # full ChecksResult from the batch LLM call — used for summary + token counts
        self._meta: dict[tuple[str, str], ChecksResult] = {}

    def _get_model(self) -> str:
        from noisegate.llm import get_default_model
        return self._model or get_default_model()

    @hookimpl
    def run_check(
        self,
        check: Check,
        report: Report,
        program: Program,
    ) -> CheckResult | None:
        if check.id not in _BUILTIN_CHECK_IDS:
            return None

        cache_key = (report.raw_text, program.model_dump_json())
        if cache_key not in self._cache:
            from noisegate.llm import run_checks
            output = run_checks(
                [Check(id=cid) for cid in _BUILTIN_CHECK_IDS],
                report,
                program,
                self._get_model(),
            )
            self._cache[cache_key] = {r.check_id: r for r in output.checks}
            self._meta[cache_key] = output

        return self._cache[cache_key].get(check.id)

    def get_meta(self, report: Report, program: Program) -> ChecksResult | None:
        """Return the full ChecksResult from the batch LLM call, or None if no builtins ran."""
        return self._meta.get((report.raw_text, program.model_dump_json()))


def create_plugin_manager(model: str | None = None) -> tuple[pluggy.PluginManager, BuiltinChecks]:
    """Create a plugin manager with built-in checks pre-registered.

    Returns a (plugin_manager, builtin_checks) tuple. The BuiltinChecks instance
    is exposed so callers can retrieve LLM metadata (summary, token counts) after
    the checks have run.
    """
    pm = pluggy.PluginManager(PROJECT_NAME)
    pm.add_hookspecs(CheckSpec)
    builtin = BuiltinChecks(model=model)
    pm.register(builtin)
    # Load third-party checks registered via entry point group "noisegate"
    pm.load_setuptools_entrypoints(PROJECT_NAME)
    return pm, builtin


def run_triage(
    checks: list[Check],
    report: Report,
    program: Program,
    model: str,
) -> ChecksResult:
    """Evaluate a policy's checks against a report via the plugin manager.

    Dispatches each check to the registered plugin that handles its ID. Built-in
    checks are evaluated in a single batched LLM call. Third-party checks registered
    via the ``noisegate`` entry point group are called for check IDs they handle.
    """
    pm, builtin = create_plugin_manager(model=model)

    results: list[CheckResult] = []
    for check in checks:
        result = pm.hook.run_check(check=check, report=report, program=program)
        if result is None:
            raise NoiseGateError(
                f"No handler found for check '{check.id}'. "
                "Ensure a plugin is installed that handles this check ID."
            )
        results.append(result)

    meta = builtin.get_meta(report, program)
    return ChecksResult(
        checks=results,
        summary=meta.summary if meta else "",
        verdict=derive_verdict(results, Policy(checks=checks)),
        model_used=meta.model_used if meta else model,
        prompt_tokens=meta.prompt_tokens if meta else 0,
        completion_tokens=meta.completion_tokens if meta else 0,
    )
