"""
Core Visibe SDK client.
Used by all framework integrations.
"""
import logging
import os
import sys
from typing import Optional

from .api import APIClient, SpanBatcher

# Production API URL (hardcoded - users never need to change this)
_PRODUCTION_API_URL = "https://api.visibe.ai"


def _is_openai_client(obj) -> bool:
    """Duck-type check: does this look like an OpenAI client?"""
    return hasattr(obj, 'chat') and hasattr(obj.chat, 'completions')


def _is_langchain_runnable(obj) -> bool:
    """Duck-type check: does this look like a LangChain/LangGraph runnable?"""
    return hasattr(obj, 'invoke') and hasattr(obj, 'get_graph')


def _is_langgraph_runnable(obj) -> bool:
    """Duck-type check: does this look like a LangGraph compiled graph?"""
    try:
        from langgraph.pregel import Pregel
        return isinstance(obj, Pregel)
    except ImportError:
        return False


def _is_crewai_crew(obj) -> bool:
    """Duck-type check: does this look like a CrewAI Crew?"""
    return hasattr(obj, 'kickoff') and hasattr(obj, 'agents') and hasattr(obj, 'tasks')


def _is_autogen_model_client(obj) -> bool:
    """Duck-type check: does this look like an AutoGen model client?"""
    return (
        hasattr(obj, 'create') and
        hasattr(obj, 'create_stream') and
        hasattr(obj, 'model_info')
    )


def _is_bedrock_client(obj) -> bool:
    """Check if this is a Bedrock runtime client.

    Uses boto3's service metadata for reliable detection rather than
    pure duck-typing (which could false-positive on any object with
    converse/converse_stream/invoke_model methods).
    """
    # Primary: use boto3 service metadata (most reliable)
    try:
        if hasattr(obj, 'meta') and hasattr(obj.meta, 'service_model'):
            return obj.meta.service_model.service_name == 'bedrock-runtime'
    except Exception:
        pass

    # Fallback: duck-type check for non-standard boto3 wrappers
    return (
        hasattr(obj, 'converse') and
        hasattr(obj, 'converse_stream') and
        hasattr(obj, 'invoke_model') and
        hasattr(obj, 'meta')  # all boto3 clients have .meta
    )


# SDK logger
logger = logging.getLogger("visibe")
logger.addHandler(logging.NullHandler())  # No output by default


class _InstrumentContext:
    """Optional context manager for auto-cleanup after instrument().

    Instrumentation happens eagerly in instrument(), not deferred to __enter__.
    If the return value is ignored, behavior is identical to calling instrument()
    without a context manager — the caller must call uninstrument() manually.
    """

    def __init__(self, visibe: 'Visibe', client):
        self._visibe = visibe
        self._client = client

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._visibe.uninstrument(self._client)
        return False


class Visibe:
    """
    Main SDK client for Visibe observability platform.

    Usage:
        from visibe import Visibe
        from visibe.integrations.crewai import CrewAIIntegration

        # Production
        obs = Visibe(api_key="sk_live_abc123")

        # Or use environment variable
        export VISIBE_API_KEY=sk_live_abc123
        obs = Visibe()

    Environment Variables:
        VISIBE_API_KEY: Your API key (required for production)

    Example:
        >>> import visibe
        >>> obs = visibe.Visibe(api_key="sk_live_abc123")
        >>> # Traces automatically sent to https://api.visibe.ai
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        session_id: Optional[str] = None,
        timeout: int = 10,
        api_url: Optional[str] = None  # Hidden parameter for testing
    ):
        """
        Initialize Visibe client.

        Args:
            api_key: Your API key (reads from VISIBE_API_KEY env if not provided)
            session_id: Optional session ID for grouping traces
            timeout: Request timeout in seconds (default: 10)
            api_url: Override API URL (for internal testing only - not documented)

        Example:
            >>> obs = Visibe(api_key="sk_live_abc123")
            >>> obs = Visibe()  # Uses VISIBE_API_KEY environment variable
        """
        # API key from parameter or environment variable
        self.api_key = api_key or os.getenv('VISIBE_API_KEY')
        
        # Warn if no API key provided (print to stderr so it's always visible)
        if not self.api_key:
            print(
                "[Visibe] Warning: No API key found. "
                "Set VISIBE_API_KEY env var or pass api_key parameter.",
                file=sys.stderr
            )

        # Validate API key format (if provided)
        if self.api_key and not self.api_key.startswith(('sk_live_', 'sk_test_')):
            print(
                "[Visibe] Warning: API key format unrecognized "
                "(expected sk_live_* or sk_test_*).",
                file=sys.stderr
            )
        
        # Session ID (parameter only, not env var)
        self.session_id = session_id
        
        # Timeout validation
        if timeout <= 0:
            raise ValueError(f"timeout must be positive, got: {timeout}")
        self.timeout = timeout
        
        # API URL - use production by default, allow override via env or parameter
        self.api_url = api_url or os.getenv('VISIBE_API_URL') or _PRODUCTION_API_URL
        
        # Validate API URL format
        if not self.api_url.startswith(('http://', 'https://')):
            raise ValueError(
                f"api_url must start with http:// or https://, got: {self.api_url}"
            )
        
        # Normalize API URL
        self.api_url = self.api_url.rstrip('/')
        
        # Initialize API client and span batcher
        self.api_client = APIClient(self.api_url, self.api_key, self.timeout)
        self._batcher = SpanBatcher(self.api_client)

        # Log initialization at debug level
        logger.debug(f"Visibe initialized: url={self.api_url}, has_key={bool(self.api_key)}")

    def create_trace(self, trace_data: dict) -> bool:
        """Create a trace header on the backend."""
        if self.session_id and 'session_id' not in trace_data:
            trace_data['session_id'] = self.session_id
        return self.api_client.create_trace(trace_data)

    def send_span(self, trace_id: str, span_data: dict) -> bool:
        """Send a single span directly (bypasses batcher)."""
        return self.api_client.send_span(trace_id, span_data)

    def queue_span(self, trace_id: str, span_data: dict):
        """Queue a span for batched sending. Non-blocking."""
        self._batcher.add(trace_id, span_data)

    def flush_spans(self):
        """Flush all queued spans immediately."""
        self._batcher.flush()

    def complete_trace(self, trace_id: str, summary: dict) -> bool:
        """Flush pending spans, then mark trace as complete."""
        self._batcher.flush()
        return self.api_client.complete_trace(trace_id, summary)

    def _get_openai_integration(self):
        """Lazily create and cache the OpenAI integration."""
        if not hasattr(self, '_openai_integration'):
            from .integrations.openai import OpenAIIntegration
            self._openai_integration = OpenAIIntegration(self)
        return self._openai_integration

    def _get_langchain_integration(self):
        """Lazily create and cache the LangChain integration."""
        if not hasattr(self, '_langchain_integration'):
            from .integrations.langchain import LangChainIntegration
            self._langchain_integration = LangChainIntegration(self)
        return self._langchain_integration

    def _get_langgraph_integration(self):
        """Lazily create and cache the LangGraph integration."""
        if not hasattr(self, '_langgraph_integration'):
            from .integrations.langgraph import LangGraphIntegration
            self._langgraph_integration = LangGraphIntegration(self)
        return self._langgraph_integration

    def _get_crewai_integration(self):
        """Lazily create and cache the CrewAI integration."""
        if not hasattr(self, '_crewai_integration'):
            from .integrations.crewai import CrewAIIntegration
            self._crewai_integration = CrewAIIntegration(self)
        return self._crewai_integration

    def _get_autogen_integration(self):
        """Lazily create and cache the AutoGen integration."""
        if not hasattr(self, '_autogen_integration'):
            from .integrations.autogen import AutoGenIntegration
            self._autogen_integration = AutoGenIntegration(self)
        return self._autogen_integration

    def _get_bedrock_integration(self):
        """Lazily create and cache the Bedrock integration."""
        if not hasattr(self, '_bedrock_integration'):
            from .integrations.bedrock import BedrockIntegration
            self._bedrock_integration = BedrockIntegration(self)
        return self._bedrock_integration

    def instrument(self, client, *, name: Optional[str] = None,
                   content_limit: Optional[int] = None):
        """Auto-instrument a client, runnable, or crew.

        Supports OpenAI clients, LangChain/LangGraph runnables, CrewAI crews,
        and AutoGen model clients.
        Returns a context manager for optional automatic cleanup.

        Args:
            client: An OpenAI client, LangChain/LangGraph runnable, or CrewAI Crew
            name: Optional trace name. Shown in the Visibe dashboard.
            content_limit: Max chars for LLM/tool input/output in trace spans.
                          None = defaults. 0 = omit content entirely.

        Returns:
            _InstrumentContext: Use with 'with' for auto-cleanup, or ignore.

        Example:
            >>> obs = Visibe()
            >>> with obs.instrument(crew, name="my-workflow"):
            ...     crew.kickoff()
            >>> # Or classic style:
            >>> obs.instrument(graph, name="trip-planner")
            >>> graph.invoke(...)
            >>> obs.uninstrument(graph)
        """
        if _is_openai_client(client):
            self._get_openai_integration().instrument(client, name=name)
        elif _is_crewai_crew(client):
            self._get_crewai_integration().instrument(client, name=name)
        elif _is_autogen_model_client(client):
            self._get_autogen_integration().instrument(client, name=name)
        elif _is_bedrock_client(client):
            self._get_bedrock_integration().instrument(client, name=name)
        elif _is_langchain_runnable(client):
            if _is_langgraph_runnable(client):
                self._get_langgraph_integration().instrument(
                    client, name=name, content_limit=content_limit
                )
            else:
                self._get_langchain_integration().instrument(
                    client, name=name, content_limit=content_limit
                )
        else:
            raise TypeError(
                f"Unsupported type for instrument(): {type(client).__name__}. "
                f"Expected an OpenAI client, Bedrock client, "
                f"LangChain/LangGraph runnable, CrewAI Crew, or AutoGen model client."
            )

        if name:
            print(f'[Visibe] Tracking enabled for "{name}"')
        else:
            print('[Visibe] Tracking enabled')

        return _InstrumentContext(self, client)

    def uninstrument(self, client):
        """Remove instrumentation from a previously instrumented client, runnable, or crew.

        Args:
            client: A previously instrumented OpenAI client, LangChain/LangGraph runnable, or CrewAI Crew
        """
        if _is_openai_client(client):
            self._get_openai_integration().uninstrument(client)
        elif _is_crewai_crew(client):
            self._get_crewai_integration().uninstrument(client)
        elif _is_autogen_model_client(client):
            self._get_autogen_integration().uninstrument(client)
        elif _is_bedrock_client(client):
            self._get_bedrock_integration().uninstrument(client)
        elif _is_langchain_runnable(client):
            if _is_langgraph_runnable(client):
                self._get_langgraph_integration().uninstrument(client)
            else:
                self._get_langchain_integration().uninstrument(client)
        else:
            raise TypeError(
                f"Unsupported type for uninstrument(): {type(client).__name__}."
            )

    def track(self, client=None, name: str = "trace"):
        """Context manager to group calls into one trace.

        Supports OpenAI clients, CrewAI crews, LangChain/LangGraph runnables,
        and AutoGen model clients.

        Args:
            client: An OpenAI client, CrewAI Crew, or LangChain/LangGraph runnable
            name: Name for the grouped trace

        Example:
            >>> with obs.track(openai_client, name="my-chat"):
            ...     r1 = client.chat.completions.create(...)
            >>> with obs.track(crew, name="my-workflow"):
            ...     crew.kickoff()
        """
        if client is None:
            raise TypeError("track() requires a client argument")

        if _is_openai_client(client):
            return self._get_openai_integration().track(client=client, name=name)
        elif _is_crewai_crew(client):
            return self._get_crewai_integration().track(client, name=name)
        elif _is_autogen_model_client(client):
            return self._get_autogen_integration().track(
                model_client=client, name=name
            )
        elif _is_bedrock_client(client):
            return self._get_bedrock_integration().track(client=client, name=name)
        elif _is_langchain_runnable(client):
            if _is_langgraph_runnable(client):
                return self._get_langgraph_integration().track(name=name)
            return self._get_langchain_integration().track(name=name)
        else:
            raise TypeError(
                f"Unsupported type for track(): {type(client).__name__}. "
                f"Expected an OpenAI client, Bedrock client, "
                f"LangChain/LangGraph runnable, CrewAI Crew, or AutoGen model client."
            )
