"""
LLM client wrappers for tracking LLM calls
"""
import time
import logging
from typing import Optional, Dict, Any, Union
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode, SpanKind
from cascade.tracing import get_tracer

logger = logging.getLogger(__name__)

# Cost per 1K tokens (input, output) for Anthropic models
ANTHROPIC_COST_PER_1K_TOKENS = {
    "claude-3-opus-20240229": {"input": 0.015, "output": 0.075},
    "claude-3-5-sonnet-20241022": {"input": 0.003, "output": 0.015},
    "claude-3-5-haiku-20241022": {"input": 0.0008, "output": 0.004},
    "claude-3-sonnet-20240229": {"input": 0.003, "output": 0.015},
    "claude-3-haiku-20240307": {"input": 0.00025, "output": 0.00125},
    "claude-3-5-opus-20241022": {"input": 0.015, "output": 0.075},
}


def _calculate_anthropic_cost_usd(model: str, input_tokens: int, output_tokens: int) -> Optional[float]:
    """
    Calculate estimated cost in USD for Anthropic models.

    We only compute cost when we have an explicit price entry for the model;
    returning an incorrect cost is worse than returning no cost.
    """
    costs = ANTHROPIC_COST_PER_1K_TOKENS.get(model)
    if not costs:
        return None
    input_cost = (input_tokens / 1000) * costs["input"]
    output_cost = (output_tokens / 1000) * costs["output"]
    return input_cost + output_cost


def _truncate_text(text: str, max_length: Optional[int] = None) -> str:
    """Truncate text if it's too long, preserving structure."""
    if max_length is None:
        return text
    if len(text) <= max_length:
        return text
    return text[:max_length] + f"\n... (truncated, total length: {len(text)})"


def _extract_messages(messages: list) -> Dict[str, Any]:
    """Extract and format messages for span attributes."""
    formatted_messages = []
    system_message = None
    
    for msg in messages:
        if isinstance(msg, dict):
            role = msg.get("role", "unknown")
            content = msg.get("content", "")
            
            if role == "system":
                system_message = content
            else:
                formatted_messages.append({
                    "role": role,
                    "content": _truncate_text(str(content))
                })
        else:
            formatted_messages.append({
                "role": getattr(msg, "role", "unknown"),
                "content": _truncate_text(str(getattr(msg, "content", "")))
            })
    
    return {
        "system": system_message,
        "messages": formatted_messages
    }


def _is_llm_error(e: Exception) -> bool:
    """Check if an exception originated from the LLM provider (not from Cascade instrumentation)."""
    module = type(e).__module__ or ""
    return any(provider in module for provider in ("openai", "anthropic", "httpx"))


def _set_prompt_attributes(span: trace.Span, prompt_messages: list, include_role_prefix: bool = False) -> None:
    """
    Store both full prompt context and the latest user prompt.

    - llm.prompt_full: full accumulated prompt context sent to model
    - llm.prompt_last_user: latest user message only
    - llm.prompt: alias to latest user message when available, otherwise full
    """
    if not prompt_messages:
        return

    prompt_lines = []
    for msg in prompt_messages:
        content = str(msg.get("content", ""))
        if not content:
            continue
        if include_role_prefix:
            role = msg.get("role", "unknown")
            prompt_lines.append(f"{role}: {content}")
        else:
            prompt_lines.append(content)

    if not prompt_lines:
        return

    full_prompt = "\n".join(prompt_lines)
    span.set_attribute("llm.prompt_full", _truncate_text(full_prompt))

    last_user_prompt = None
    for msg in reversed(prompt_messages):
        if msg.get("role") == "user":
            content = str(msg.get("content", ""))
            if content:
                last_user_prompt = content
                break

    if last_user_prompt:
        truncated_last = _truncate_text(last_user_prompt)
        span.set_attribute("llm.prompt_last_user", truncated_last)
        span.set_attribute("llm.prompt", truncated_last)
    else:
        span.set_attribute("llm.prompt", _truncate_text(full_prompt))


class _AnthropicMessagesWrapper:
    """Wrapper for Anthropic Messages API to track LLM calls."""
    
    def __init__(self, original_messages, tracer: Optional[trace.Tracer]):
        self._original = original_messages
        self._tracer = tracer
    
    def create(self, *args, **kwargs):
        """Intercept messages.create() to create spans."""
        if not self._tracer:
            return self._original.create(*args, **kwargs)
        
        try:
            model = kwargs.get("model", "unknown")
            messages = kwargs.get("messages", [])
            max_tokens = kwargs.get("max_tokens")
            message_data = _extract_messages(messages)
            start_time = time.time()
            
            span_name = f"llm.{model}"
            with self._tracer.start_as_current_span(
                span_name,
                kind=SpanKind.CLIENT
            ) as span_context:
                span = trace.get_current_span()
                
                span.set_attribute("cascade.span_type", "llm")
                span.set_attribute("llm.model", model)
                span.set_attribute("llm.provider", "anthropic")
                
                from cascade.tracing import get_current_agent
                current_agent = get_current_agent()
                if current_agent:
                    span.set_attribute("cascade.agent_name", current_agent)
                
                if max_tokens:
                    span.set_attribute("llm.max_tokens", max_tokens)
                
                if message_data["system"]:
                    span.set_attribute("llm.system_message", _truncate_text(message_data["system"]))
                
                user_messages = [msg for msg in message_data["messages"] if msg["role"] == "user"]
                _set_prompt_attributes(span, user_messages, include_role_prefix=False)
                
                span.set_attribute("llm.messages", str(message_data["messages"]))
                
                try:
                    response = self._original.create(*args, **kwargs)
                    
                    latency_ms = (time.time() - start_time) * 1000
                    input_tokens = 0
                    output_tokens = 0
                    total_tokens = 0
                    
                    if hasattr(response, "usage"):
                        usage = response.usage
                        input_tokens = getattr(usage, "input_tokens", 0)
                        output_tokens = getattr(usage, "output_tokens", 0)
                        total_tokens = input_tokens + output_tokens
                    
                    completion_text = ""
                    reasoning_text = None
                    
                    if hasattr(response, "content"):
                        content_parts = []
                        reasoning_parts = []
                        
                        for item in response.content:
                            if hasattr(item, "type"):
                                if item.type == "text":
                                    content_parts.append(getattr(item, "text", ""))
                                elif item.type == "tool_use":
                                    tool_content = f"[Tool Use: {getattr(item, 'name', 'unknown')}]"
                                    content_parts.append(tool_content)
                                elif hasattr(item, "text"):
                                    content_parts.append(item.text)
                            
                            if hasattr(item, "reasoning"):
                                reasoning_parts.append(str(item.reasoning))
                        
                        completion_text = "\n".join(content_parts)
                        if reasoning_parts:
                            reasoning_text = "\n".join(reasoning_parts)
                    
                    span.set_attribute("llm.input_tokens", input_tokens)
                    span.set_attribute("llm.output_tokens", output_tokens)
                    span.set_attribute("llm.total_tokens", total_tokens)
                    span.set_attribute("llm.latency_ms", latency_ms)
                    
                    if completion_text:
                        span.set_attribute("llm.completion", completion_text)
                        
                        from cascade.celestra_extensions import _extract_reasoning
                        extracted_reasoning = _extract_reasoning(completion_text)
                        if extracted_reasoning:
                            span.set_attribute("llm.reasoning", extracted_reasoning)
                    
                    if reasoning_text:
                        span.set_attribute("llm.reasoning", reasoning_text)
                    
                    if input_tokens > 0 or output_tokens > 0:
                        cost = _calculate_anthropic_cost_usd(model, input_tokens, output_tokens)
                        if cost is not None:
                            span.set_attribute("llm.cost_usd", cost)
                            span.set_attribute("llm.cost", cost)
                    
                    span.set_status(Status(StatusCode.OK))
                    
                    return response
                    
                except Exception as e:
                    latency_ms = (time.time() - start_time) * 1000
                    span.set_attribute("llm.latency_ms", latency_ms)
                    span.set_status(Status(StatusCode.ERROR, str(e)))
                    span.record_exception(e)
                    raise
        except Exception as e:
            if _is_llm_error(e):
                raise
            logger.debug(f"Cascade tracing error in Anthropic create(), falling back to original: {e}")
            return self._original.create(*args, **kwargs)
    
    def stream(self, *args, **kwargs):
        """Intercept messages.stream() for streaming responses."""
        if not self._tracer:
            return self._original.stream(*args, **kwargs)
        
        try:
            stream_manager = self._original.stream(*args, **kwargs)
        except Exception:
            raise
        
        try:
            model = kwargs.get("model", "unknown")
            messages = kwargs.get("messages", [])
            message_data = _extract_messages(messages)
            start_time = time.time()
        except Exception as e:
            logger.debug(f"Cascade tracing error in Anthropic stream() setup: {e}")
            return stream_manager
        
        class _StreamWrapper:
            """Wrapper for MessageStreamManager that adds tracing."""
            
            def __init__(self, stream_manager, tracer, model, message_data, start_time):
                self._stream_manager = stream_manager
                self._tracer = tracer
                self._model = model
                self._message_data = message_data
                self._start_time = start_time
                self._span = None
                self._chunks = []
                self._text_content = ""
                self._final_message = None
            
            def __enter__(self):
                self._stream = self._stream_manager.__enter__()
                
                span_name = f"llm.{self._model}.stream"
                self._span_context = self._tracer.start_as_current_span(
                    span_name,
                    kind=SpanKind.CLIENT
                )
                self._span_context.__enter__()
                self._span = trace.get_current_span()
                
                self._span.set_attribute("cascade.span_type", "llm")
                self._span.set_attribute("llm.model", self._model)
                self._span.set_attribute("llm.provider", "anthropic")
                self._span.set_attribute("llm.streaming", True)
                
                from cascade.tracing import get_current_agent
                current_agent = get_current_agent()
                if current_agent:
                    self._span.set_attribute("cascade.agent_name", current_agent)
                
                if self._message_data["system"]:
                    self._span.set_attribute("llm.system_message", _truncate_text(self._message_data["system"]))
                
                user_messages = [msg for msg in self._message_data["messages"] if msg["role"] == "user"]
                _set_prompt_attributes(self._span, user_messages, include_role_prefix=False)
                
                return self
            
            def __exit__(self, exc_type, exc_val, exc_tb):
                if self._span:
                    latency_ms = (time.time() - self._start_time) * 1000
                    
                    input_tokens = 0
                    output_tokens = 0
                    total_tokens = 0
                    
                    try:
                        final_message = self._final_message
                        if final_message is None:
                            if hasattr(self, '_stream') and hasattr(self._stream, 'get_final_message'):
                                try:
                                    final_message = self._stream.get_final_message()
                                    self._final_message = final_message
                                except Exception:
                                    pass
                        
                        if final_message and hasattr(final_message, "usage"):
                            usage = final_message.usage
                            input_tokens = getattr(usage, "input_tokens", 0)
                            output_tokens = getattr(usage, "output_tokens", 0)
                            total_tokens = input_tokens + output_tokens
                    except Exception as e:
                        logger.debug(f"Could not get usage from final message: {e}")
                        if self._text_content:
                            output_tokens = len(self._text_content) // 4
                            total_tokens = output_tokens
                    
                    self._span.set_attribute("llm.latency_ms", latency_ms)
                    self._span.set_attribute("llm.input_tokens", input_tokens)
                    self._span.set_attribute("llm.output_tokens", output_tokens)
                    self._span.set_attribute("llm.total_tokens", total_tokens)
                    
                    completion_text = self._text_content
                    if final_message and hasattr(final_message, "content"):
                        try:
                            final_text_parts = []
                            for block in final_message.content:
                                if hasattr(block, "type") and block.type == "text":
                                    if hasattr(block, "text"):
                                        final_text_parts.append(block.text)
                            if final_text_parts:
                                completion_text = "\n".join(final_text_parts)
                        except Exception:
                            pass
                    
                    if completion_text:
                        self._span.set_attribute("llm.completion", completion_text)
                        
                        from cascade.celestra_extensions import _extract_reasoning
                        reasoning_text = _extract_reasoning(completion_text)
                        if reasoning_text:
                            self._span.set_attribute("llm.reasoning", reasoning_text)
                    
                    if exc_type:
                        self._span.set_status(Status(StatusCode.ERROR, str(exc_val)))
                        self._span.record_exception(exc_val)
                    else:
                        self._span.set_status(Status(StatusCode.OK))
                
                self._span_context.__exit__(exc_type, exc_val, exc_tb)
                
                return self._stream_manager.__exit__(exc_type, exc_val, exc_tb)
            
            def __iter__(self):
                if not hasattr(self, '_stream'):
                    self.__enter__()
                    self._auto_exit = True
                else:
                    self._auto_exit = False
                
                return self
            
            def __next__(self):
                if not hasattr(self, '_stream'):
                    raise RuntimeError("Stream not initialized. Use 'with stream:' or iterate directly.")
                
                try:
                    event = next(self._stream)
                    self._chunks.append(event)
                    
                    if hasattr(event, "type") and event.type == "content_block_delta":
                        if hasattr(event, "delta") and hasattr(event.delta, "text"):
                            self._text_content += event.delta.text
                    
                    return event
                except StopIteration:
                    if hasattr(self, '_auto_exit') and self._auto_exit:
                        self.__exit__(None, None, None)
                    raise
            
            def get_final_message(self):
                if not hasattr(self, '_stream'):
                    raise RuntimeError("Stream not initialized. Use 'with stream:' first.")
                
                if hasattr(self._stream, 'get_final_message'):
                    self._final_message = self._stream.get_final_message()
                    
                    if self._span and self._final_message:
                        try:
                            if hasattr(self._final_message, "usage"):
                                usage = self._final_message.usage
                                input_tokens = getattr(usage, "input_tokens", 0)
                                output_tokens = getattr(usage, "output_tokens", 0)
                                total_tokens = input_tokens + output_tokens
                                
                                self._span.set_attribute("llm.input_tokens", input_tokens)
                                self._span.set_attribute("llm.output_tokens", output_tokens)
                                self._span.set_attribute("llm.total_tokens", total_tokens)
                            
                            if hasattr(self._final_message, "content"):
                                final_text_parts = []
                                for block in self._final_message.content:
                                    if hasattr(block, "type") and block.type == "text":
                                        if hasattr(block, "text"):
                                            final_text_parts.append(block.text)
                                if final_text_parts:
                                    full_completion = "\n".join(final_text_parts)
                                    self._span.set_attribute("llm.completion", full_completion)
                                    
                                    from cascade.celestra_extensions import _extract_reasoning
                                    reasoning_text = _extract_reasoning(full_completion)
                                    if reasoning_text:
                                        self._span.set_attribute("llm.reasoning", reasoning_text)
                        except Exception as e:
                            logger.debug(f"Could not update span from final message: {e}")
                    
                    return self._final_message
                else:
                    raise AttributeError(
                        f"get_final_message not available on stream type {type(self._stream)}. "
                        "Make sure you're using Anthropic SDK version that supports get_final_message()."
                    )
        
        return _StreamWrapper(stream_manager, self._tracer, model, message_data, start_time)


class _AnthropicWrapper:
    """Wrapper for Anthropic client to track LLM calls."""
    
    def __init__(self, client, tracer: Optional[trace.Tracer]):
        self._client = client
        self._tracer = tracer
        for attr in dir(client):
            if not attr.startswith("_") and attr != "messages":
                try:
                    setattr(self, attr, getattr(client, attr))
                except:
                    pass
    
    @property
    def messages(self):
        """Return wrapped messages API."""
        return _AnthropicMessagesWrapper(self._client.messages, self._tracer)
    
    def __getattr__(self, name):
        """Delegate any other attribute access to original client."""
        return getattr(self._client, name)


class _OpenAICompletionsWrapper:
    """Wrapper for OpenAI Chat Completions API to track LLM calls."""
    
    def __init__(self, original_completions, tracer: Optional[trace.Tracer], base_url: str):
        self._original = original_completions
        self._tracer = tracer
        self._base_url = base_url or ""
    
    def create(self, *args, **kwargs):
        """Intercept chat.completions.create() to create spans."""
        if not self._tracer:
            return self._original.create(*args, **kwargs)
        
        if "openrouter.ai" in self._base_url.lower():
            if "extra_body" not in kwargs:
                kwargs["extra_body"] = {}
            kwargs["extra_body"]["usage"] = {"include": True}
        
        is_streaming = kwargs.get("stream", False)
        
        if is_streaming:
            return self._create_streaming(*args, **kwargs)
        else:
            return self._create_non_streaming(*args, **kwargs)
    
    def _create_non_streaming(self, *args, **kwargs):
        """Handle non-streaming requests."""
        try:
            model = kwargs.get("model", "unknown")
            messages = kwargs.get("messages", [])
            max_tokens = kwargs.get("max_tokens")
            message_data = _extract_messages(messages)
            start_time = time.time()
            
            span_name = f"llm.{model}"
            with self._tracer.start_as_current_span(
                span_name,
                kind=SpanKind.CLIENT
            ) as span_context:
                span = trace.get_current_span()
                
                span.set_attribute("cascade.span_type", "llm")
                span.set_attribute("llm.model", model)
                span.set_attribute("llm.provider", "openai")
                
                from cascade.tracing import get_current_agent
                current_agent = get_current_agent()
                if current_agent:
                    span.set_attribute("cascade.agent_name", current_agent)
                
                if max_tokens:
                    span.set_attribute("llm.max_tokens", max_tokens)
                
                messages_with_content = [
                    msg for msg in message_data["messages"] 
                    if msg.get("content") and msg["role"] in ["user", "tool"]
                ]
                _set_prompt_attributes(span, messages_with_content, include_role_prefix=True)
                
                span.set_attribute("llm.messages", str(message_data["messages"]))
                
                try:
                    response = self._original.create(*args, **kwargs)
                    
                    latency_ms = (time.time() - start_time) * 1000
                    input_tokens = 0
                    output_tokens = 0
                    total_tokens = 0
                    
                    if hasattr(response, "usage") and response.usage:
                        usage = response.usage
                        input_tokens = getattr(usage, "prompt_tokens", 0)
                        output_tokens = getattr(usage, "completion_tokens", 0)
                        total_tokens = getattr(usage, "total_tokens", input_tokens + output_tokens)
                    
                    completion_text = ""
                    tool_calls_text = []
                    
                    if hasattr(response, "choices") and len(response.choices) > 0:
                        message = response.choices[0].message
                        
                        if hasattr(message, "content") and message.content:
                            completion_text = message.content
                        
                        if hasattr(message, "tool_calls") and message.tool_calls:
                            for tc in message.tool_calls:
                                tool_name = tc.function.name if hasattr(tc.function, "name") else "unknown"
                                tool_calls_text.append(f"[Tool Use: {tool_name}]")
                    
                    if tool_calls_text:
                        if completion_text:
                            completion_text = completion_text + "\n" + "\n".join(tool_calls_text)
                        else:
                            completion_text = "\n".join(tool_calls_text)
                    
                    span.set_attribute("llm.input_tokens", input_tokens)
                    span.set_attribute("llm.output_tokens", output_tokens)
                    span.set_attribute("llm.total_tokens", total_tokens)
                    span.set_attribute("llm.latency_ms", latency_ms)
                    
                    if completion_text:
                        span.set_attribute("llm.completion", completion_text)
                        
                        from cascade.celestra_extensions import _extract_reasoning
                        extracted_reasoning = _extract_reasoning(completion_text)
                        if extracted_reasoning:
                            span.set_attribute("llm.reasoning", extracted_reasoning)
                    
                    # OpenAI pricing varies by model/provider; don't guess.
                    
                    span.set_status(Status(StatusCode.OK))
                    
                    return response
                    
                except Exception as e:
                    latency_ms = (time.time() - start_time) * 1000
                    span.set_attribute("llm.latency_ms", latency_ms)
                    span.set_status(Status(StatusCode.ERROR, str(e)))
                    span.record_exception(e)
                    raise
        except Exception as e:
            if _is_llm_error(e):
                raise
            logger.debug(f"Cascade tracing error in OpenAI create(), falling back to original: {e}")
            return self._original.create(*args, **kwargs)
    
    def _create_streaming(self, *args, **kwargs):
        """Handle streaming requests."""
        try:
            stream = self._original.create(*args, **kwargs)
        except Exception:
            raise
        
        try:
            model = kwargs.get("model", "unknown")
            messages = kwargs.get("messages", [])
            message_data = _extract_messages(messages)
            start_time = time.time()
        except Exception as e:
            logger.debug(f"Cascade tracing error in OpenAI stream setup: {e}")
            return stream
        
        class _OpenAIStreamWrapper:
            """Wrapper for OpenAI stream that adds tracing."""
            
            def __init__(self, stream, tracer, model, message_data, start_time, base_url):
                self._stream = stream
                self._tracer = tracer
                self._model = model
                self._message_data = message_data
                self._start_time = start_time
                self._base_url = base_url
                self._span = None
                self._span_context = None
                self._chunks = []
                self._text_content = ""
                self._finish_reason = None
                self._usage_from_stream = None
                self._auto_exit = False
            
            def __enter__(self):
                span_name = f"llm.{self._model}.stream"
                self._span_context = self._tracer.start_as_current_span(
                    span_name,
                    kind=SpanKind.CLIENT
                )
                self._span_context.__enter__()
                self._span = trace.get_current_span()
                
                self._span.set_attribute("cascade.span_type", "llm")
                self._span.set_attribute("llm.model", self._model)
                self._span.set_attribute("llm.provider", "openai")
                self._span.set_attribute("llm.streaming", True)
                
                from cascade.tracing import get_current_agent
                current_agent = get_current_agent()
                if current_agent:
                    self._span.set_attribute("cascade.agent_name", current_agent)
                
                messages_with_content = [
                    msg for msg in self._message_data["messages"] 
                    if msg.get("content") and msg["role"] in ["user", "tool"]
                ]
                _set_prompt_attributes(self._span, messages_with_content, include_role_prefix=True)
                
                return self
            
            def __exit__(self, exc_type, exc_val, exc_tb):
                if self._span:
                    latency_ms = (time.time() - self._start_time) * 1000
                    
                    input_tokens = 0
                    output_tokens = 0
                    total_tokens = 0
                    
                    if self._usage_from_stream:
                        input_tokens = getattr(self._usage_from_stream, "prompt_tokens", 0)
                        output_tokens = getattr(self._usage_from_stream, "completion_tokens", 0)
                        total_tokens = getattr(self._usage_from_stream, "total_tokens", input_tokens + output_tokens)
                    else:
                        if self._text_content:
                            output_tokens = len(self._text_content) // 4
                            total_tokens = output_tokens
                    
                    self._span.set_attribute("llm.latency_ms", latency_ms)
                    self._span.set_attribute("llm.input_tokens", input_tokens)
                    self._span.set_attribute("llm.output_tokens", output_tokens)
                    self._span.set_attribute("llm.total_tokens", total_tokens)
                    
                    if self._text_content:
                        self._span.set_attribute("llm.completion", self._text_content)
                        
                        from cascade.celestra_extensions import _extract_reasoning
                        reasoning_text = _extract_reasoning(self._text_content)
                        if reasoning_text:
                            self._span.set_attribute("llm.reasoning", reasoning_text)
                    
                    if exc_type:
                        self._span.set_status(Status(StatusCode.ERROR, str(exc_val)))
                        self._span.record_exception(exc_val)
                    else:
                        self._span.set_status(Status(StatusCode.OK))
                
                if self._span_context:
                    self._span_context.__exit__(exc_type, exc_val, exc_tb)
                
                return False
            
            def __iter__(self):
                if self._span is None:
                    self.__enter__()
                    self._auto_exit = True
                
                return self
            
            def __next__(self):
                try:
                    chunk = next(self._stream)
                    self._chunks.append(chunk)
                    
                    if hasattr(chunk, "choices") and len(chunk.choices) > 0:
                        choice = chunk.choices[0]
                        
                        if hasattr(choice, "delta"):
                            delta = choice.delta
                            if hasattr(delta, "content") and delta.content:
                                self._text_content += delta.content
                        
                        if hasattr(choice, "finish_reason") and choice.finish_reason:
                            self._finish_reason = choice.finish_reason
                    
                    if hasattr(chunk, "usage") and chunk.usage:
                        self._usage_from_stream = chunk.usage
                    
                    return chunk
                    
                except StopIteration:
                    if self._auto_exit:
                        self.__exit__(None, None, None)
                    raise
            
            def __del__(self):
                if self._span_context and self._auto_exit:
                    try:
                        self.__exit__(None, None, None)
                    except:
                        pass
        
        return _OpenAIStreamWrapper(stream, self._tracer, model, message_data, start_time, self._base_url)


class _OpenAIChatWrapper:
    """Wrapper for OpenAI Chat API."""
    
    def __init__(self, original_chat, tracer: Optional[trace.Tracer], base_url: str):
        self._original = original_chat
        self._tracer = tracer
        self._base_url = base_url
    
    @property
    def completions(self):
        """Return wrapped completions API."""
        return _OpenAICompletionsWrapper(self._original.completions, self._tracer, self._base_url)
    
    def __getattr__(self, name):
        """Delegate any other attribute access to original chat."""
        return getattr(self._original, name)


class _OpenAIWrapper:
    """Wrapper for OpenAI client to track LLM calls."""
    
    def __init__(self, client, tracer: Optional[trace.Tracer]):
        self._client = client
        self._tracer = tracer
        self._base_url = str(getattr(client, '_base_url', ''))
        
        for attr in dir(client):
            if not attr.startswith("_") and attr != "chat":
                try:
                    setattr(self, attr, getattr(client, attr))
                except:
                    pass
    
    @property
    def chat(self):
        """Return wrapped chat API."""
        return _OpenAIChatWrapper(self._client.chat, self._tracer, self._base_url)
    
    def __getattr__(self, name):
        """Delegate any other attribute access to original client."""
        return getattr(self._client, name)


def wrap_llm_client(client: Any) -> Any:
    """
    Wrap an LLM client to automatically track all LLM calls.
    
    Currently supports:
    - Anthropic client (sync)
    - OpenAI client (sync, including OpenRouter and other OpenAI-compatible APIs)
    
    Args:
        client: The LLM client instance to wrap
        
    Returns:
        Wrapped client that behaves identically but creates spans for LLM calls
        
    Examples:
        ```python
        # Anthropic
        from anthropic import Anthropic
        from cascade.llm_wrappers import wrap_llm_client
        
        client = wrap_llm_client(Anthropic(api_key="..."))
        response = client.messages.create(...)  # Automatically traced!
        
        # OpenAI
        from openai import OpenAI
        
        client = wrap_llm_client(OpenAI(api_key="..."))
        response = client.chat.completions.create(...)  # Automatically traced!
        
        # OpenRouter
        client = wrap_llm_client(OpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key="..."
        ))
        response = client.chat.completions.create(...)  # Automatically traced!
        ```
    """
    tracer = get_tracer()
    
    client_type = type(client).__name__
    client_module = type(client).__module__
    
    if "openai" in client_module.lower() or "OpenAI" in client_type:
        logger.info(f"Wrapping OpenAI client: {client_module}.{client_type}")
        return _OpenAIWrapper(client, tracer)
    
    if hasattr(client, "chat"):
        chat_obj = getattr(client, "chat")
        if hasattr(chat_obj, "completions"):
            logger.info(f"Detected OpenAI-compatible client by API structure: {client_module}.{client_type}")
            return _OpenAIWrapper(client, tracer)
    
    if "anthropic" in client_module.lower() or "Anthropic" in client_type:
        logger.info(f"Wrapping Anthropic client: {client_module}.{client_type}")
        return _AnthropicWrapper(client, tracer)
    
    if hasattr(client, "messages"):
        messages_obj = getattr(client, "messages")
        if hasattr(messages_obj, "create") and hasattr(messages_obj, "stream"):
            logger.info(f"Detected Anthropic-compatible client by API structure: {client_module}.{client_type}")
            return _AnthropicWrapper(client, tracer)
    
    logger.warning(
        f"Unknown LLM client type: {client_module}.{client_type}. "
        "Returning unwrapped client. LLM calls will not be traced."
    )
    return client
