"""Pluggable AI-generated-text detection for submitted manuscripts.

Three selectable backends:

* ``local``     — desklib DeBERTa, offline, calibrated (default; needs download)
* ``llm-judge`` — reuses the configured LLM provider (no download, uncalibrated)
* ``api``       — Pangram / GPTZero (opt-in, needs key + privacy consent)

All detection flows through :func:`run_detection`, which always returns an
:class:`AIDetectionResult` (never raises) so a detection failure can never
break the surrounding citation check.  See :mod:`base` for the honesty policy
(banding, abstention, disclaimer).
"""

from __future__ import annotations

import logging
from typing import Optional

from .base import (
    AIDetectionResult,
    DetectionBackend,
    SuspectSpan,
    DISCLAIMER,
    MIN_WORDS,
    HIGH_THRESHOLD,
    MEDIUM_THRESHOLD,
    make_unavailable,
)
from . import model_manager

logger = logging.getLogger(__name__)

DEFAULT_BACKEND = "local"
VALID_BACKENDS = ("local", "llm-judge", "api")


def build_detector(backend: str, *, check_id=None, **opts) -> Optional[DetectionBackend]:
    """Construct a detection backend by name, or None if unknown.

    ``check_id`` is threaded to the backend so its token/cost usage can be
    attributed to the right check in the usage meter.
    """
    backend = (backend or DEFAULT_BACKEND).lower()
    if backend == "local":
        from .local_backend import LocalDetectorBackend
        return LocalDetectorBackend(check_id=check_id)
    if backend in ("llm-judge", "llm"):
        from .llm_backend import LLMJudgeBackend
        return LLMJudgeBackend(
            provider=opts.get("provider"),
            api_key=opts.get("api_key"),
            endpoint=opts.get("endpoint"),
            model=opts.get("model"),
            check_id=check_id,
        )
    if backend == "api":
        from .api_backend import ApiBackend
        return ApiBackend(
            service=opts.get("service", "pangram"),
            api_key=opts.get("api_key"),
            consent=opts.get("consent", False),
            check_id=check_id,
        )
    logger.warning("Unknown AI-detection backend: %s", backend)
    return None


def run_detectors(text_or_pages, keys):
    """Run one or more registered detectors and compare them side-by-side.

    Thin re-export of :func:`multi_run.run_detectors` so callers can use
    ``from refchecker.ai_detection import run_detectors``. Returns the
    per-detector results + the plain-code comparison summary (no synthetic
    ensemble). An uninstalled detector abstains — it never reports a number.
    """
    from .multi_run import run_detectors as _rd
    return _rd(text_or_pages, keys)


def run_detection(
    text: str,
    *,
    title: Optional[str] = None,
    backend: str = DEFAULT_BACKEND,
    check_id=None,
    **opts,
) -> AIDetectionResult:
    """Run the selected backend over manuscript body ``text``.

    Always returns a result; on any failure returns an "unavailable" result
    with a machine-readable ``abstain_reason``.
    """
    import time as _time
    from . import diagnostics

    detector = build_detector(backend, check_id=check_id, **opts)
    if detector is None:
        diagnostics.record({"backend": backend, "outcome": "unavailable",
                            "reason": "unknown_backend"})
        return make_unavailable("unknown_backend", backend)
    started = _time.monotonic()
    try:
        result = detector.detect(text, title=title)
    except Exception as exc:  # noqa: BLE001
        logger.warning("AI detection (%s) raised: %s", backend, exc)
        diagnostics.record({"backend": backend, "outcome": "error",
                            "reason": "detection_error", "error": str(exc)[:200]})
        return make_unavailable("detection_error", backend)
    # Record a text-free summary for the Settings debugger.
    try:
        diagnostics.record({
            "backend": result.backend_used or backend,
            "outcome": result.band,
            "score": result.overall_score,
            "reason": result.abstain_reason,
            "word_count": result.word_count,
            "spans": len(result.spans or []),
            "duration_ms": int((_time.monotonic() - started) * 1000),
        })
    except Exception:  # noqa: BLE001
        pass
    return result


__all__ = [
    "AIDetectionResult",
    "DetectionBackend",
    "SuspectSpan",
    "DISCLAIMER",
    "MIN_WORDS",
    "HIGH_THRESHOLD",
    "MEDIUM_THRESHOLD",
    "DEFAULT_BACKEND",
    "VALID_BACKENDS",
    "build_detector",
    "run_detection",
    "run_detectors",
    "model_manager",
]
