"""PlyraClient and initialization."""

from __future__ import annotations

import importlib.util
import logging
import sys
from typing import TYPE_CHECKING

from opentelemetry import trace as trace_api
from opentelemetry.sdk.trace.export import SpanProcessor

from plyra_trace import _tracer

if TYPE_CHECKING:
    pass

logger = logging.getLogger("plyra_trace._client")

_VALID_KEY_PREFIXES = ("plt_live_", "plt_test_")

# Frameworks we know how to auto-instrument
_FRAMEWORK_CHECKS: list[tuple[str, str]] = [
    ("openai", "openai"),
    ("anthropic", "anthropic"),
    ("litellm", "litellm"),
    ("langchain", "langchain_core"),
    ("langgraph", "langgraph"),
    ("crewai", "crewai"),
    ("autogen", "autogen"),
    ("haystack", "haystack"),
    ("dspy", "dspy"),
    ("llama_index", "llama_index"),
]

# Instrumentor entry-point package for each framework
_INSTRUMENTOR_PACKAGE: dict[str, str] = {
    "openai": "plyra_instrument_openai",
    "anthropic": "plyra_instrument_anthropic",
    "langchain": "plyra_instrument_langchain",
    "langgraph": "plyra_instrument_langchain",  # shares langchain handler
    "litellm": "plyra_instrument_litellm",
    "crewai": "plyra_instrument_crewai",
    "autogen": "plyra_instrument_autogen",
}


class PlyraClient:
    """
    plyra-trace client.
    Returned by init() and provides access to configuration and shutdown.
    """

    def __init__(
        self,
        project: str,
        endpoint: str | None,
        protocol: str,
        environment: str,
        api_key: str | None = None,
        instrumented_frameworks: list[str] | None = None,
        filter_spans: list[str] | None = None,
    ):
        self.project = project
        self.endpoint = endpoint
        self.protocol = protocol
        self.environment = environment
        self.api_key = api_key
        self.instrumented_frameworks: list[str] = instrumented_frameworks or []
        self.filter_spans: list[str] = filter_spans or []

    def shutdown(self) -> None:
        """Shutdown the tracer provider and flush pending spans."""
        _tracer.shutdown()

    def get_tracer(self, name: str = "plyra-trace") -> trace_api.Tracer:
        """
        Get a named tracer for instrumentors.

        Args:
            name: Tracer name

        Returns:
            Tracer instance
        """
        return _tracer.get_tracer(name)


def _detect_frameworks() -> list[str]:
    """
    Check sys.modules and importlib for installed packages.
    Returns list of detected framework names (e.g. ["openai", "langchain"]).
    """
    detected: list[str] = []
    for name, pkg in _FRAMEWORK_CHECKS:
        # Fast path: already imported
        if pkg in sys.modules or pkg.split(".")[0] in sys.modules:
            detected.append(name)
            continue
        # Slow path: check if package is installed (without importing it)
        try:
            spec = importlib.util.find_spec(pkg)
            if spec is not None:
                detected.append(name)
        except (ImportError, ValueError, ModuleNotFoundError):
            pass
    return detected


def _apply_instrumentor(framework: str) -> bool:
    """
    Attempt to load and call the plyra-instrument-{framework} package's
    ``instrument()`` function. Returns True on success, False if the
    instrumentor package is not installed.
    """
    pkg_name = _INSTRUMENTOR_PACKAGE.get(framework)
    if not pkg_name:
        return False
    try:
        spec = importlib.util.find_spec(pkg_name)
        if spec is None:
            return False
        mod = importlib.import_module(pkg_name)
        if callable(getattr(mod, "instrument", None)):
            mod.instrument()
            logger.debug("Auto-instrumented framework: %s via %s", framework, pkg_name)
            return True
    except Exception as exc:  # noqa: BLE001
        logger.debug("Failed to auto-instrument %s: %s", framework, exc)
    return False


def _validate_api_key(api_key: str) -> None:
    """
    Validate the format of a plyra-trace API key.

    Args:
        api_key: Key to validate

    Raises:
        ValueError: If the key format is invalid
    """
    if not any(api_key.startswith(prefix) for prefix in _VALID_KEY_PREFIXES):
        raise ValueError(
            f"Invalid API key format. Key must start with one of: "
            f"{', '.join(_VALID_KEY_PREFIXES)}. "
            f"Generate keys with: plyra-trace keys create --project NAME"
        )


def init(
    project: str,
    api_key: str | None = None,
    endpoint: str | None = None,
    protocol: str = "http/protobuf",
    environment: str = "development",
    framework: str | None = None,
    auto_instrument: bool = False,
    filter_spans: list[str] | None = None,
    headers: dict[str, str] | None = None,
    span_processors: list[SpanProcessor] | None = None,
    batch: bool = True,
) -> PlyraClient:
    """
    Initialize plyra-trace. Call once at application startup.

    Args:
        project: Project name (sets service.name resource attribute)
        api_key: Optional API key ("plt_live_..." or "plt_test_...").
            Sets Authorization: Bearer header on OTLP requests.
        endpoint: OTLP endpoint URL. None = ConsoleSpanExporter (dev mode)
        protocol: "http/protobuf" or "grpc"
        environment: Deployment environment (e.g., "production", "staging")
        framework: Explicit framework(s) to instrument. None = auto-detect.
            Accepts a single name ("openai") or comma-separated list
            ("openai+langchain" or "openai,langchain").
            Supported: openai, anthropic, litellm, langchain, langgraph,
            crewai, autogen, haystack, dspy, llama_index.
        auto_instrument: Auto-detect installed frameworks and patch them.
            When True, all detected frameworks that have a corresponding
            plyra-instrument-* package installed will be instrumented.
        filter_spans: Glob patterns for span names to drop before export.
            E.g. ["starlette.*", "GET /health", "a2a.*"].
            Uses fnmatch syntax. Empty list or None = no filtering.
        headers: Optional extra auth headers for managed backends
        span_processors: Additional OTel SpanProcessors
            (e.g., compat processors)
        batch: Use BatchSpanProcessor (True) or SimpleSpanProcessor (False)

    Returns:
        PlyraClient instance

    Examples:
        2-line quickstart (auto-detects openai, anthropic, langchain, etc.):
        >>> pt = plyra_trace.init(
        ...     project="my-agent",
        ...     endpoint="http://localhost:7700",
        ...     auto_instrument=True,
        ... )

        Export to any OTLP backend:
        >>> pt = plyra_trace.init(
        ...     project="my-agent",
        ...     endpoint="http://localhost:4318",
        ... )

        Console output (dev mode):
        >>> pt = plyra_trace.init(project="my-agent")

        Explicit framework selection:
        >>> pt = plyra_trace.init(
        ...     project="my-agent",
        ...     endpoint="http://localhost:7700",
        ...     framework="openai+langchain",
        ... )

        With compat processors for existing OpenInference instrumentation:
        >>> from plyra_trace.compat import OpenInferenceSpanProcessor
        >>> pt = plyra_trace.init(
        ...     project="my-agent",
        ...     endpoint="http://localhost:4318",
        ...     span_processors=[OpenInferenceSpanProcessor()],
        ... )

        With API key auth (managed backend or local collector with auth):
        >>> pt = plyra_trace.init(
        ...     project="my-agent",
        ...     api_key="plt_live_7f3a2bc849d1e6f0",
        ...     endpoint="http://localhost:7700",
        ... )
    """
    # Validate and inject API key
    merged_headers: dict[str, str] = dict(headers or {})
    if api_key is not None:
        _validate_api_key(api_key)
        merged_headers["Authorization"] = f"Bearer {api_key}"

    # Configure the tracer
    _tracer.configure(
        project=project,
        endpoint=endpoint,
        protocol=protocol,
        environment=environment,
        headers=merged_headers if merged_headers else None,
        span_processors=span_processors,
        batch=batch,
        filter_spans=filter_spans or None,
    )

    # Determine which frameworks to instrument
    instrumented: list[str] = []

    frameworks_to_instrument: list[str] = []
    if framework is not None:
        # Parse comma- or plus-separated list
        for sep in ("+", ","):
            if sep in framework:
                frameworks_to_instrument = [f.strip() for f in framework.split(sep)]
                break
        else:
            frameworks_to_instrument = [framework.strip()]

    if auto_instrument:
        # Merge explicit list with auto-detected ones (deduplicated)
        detected = _detect_frameworks()
        logger.debug("Auto-detected frameworks: %s", detected)
        seen: set[str] = set(frameworks_to_instrument)
        for f in detected:
            if f not in seen:
                frameworks_to_instrument.append(f)
                seen.add(f)

    for fw in frameworks_to_instrument:
        if _apply_instrumentor(fw):
            instrumented.append(fw)

    if instrumented:
        logger.info("plyra-trace: instrumented frameworks: %s", ", ".join(instrumented))

    return PlyraClient(
        project=project,
        endpoint=endpoint,
        protocol=protocol,
        environment=environment,
        api_key=api_key,
        instrumented_frameworks=instrumented,
        filter_spans=filter_spans,
    )
