"""AgentBill SDK Client"""
import time
import json
import hashlib
import httpx
from typing import Any, Optional, Dict
from .tracer import AgentBillTracer
from .types import AgentBillConfig
from .http_client import AgentBillHTTPClient
from .resources.customers import CustomersResource
from .resources.agents import AgentsResource
from .resources.contacts import ContactsResource
from .resources.orders import OrdersResource
from .resources.signal_types import SignalTypesResource
from .resources.signals_resource import SignalsResource
from .resources.orders import OrdersResource

# Cost calculation is now 100% server-side - SDK only sends tokens


class DictToObject:
    """
    v7.6.6: Wrapper class to allow attribute-style access to dict data.
    Used to wrap cached responses so they behave like OpenAI ChatCompletion objects.
    
    Example:
        cached = {"choices": [{"message": {"content": "Hello"}}]}
        obj = DictToObject(cached)
        print(obj.choices[0].message.content)  # "Hello"
    """
    def __init__(self, data: dict):
        self._data = data
        for key, value in data.items():
            if isinstance(value, dict):
                setattr(self, key, DictToObject(value))
            elif isinstance(value, list):
                setattr(self, key, [
                    DictToObject(item) if isinstance(item, dict) else item 
                    for item in value
                ])
            else:
                setattr(self, key, value)
    
    def __getitem__(self, key):
        return self._data[key]
    
    def __repr__(self):
        return f"DictToObject({self._data})"
    
    def to_dict(self) -> dict:
        """Return the underlying dict"""
        return self._data


class AgentBill:
    """
    AgentBill SDK for Python
    
    Example:
        >>> from agentbill import AgentBill
        >>> import openai
        >>> 
        >>> # Initialize AgentBill
        >>> agentbill = AgentBill.init({
        ...     "api_key": "your-api-key",
        ...     "customer_id": "customer-123",
        ...     "debug": True
        ... })
        >>> 
        >>> # Wrap your OpenAI client
        >>> client = agentbill.wrap_openai(openai.OpenAI(api_key="sk-..."))
        >>> 
        >>> # Use normally - all calls are tracked!
        >>> response = client.chat.completions.create(
        ...     model="gpt-4",
        ...     messages=[{"role": "user", "content": "Hello!"}]
        ... )
        >>>
        >>> # Business Objects CRUD (v7.5.0+)
        >>> # Customers
        >>> customers = agentbill.customers.list(limit=10)
        >>> customer = agentbill.customers.create(name="Acme", email="a@b.com")
        >>>
        >>> # Agents with explicit signal type assignment
        >>> agent = agentbill.agents.create(name="Support Bot")
        >>> agentbill.agents.assign_signal_types(
        ...     agent_id=agent["id"],
        ...     signal_type_names=["ai_call", "completion"]
        ... )
    """
    
    def __init__(self, config: AgentBillConfig):
        self.config = config
        self.tracer = AgentBillTracer(config)
        
        # v10.2.0: Unified HTTP client — shared across all resources
        base_url = config.get("base_url", "https://api.agentbill.io")
        api_key = config["api_key"]
        self.http = AgentBillHTTPClient(base_url=base_url, api_key=api_key)
        
        # Business Object Resources (v7.5.0+) — now use shared HTTP client
        self.customers = CustomersResource(config, http_client=self.http)
        self.agents = AgentsResource(config, http_client=self.http)
        self.contacts = ContactsResource(config, http_client=self.http)
        self.orders = OrdersResource(config, http_client=self.http)
        self.signal_types = SignalTypesResource(config, http_client=self.http)
        self.signals = SignalsResource(config, http_client=self.http)
    
    @classmethod
    def init(cls, config: AgentBillConfig) -> "AgentBill":
        """Initialize AgentBill SDK"""
        return cls(config)
    
    def request(self, method: str, path: str, **kwargs):
        """Raw HTTP escape hatch — call any endpoint with shared auth, retries, and error mapping.
        
        Args:
            method: HTTP method (GET, POST, PUT, PATCH, DELETE)
            path: API path (e.g. "/functions/v1/custom-endpoint")
            **kwargs: Passed to AgentBillHTTPClient.request() (json=, params=, headers=, timeout=)
            
        Returns:
            AgentBillResponse with .status, .headers, .data, .ok
            
        Example:
            >>> resp = client.request("GET", "/v1/custom-endpoint", params={"foo": "bar"})
            >>> print(resp.status, resp.data)
        """
        return self.http.request(method, path, **kwargs)
    
    def _estimate_tokens(self, text: str) -> int:
        """Simple token estimation: ~4 chars per token"""
        return max(1, len(str(text)) // 4)
    
    # v9.1.0: _estimate_cost() REMOVED. Cost calculation is 100% server-side
    # via model_pricing table. SDKs only send token counts in OTEL spans.
    
    def _validate_request(self, model: str, messages: Any, estimated_output_tokens: int = 1000) -> Dict:
        """
        Call ai-cost-guard-router edge function (tier-based routing)
        
        v7.3.0 CRITICAL FIX: ALWAYS call validation - server decides based on:
        - Company budgets (via api_key)
        - Customer budgets (if customer_id provided)
        - Agent budgets (if agent_id provided)
        
        SDK budgets (daily_budget, monthly_budget) are OPTIONAL overrides that add
        stricter limits on top of DB policies - they are NOT required for validation.
        """
        # v7.3.0: ALWAYS validate - removed customer_id check
        # Server-side validation uses api_key to enforce company budgets even without customer_id
        
        url = f"{self.config.get('base_url', 'https://api.agentbill.io')}/functions/v1/ai-cost-guard-router"
        
        payload = {
            "api_key": self.config["api_key"],
            "customer_id": self.config.get("customer_id"),
            "model": model,
            "messages": messages,
            # SDK budgets are OPTIONAL overrides - backend uses DB budgets if not set
            "daily_budget_override": self.config.get("daily_budget"),
            "monthly_budget_override": self.config.get("monthly_budget"),
        }
        
        try:
            with httpx.Client(timeout=10) as client:
                response = client.post(url, json=payload)
                
                # Handle HTTP errors explicitly - don't fail open on 4xx/5xx
                if response.status_code >= 500:
                    error_text = response.text
                    if self.config.get("debug"):
                        print(f"[AgentBill Cost Guard] ❌ Router error {response.status_code}: {error_text}")
                    # Only fail open on server errors (temporary issues)
                    return {"allowed": True, "reason": "Router server error (failed open)"}
                elif response.status_code >= 400:
                    error_text = response.text
                    if self.config.get("debug"):
                        print(f"[AgentBill Cost Guard] ❌ Router rejected {response.status_code}: {error_text}")
                    # Fail closed on client errors (bad request, auth issues)
                    return {"allowed": False, "reason": f"Router error: {error_text}"}
                
                result = response.json()
                
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] Router response: {result}")
                    if result.get("tier"):
                        print(f"[AgentBill] Tier: {result['tier']}, Mode: {result.get('mode', 'unknown')}")
                    if not result.get("allowed"):
                        print(f"[AgentBill Cost Guard] ❌ BLOCKED: {result.get('reason', 'Unknown reason')}")
                
                return result
        except Exception as e:
            if self.config.get("debug"):
                print(f"[AgentBill Cost Guard] ⚠️ Router network error: {e} (failing open)")
            # Only fail open on network errors (temporary issues)
            return {"allowed": True, "reason": "Router network error (failed open)"}
    
    # v7.17.3: _track_usage() method DELETED - all tracking now goes through OTEL spans
    # The wrap_* methods use self.tracer.start_span() + span.end() + self.tracer.flush_sync()
    
    def _cache_response(self, model: str, prompt_hash: str, response_content: str, 
                        input_tokens: int, output_tokens: int, cost: float, prompt_content: str = None):
        """Cache AI response for semantic caching (v7.15.2: now with separate token counts)"""
        if not response_content:
            return
            
        url = f"{self.config.get('base_url', 'https://api.agentbill.io')}/functions/v1/cache-ai-response"
        
        # v7.15.2: Send prompt_tokens and completion_tokens separately for accurate cost calculation
        payload = {
            "api_key": self.config["api_key"],
            "prompt_hash": prompt_hash,
            "response_content": response_content,
            "model": model,
            "tokens_used": input_tokens + output_tokens,
            "prompt_tokens": input_tokens,
            "completion_tokens": output_tokens,
            "cost": cost,
            "prompt_content": prompt_content,
            "customer_id": self.config.get("customer_id"),
            "cacheable": True,
            "ttl_hours": 24
        }
        
        try:
            with httpx.Client(timeout=5) as client:
                client.post(url, json=payload)
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ Response cached (hash: {prompt_hash[:16]}...)")
        except Exception as e:
            if self.config.get("debug"):
                print(f"[AgentBill] Cache storage failed (non-blocking): {e}")
    
    def wrap_openai(self, client: Any) -> Any:
        """Wrap OpenAI client with Cost Guard protection"""
        
        # Track chat completions
        original_chat_create = client.chat.completions.create
        def tracked_chat_create(*args, **kwargs):
            model = kwargs.get("model", "unknown")
            messages = kwargs.get("messages", [])
            temperature = kwargs.get("temperature")
            max_tokens = kwargs.get("max_tokens", kwargs.get("max_completion_tokens"))
            
            # Extract event_name from agentbill_options if provided (don't pass to OpenAI)
            agentbill_options = kwargs.pop("agentbill_options", {})
            event_name = agentbill_options.get("event_name", "ai_request")
            
            # Extract prompt metadata for AI Economics analysis
            prompt_metadata = {
                "prompt_hash": agentbill_options.get("prompt_hash"),
                "prompt_name": agentbill_options.get("prompt_name"),
                "prompt_version": agentbill_options.get("prompt_version"),
                "prompt_owner": agentbill_options.get("prompt_owner"),
                "prompt_sample": agentbill_options.get("prompt_sample"),
            }
            # Remove None values
            prompt_metadata = {k: v for k, v in prompt_metadata.items() if v is not None}
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting OpenAI chat.completions.create with model: {model}")
            
            start_time = time.time()
            span = self.tracer.start_span("openai.chat.completions.create", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "openai",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "chat",
                # Backward compatibility
                "model": model,
                "provider": "openai"
            })
            
            # CRITICAL: ALWAYS validate before AI calls (regardless of SDK budget config)
            # Backend checks company/customer/agent budgets from DB
            # SDK budgets are optional overrides that ADD stricter limits
            validation_request_id = None
            validation = self._validate_request(model, messages)
            
            # Extract request_id for audit log correlation
            validation_request_id = validation.get("request_id")
            
            # CRITICAL: Block request if not allowed
            if not validation.get("allowed"):
                error_msg = validation.get("reason", "Request blocked by Cost Guard")
                error = Exception(error_msg)
                error.code = "BUDGET_EXCEEDED" if "budget" in error_msg.lower() or "BUDGET" in error_msg else \
                             "RATE_LIMIT_EXCEEDED" if "rate" in error_msg.lower() or "RATE" in error_msg else \
                             "POLICY_VIOLATION"
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ❌ Request BLOCKED: {error_msg}")
                span.set_status(1, str(error))
                span.end()
                raise error
            
            # v7.6.8: Check for cached response from semantic cache
            # The response_data is a full OpenAI ChatCompletion-compatible object
            if validation.get("cached") and validation.get("response_data"):
                if self.config.get("debug"):
                    print("[AgentBill] ✓ Cache hit - returning cached ChatCompletion object")
                    print(f"[AgentBill] Cached response data: {validation.get('response_data')}")
                
                # v7.6.8 FIX: Router sends "cache" not "cache_info"
                cache_info = validation.get("cache", {}) or validation.get("cache_info", {})
                cached_response = validation.get("response_data", {})
                
                # Get tokens_saved from cache info or calculate from response
                tokens_saved = cache_info.get("tokens_saved", 0) or validation.get("tokens_saved", 0)
                cost_saved = cache_info.get("cost_saved", 0) or validation.get("cost_saved", 0)
                usage = cached_response.get("usage", {}) if isinstance(cached_response, dict) else {}
                
                input_tokens = usage.get("prompt_tokens", 0)
                output_tokens = usage.get("completion_tokens", 0)
                total_tokens = usage.get("total_tokens", tokens_saved)
                
                # If no tokens_saved recorded, use total_tokens as saved amount
                if tokens_saved == 0 and total_tokens > 0:
                    tokens_saved = total_tokens
                
                # v7.6.8 FIX: For cache hits, set agentbill.* metrics for SAVED amounts
                # DO NOT set gen_ai.usage.* for cache hits - those indicate CONSUMED tokens
                # OTEL ingestion will calculate cost=0 when agentbill.cache_hit=True
                span.set_attributes({
                    "agentbill.cache_hit": True,
                    "agentbill.from_cache": True,  # Explicit flag for OTEL ingestion
                    "agentbill.tokens_saved": tokens_saved,
                    "agentbill.cost_saved": cost_saved,
                    "agentbill.cached_input_tokens": input_tokens,   # Informational only
                    "agentbill.cached_output_tokens": output_tokens, # Informational only
                    "agentbill.cached_total_tokens": total_tokens,   # Informational only
                    # v7.6.8: Set actual usage to 0 for cache hits (no tokens consumed)
                    "gen_ai.usage.input_tokens": 0,
                    "gen_ai.usage.output_tokens": 0,
                    "gen_ai.usage.total_tokens": 0,
                })
                span.set_status(0)
                span.end()
                self.tracer.flush_sync()
                
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ Returning cached response (saved {tokens_saved} tokens, ${cost_saved:.6f})")
                
                # v7.6.6 FIX: Wrap dict in DictToObject for attribute-style access
                # This allows response.choices[0].message.content to work
                if isinstance(cached_response, dict):
                    cached_response["_agentbill_cached"] = True
                    return DictToObject(cached_response)
                return cached_response
            
            # Validation passed - proceed with OpenAI call
            if self.config.get("debug"):
                print(f"[AgentBill Cost Guard] ✓ Validation passed: {validation.get('mode', 'validated')}")
                if validation_request_id:
                    print(f"[AgentBill Cost Guard] request_id: {validation_request_id}")
            
            # Step 2: Execute OpenAI call (after validation passed or no customer_id set)
            try:
                response = original_chat_create(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # Step 3: Track usage after successful AI call (with OTEL + Cost Guard correlation)
                input_tokens = response.usage.prompt_tokens
                output_tokens = response.usage.completion_tokens
                # v9.1.0: Cost calculated server-side from token counts in span
                
                # NOTE: wrap_openai() only creates OTEL spans, NOT signals
                # Signals should only be created when signal() is called explicitly
                # within the trace context. This ensures OTEL spans are "untracked"
                # until the user explicitly tracks business events via signal()
                
                span.set_attributes({
                    "gen_ai.usage.input_tokens": input_tokens,
                    "gen_ai.usage.output_tokens": output_tokens,
                    "gen_ai.usage.total_tokens": response.usage.total_tokens,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ OpenAI call: {input_tokens}in/{output_tokens}out tokens (cost calculated server-side)")
                
                # v7.6.6: Cache AI response for semantic caching (with prompt_content)
                try:
                    response_content = response.choices[0].message.content if response.choices else ""
                    prompt_text = json.dumps(messages, separators=(',', ':'), default=str)
                    prompt_hash = hashlib.sha256(prompt_text.encode()).hexdigest()
                    # v7.15.2: Pass separate token counts for accurate cost calculation
                    self._cache_response(model, prompt_hash, response_content, input_tokens, output_tokens, 0, prompt_text)
                except Exception as cache_err:
                    if self.config.get("debug"):
                        print(f"[AgentBill] Cache population failed: {cache_err}")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss on script exit
                self.tracer.flush_sync()
        
        client.chat.completions.create = tracked_chat_create
        
        # Track embeddings
        original_embeddings_create = client.embeddings.create
        def tracked_embeddings_create(*args, **kwargs):
            model = kwargs.get("model", "text-embedding-ada-002")
            
            # Extract event_name from agentbill_options if provided
            agentbill_options = kwargs.pop("agentbill_options", {})
            event_name = agentbill_options.get("event_name", "embedding_request")
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting OpenAI embeddings.create with model: {model}")
            
            start_time = time.time()
            span = self.tracer.start_span("openai.embeddings.create", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "openai",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "text_embedding",
                # Backward compatibility
                "model": model,
                "provider": "openai"
            })
            
            try:
                response = original_embeddings_create(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # NOTE: wrap_openai() only creates OTEL spans, NOT signals
                input_tokens = response.usage.prompt_tokens
                # v9.1.0: Cost calculated server-side from token counts in span
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes
                    "gen_ai.usage.input_tokens": input_tokens,
                    "gen_ai.usage.total_tokens": response.usage.total_tokens,
                    # Backward compatibility
                    "response.prompt_tokens": input_tokens,
                    "response.total_tokens": response.usage.total_tokens,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ Embedding tracked: {input_tokens} tokens (cost calculated server-side)")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss on script exit
                self.tracer.flush_sync()
        
        client.embeddings.create = tracked_embeddings_create
        
        # Track image generation
        original_images_generate = client.images.generate
        def tracked_images_generate(*args, **kwargs):
            model = kwargs.get("model", "dall-e-3")
            size = kwargs.get("size", "1024x1024")
            quality = kwargs.get("quality", "standard")
            n = kwargs.get("n", 1)  # Number of images
            
            # Extract event_name from agentbill_options if provided
            agentbill_options = kwargs.pop("agentbill_options", {})
            event_name = agentbill_options.get("event_name", "image_generation")
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting OpenAI images.generate: {model} {size} {quality}")
            
            start_time = time.time()
            span = self.tracer.start_span("openai.images.generate", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "openai",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "image_generation",
                # Backward compatibility
                "model": model,
                "provider": "openai",
                "size": size,
                "quality": quality
            })
            
            try:
                response = original_images_generate(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # NOTE: wrap_openai() only creates OTEL spans, NOT signals
                # Cost calculation is 100% SERVER-SIDE using model_pricing table
                # SDK sends metadata only - server calculates actual cost
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes for image generation
                    "gen_ai.request.image_size": size,
                    "gen_ai.request.image_quality": quality,
                    "gen_ai.request.image_count": n,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ✓ Image generation tracked: {model} {size} {quality} ({n} images)")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss on script exit
                self.tracer.flush_sync()
        
        client.images.generate = tracked_images_generate
        
        # Track audio transcription (Whisper)
        original_audio_transcriptions_create = client.audio.transcriptions.create
        def tracked_audio_transcriptions_create(*args, **kwargs):
            model = kwargs.get("model", "whisper-1")
            
            # Extract event_name from agentbill_options if provided
            agentbill_options = kwargs.pop("agentbill_options", {})
            event_name = agentbill_options.get("event_name", "audio_transcription")
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting OpenAI audio.transcriptions.create")
            
            start_time = time.time()
            span = self.tracer.start_span("openai.audio.transcriptions.create", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "openai",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "audio_transcription",
                # Backward compatibility
                "model": model,
                "provider": "openai"
            })
            
            try:
                response = original_audio_transcriptions_create(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # NOTE: wrap_openai() only creates OTEL spans, NOT signals
                # Cost calculation is 100% SERVER-SIDE using model_pricing table
                # SDK sends metadata only - server calculates actual cost
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes for audio transcription
                    "gen_ai.audio.estimated_duration_ms": latency,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ✓ Audio transcription OTEL span: ${cost:.6f}")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss on script exit
                self.tracer.flush_sync()
        
        client.audio.transcriptions.create = tracked_audio_transcriptions_create
        original_audio_speech_create = client.audio.speech.create
        def tracked_audio_speech_create(*args, **kwargs):
            model = kwargs.get("model", "tts-1")
            input_text = kwargs.get("input", "")
            voice = kwargs.get("voice", "alloy")
            
            # Extract event_name from agentbill_options if provided
            agentbill_options = kwargs.pop("agentbill_options", {})
            event_name = agentbill_options.get("event_name", "text_to_speech")
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting OpenAI audio.speech.create: {model}")
            
            start_time = time.time()
            span = self.tracer.start_span("openai.audio.speech.create", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "openai",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "audio_speech",
                # Backward compatibility
                "model": model,
                "provider": "openai",
                "voice": voice
            })
            
            try:
                response = original_audio_speech_create(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # NOTE: wrap_openai() only creates OTEL spans, NOT signals
                # Cost calculation is 100% SERVER-SIDE using model_pricing table
                # SDK sends metadata only - server calculates actual cost
                chars = len(input_text)
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes for text-to-speech
                    "gen_ai.tts.character_count": chars,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ✓ TTS OTEL span: ${cost:.6f} ({chars} chars)")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss on script exit
                self.tracer.flush_sync()
        
        client.audio.speech.create = tracked_audio_speech_create
        
        # Track moderations
        original_moderations_create = client.moderations.create
        def tracked_moderations_create(*args, **kwargs):
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting OpenAI moderations.create")
            
            start_time = time.time()
            span = self.tracer.start_span("openai.moderations.create", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "openai",
                "gen_ai.request.model": kwargs.get("model", "text-moderation-latest"),
                "gen_ai.operation.name": "moderation",
                # Backward compatibility
                "model": kwargs.get("model", "text-moderation-latest"),
                "provider": "openai"
            })
            
            try:
                response = original_moderations_create(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                span.set_attributes({"latency_ms": latency})
                span.set_status(0)
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
        
        client.moderations.create = tracked_moderations_create
        
        return client
    
    def wrap_anthropic(self, client: Any) -> Any:
        """Wrap Anthropic client with Cost Guard protection + semantic caching"""
        original_create = client.messages.create
        
        def tracked_create(*args, **kwargs):
            model = kwargs.get("model", "unknown")
            messages = kwargs.get("messages", [])
            max_tokens = kwargs.get("max_tokens", 1000)
            
            # Extract event_name from agentbill_options if provided (don't pass to Anthropic)
            agentbill_options = kwargs.pop("agentbill_options", {})
            event_name = agentbill_options.get("event_name", "ai_request")
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting Anthropic messages.create with model: {model}")
            
            start_time = time.time()
            span = self.tracer.start_span("anthropic.messages.create", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "anthropic",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "chat",
                # Backward compatibility
                "model": model,
                "provider": "anthropic"
            })
            
            # Phase 1: Validate budget BEFORE API call (includes cache lookup)
            validation = self._validate_request(model, messages, max_tokens)
            
            # CRITICAL: Block request if not allowed
            if not validation.get("allowed"):
                error_msg = validation.get("reason", "Request blocked by Cost Guard")
                error = Exception(error_msg)
                error.code = "BUDGET_EXCEEDED" if "budget" in error_msg.lower() else "POLICY_VIOLATION"
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ❌ Request BLOCKED: {error_msg}")
                span.set_status(1, str(error))
                span.end()
                raise error
            
            # v8.7.0: Check for cached response from semantic cache
            if validation.get("cached") and validation.get("response_data"):
                if self.config.get("debug"):
                    print("[AgentBill] ✓ Cache hit - returning cached response")
                
                cache_info = validation.get("cache", {}) or validation.get("cache_info", {})
                cached_response = validation.get("response_data", {})
                tokens_saved = cache_info.get("tokens_saved", 0) or validation.get("tokens_saved", 0)
                cost_saved = cache_info.get("cost_saved", 0) or validation.get("cost_saved", 0)
                
                span.set_attributes({
                    "agentbill.cache_hit": True,
                    "agentbill.from_cache": True,
                    "agentbill.tokens_saved": tokens_saved,
                    "agentbill.cost_saved": cost_saved,
                    "gen_ai.usage.input_tokens": 0,
                    "gen_ai.usage.output_tokens": 0,
                })
                span.set_status(0)
                span.end()
                self.tracer.flush_sync()
                
                if isinstance(cached_response, dict):
                    cached_response["_agentbill_cached"] = True
                    return DictToObject(cached_response)
                return cached_response
            
            # Phase 2: Execute AI call
            try:
                response = original_create(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # Phase 3: Track actual usage via OTEL span (no _track_usage call)
                input_tokens = response.usage.input_tokens
                output_tokens = response.usage.output_tokens
                # v9.1.0: Cost calculated server-side from token counts in span
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes
                    "gen_ai.usage.input_tokens": input_tokens,
                    "gen_ai.usage.output_tokens": output_tokens,
                    "gen_ai.response.id": response.id if hasattr(response, 'id') else None,
                    "gen_ai.response.finish_reasons": [response.stop_reason] if hasattr(response, 'stop_reason') else [],
                    # Backward compatibility
                    "response.input_tokens": input_tokens,
                    "response.output_tokens": output_tokens,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ Anthropic call: {input_tokens}in/{output_tokens}out tokens (cost calculated server-side)")
                
                # v8.7.0: Cache AI response for semantic caching
                try:
                    response_content = response.content[0].text if response.content else ""
                    prompt_text = json.dumps(messages, separators=(',', ':'), default=str)
                    prompt_hash = hashlib.sha256(prompt_text.encode()).hexdigest()
                    self._cache_response(model, prompt_hash, response_content, input_tokens, output_tokens, 0, prompt_text)
                except Exception as cache_err:
                    if self.config.get("debug"):
                        print(f"[AgentBill] Cache population failed: {cache_err}")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss
                self.tracer.flush_sync()
        
        client.messages.create = tracked_create
        return client
    
    def wrap_bedrock(self, client: Any) -> Any:
        """Wrap AWS Bedrock client with Cost Guard protection + semantic caching"""
        original_invoke_model = client.invoke_model
        
        def tracked_invoke_model(*args, **kwargs):
            model = kwargs.get("modelId", "unknown")
            body_str = kwargs.get("body", "{}")
            
            # Parse body for messages
            try:
                body = json.loads(body_str)
                messages = body.get("messages", body.get("prompt", ""))
                max_tokens = body.get("max_tokens", body.get("max_tokens_to_sample", 1000))
            except:
                messages = ""
                max_tokens = 1000
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting Bedrock invoke_model with model: {model}")
            
            start_time = time.time()
            span = self.tracer.start_span("bedrock.invoke_model", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "aws.bedrock",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "chat",
                # Backward compatibility
                "model": model,
                "provider": "bedrock"
            })
            
            # Phase 1: Validate budget BEFORE API call (includes cache lookup)
            validation = self._validate_request(model, messages, max_tokens)
            
            # CRITICAL: Block request if not allowed
            if not validation.get("allowed"):
                error_msg = validation.get("reason", "Request blocked by Cost Guard")
                error = Exception(error_msg)
                error.code = "BUDGET_EXCEEDED" if "budget" in error_msg.lower() else "POLICY_VIOLATION"
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ❌ Request BLOCKED: {error_msg}")
                span.set_status(1, str(error))
                span.end()
                raise error
            
            # v8.7.0: Check for cached response from semantic cache
            if validation.get("cached") and validation.get("response_data"):
                if self.config.get("debug"):
                    print("[AgentBill] ✓ Cache hit - returning cached response")
                
                cache_info = validation.get("cache", {}) or validation.get("cache_info", {})
                cached_response = validation.get("response_data", {})
                tokens_saved = cache_info.get("tokens_saved", 0) or validation.get("tokens_saved", 0)
                cost_saved = cache_info.get("cost_saved", 0) or validation.get("cost_saved", 0)
                
                span.set_attributes({
                    "agentbill.cache_hit": True,
                    "agentbill.from_cache": True,
                    "agentbill.tokens_saved": tokens_saved,
                    "agentbill.cost_saved": cost_saved,
                    "gen_ai.usage.input_tokens": 0,
                    "gen_ai.usage.output_tokens": 0,
                })
                span.set_status(0)
                span.end()
                self.tracer.flush_sync()
                
                if isinstance(cached_response, dict):
                    cached_response["_agentbill_cached"] = True
                    return DictToObject(cached_response)
                return cached_response
            
            # Phase 2: Execute AI call
            try:
                response = original_invoke_model(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # Parse response body for token usage
                response_body = json.loads(response['body'].read())
                
                # Handle different Bedrock model response formats
                input_tokens = 0
                output_tokens = 0
                response_content = ""
                if 'usage' in response_body:  # Claude models
                    input_tokens = response_body['usage'].get('input_tokens', 0)
                    output_tokens = response_body['usage'].get('output_tokens', 0)
                    if 'content' in response_body and response_body['content']:
                        response_content = response_body['content'][0].get('text', '')
                elif 'inputTextTokenCount' in response_body:  # Titan models
                    input_tokens = response_body.get('inputTextTokenCount', 0)
                    output_tokens = response_body['results'][0].get('tokenCount', 0) if 'results' in response_body else 0
                    if 'results' in response_body and response_body['results']:
                        response_content = response_body['results'][0].get('outputText', '')
                
                # Phase 3: Track actual usage via OTEL span (no _track_usage call)
                # v9.1.0: Cost calculated server-side from token counts in span
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes
                    "gen_ai.usage.input_tokens": input_tokens,
                    "gen_ai.usage.output_tokens": output_tokens,
                    # Backward compatibility
                    "response.input_tokens": input_tokens,
                    "response.output_tokens": output_tokens,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ Bedrock call: {input_tokens}in/{output_tokens}out tokens (cost calculated server-side)")
                
                # v8.7.0: Cache AI response for semantic caching
                try:
                    prompt_text = json.dumps(messages, separators=(',', ':'), default=str) if isinstance(messages, (list, dict)) else str(messages)
                    prompt_hash = hashlib.sha256(prompt_text.encode()).hexdigest()
                    self._cache_response(model, prompt_hash, response_content, input_tokens, output_tokens, 0, prompt_text)
                except Exception as cache_err:
                    if self.config.get("debug"):
                        print(f"[AgentBill] Cache population failed: {cache_err}")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss
                self.tracer.flush_sync()
        
        client.invoke_model = tracked_invoke_model
        return client
    
    def wrap_azure_openai(self, client: Any) -> Any:
        """Wrap Azure OpenAI client with Cost Guard protection + semantic caching"""
        original_create = client.chat.completions.create
        
        def tracked_create(*args, **kwargs):
            model = kwargs.get("model", "unknown")
            messages = kwargs.get("messages", [])
            max_tokens = kwargs.get("max_tokens", 1000)
            
            # Extract event_name from agentbill_options if provided
            agentbill_options = kwargs.pop("agentbill_options", {})
            event_name = agentbill_options.get("event_name", "ai_request")
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting Azure OpenAI chat.completions.create with model: {model}")
            
            start_time = time.time()
            span = self.tracer.start_span("azure_openai.chat.completions.create", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "azure_openai",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "chat",
                # Backward compatibility
                "model": model,
                "provider": "azure_openai"
            })
            
            # Phase 1: Validate budget BEFORE API call (includes cache lookup)
            validation = self._validate_request(model, messages, max_tokens)
            
            # CRITICAL: Block request if not allowed
            if not validation.get("allowed"):
                error_msg = validation.get("reason", "Request blocked by Cost Guard")
                error = Exception(error_msg)
                error.code = "BUDGET_EXCEEDED" if "budget" in error_msg.lower() else "POLICY_VIOLATION"
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ❌ Request BLOCKED: {error_msg}")
                span.set_status(1, str(error))
                span.end()
                raise error
            
            # v8.7.0: Check for cached response from semantic cache
            if validation.get("cached") and validation.get("response_data"):
                if self.config.get("debug"):
                    print("[AgentBill] ✓ Cache hit - returning cached response")
                
                cache_info = validation.get("cache", {}) or validation.get("cache_info", {})
                cached_response = validation.get("response_data", {})
                tokens_saved = cache_info.get("tokens_saved", 0) or validation.get("tokens_saved", 0)
                cost_saved = cache_info.get("cost_saved", 0) or validation.get("cost_saved", 0)
                
                span.set_attributes({
                    "agentbill.cache_hit": True,
                    "agentbill.from_cache": True,
                    "agentbill.tokens_saved": tokens_saved,
                    "agentbill.cost_saved": cost_saved,
                    "gen_ai.usage.input_tokens": 0,
                    "gen_ai.usage.output_tokens": 0,
                })
                span.set_status(0)
                span.end()
                self.tracer.flush_sync()
                
                if isinstance(cached_response, dict):
                    cached_response["_agentbill_cached"] = True
                    return DictToObject(cached_response)
                return cached_response
            
            # Phase 2: Execute AI call
            try:
                response = original_create(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # Phase 3: Track actual usage via OTEL span (no _track_usage call)
                input_tokens = response.usage.prompt_tokens
                output_tokens = response.usage.completion_tokens
                # v9.1.0: Cost calculated server-side from token counts in span
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes
                    "gen_ai.usage.input_tokens": input_tokens,
                    "gen_ai.usage.output_tokens": output_tokens,
                    "gen_ai.usage.total_tokens": response.usage.total_tokens,
                    # Backward compatibility
                    "response.prompt_tokens": input_tokens,
                    "response.completion_tokens": output_tokens,
                    "response.total_tokens": response.usage.total_tokens,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ Cohere call: {input_tokens}in/{output_tokens}out tokens (cost calculated server-side)")
                
                # v8.7.0: Cache AI response for semantic caching
                try:
                    response_content = response.choices[0].message.content if response.choices else ""
                    prompt_text = json.dumps(messages, separators=(',', ':'), default=str)
                    prompt_hash = hashlib.sha256(prompt_text.encode()).hexdigest()
                    self._cache_response(model, prompt_hash, response_content, input_tokens, output_tokens, 0, prompt_text)
                except Exception as cache_err:
                    if self.config.get("debug"):
                        print(f"[AgentBill] Cache population failed: {cache_err}")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss
                self.tracer.flush_sync()
        
        client.chat.completions.create = tracked_create
        return client
    
    def wrap_mistral(self, client: Any) -> Any:
        """Wrap Mistral AI client with Cost Guard protection + semantic caching"""
        original_create = client.chat.complete
        
        def tracked_create(*args, **kwargs):
            model = kwargs.get("model", "unknown")
            messages = kwargs.get("messages", [])
            max_tokens = kwargs.get("max_tokens", 1000)
            
            # Extract event_name from agentbill_options if provided
            agentbill_options = kwargs.pop("agentbill_options", {})
            event_name = agentbill_options.get("event_name", "ai_request")
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting Mistral chat.complete with model: {model}")
            
            start_time = time.time()
            span = self.tracer.start_span("mistral.chat.complete", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "mistral",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "chat",
                # Backward compatibility
                "model": model,
                "provider": "mistral"
            })
            
            # Phase 1: Validate budget BEFORE API call (includes cache lookup)
            validation = self._validate_request(model, messages, max_tokens)
            
            # CRITICAL: Block request if not allowed
            if not validation.get("allowed"):
                error_msg = validation.get("reason", "Request blocked by Cost Guard")
                error = Exception(error_msg)
                error.code = "BUDGET_EXCEEDED" if "budget" in error_msg.lower() else "POLICY_VIOLATION"
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ❌ Request BLOCKED: {error_msg}")
                span.set_status(1, str(error))
                span.end()
                raise error
            
            # v8.7.0: Check for cached response from semantic cache
            if validation.get("cached") and validation.get("response_data"):
                if self.config.get("debug"):
                    print("[AgentBill] ✓ Cache hit - returning cached response")
                
                cache_info = validation.get("cache", {}) or validation.get("cache_info", {})
                cached_response = validation.get("response_data", {})
                tokens_saved = cache_info.get("tokens_saved", 0) or validation.get("tokens_saved", 0)
                cost_saved = cache_info.get("cost_saved", 0) or validation.get("cost_saved", 0)
                
                span.set_attributes({
                    "agentbill.cache_hit": True,
                    "agentbill.from_cache": True,
                    "agentbill.tokens_saved": tokens_saved,
                    "agentbill.cost_saved": cost_saved,
                    "gen_ai.usage.input_tokens": 0,
                    "gen_ai.usage.output_tokens": 0,
                })
                span.set_status(0)
                span.end()
                self.tracer.flush_sync()
                
                if isinstance(cached_response, dict):
                    cached_response["_agentbill_cached"] = True
                    return DictToObject(cached_response)
                return cached_response
            
            # Phase 2: Execute AI call
            try:
                response = original_create(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # Phase 3: Track actual usage via OTEL span (no _track_usage call)
                input_tokens = response.usage.prompt_tokens
                output_tokens = response.usage.completion_tokens
                # v9.1.0: Cost calculated server-side from token counts in span
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes
                    "gen_ai.usage.input_tokens": input_tokens,
                    "gen_ai.usage.output_tokens": output_tokens,
                    "gen_ai.usage.total_tokens": response.usage.total_tokens,
                    # Backward compatibility
                    "response.prompt_tokens": input_tokens,
                    "response.completion_tokens": output_tokens,
                    "response.total_tokens": response.usage.total_tokens,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ Perplexity call: {input_tokens}in/{output_tokens}out tokens (cost calculated server-side)")
                
                # v8.7.0: Cache AI response for semantic caching
                try:
                    response_content = response.choices[0].message.content if response.choices else ""
                    prompt_text = json.dumps(messages, separators=(',', ':'), default=str)
                    prompt_hash = hashlib.sha256(prompt_text.encode()).hexdigest()
                    self._cache_response(model, prompt_hash, response_content, input_tokens, output_tokens, 0, prompt_text)
                except Exception as cache_err:
                    if self.config.get("debug"):
                        print(f"[AgentBill] Cache population failed: {cache_err}")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss
                self.tracer.flush_sync()
        
        client.chat.complete = tracked_create
        return client
    
    def wrap_google_ai(self, client: Any) -> Any:
        """Wrap Google AI (Gemini) client with Cost Guard protection + semantic caching"""
        original_generate_content = client.generate_content
        
        def tracked_generate_content(*args, **kwargs):
            model = getattr(client, '_model_name', 'gemini-pro')
            content = args[0] if args else kwargs.get("contents", "")
            
            # Extract event_name from agentbill_options if provided
            agentbill_options = kwargs.pop("agentbill_options", {}) if "agentbill_options" in kwargs else {}
            event_name = agentbill_options.get("event_name", "ai_request")
            
            if self.config.get("debug"):
                print(f"[AgentBill] Intercepting Google AI generate_content with model: {model}")
            
            start_time = time.time()
            span = self.tracer.start_span("google_ai.generate_content", {
                # OTEL GenAI compliant attributes
                "gen_ai.system": "google_ai",
                "gen_ai.request.model": model,
                "gen_ai.operation.name": "chat",
                # Backward compatibility
                "model": model,
                "provider": "google_ai"
            })
            
            # Phase 1: Validate budget BEFORE API call (includes cache lookup)
            validation = self._validate_request(model, content, 1000)
            
            # CRITICAL: Block request if not allowed
            if not validation.get("allowed"):
                error_msg = validation.get("reason", "Request blocked by Cost Guard")
                error = Exception(error_msg)
                error.code = "BUDGET_EXCEEDED" if "budget" in error_msg.lower() else "POLICY_VIOLATION"
                if self.config.get("debug"):
                    print(f"[AgentBill Cost Guard] ❌ Request BLOCKED: {error_msg}")
                span.set_status(1, str(error))
                span.end()
                raise error
            
            # v8.7.0: Check for cached response from semantic cache
            if validation.get("cached") and validation.get("response_data"):
                if self.config.get("debug"):
                    print("[AgentBill] ✓ Cache hit - returning cached response")
                
                cache_info = validation.get("cache", {}) or validation.get("cache_info", {})
                cached_response = validation.get("response_data", {})
                tokens_saved = cache_info.get("tokens_saved", 0) or validation.get("tokens_saved", 0)
                cost_saved = cache_info.get("cost_saved", 0) or validation.get("cost_saved", 0)
                
                span.set_attributes({
                    "agentbill.cache_hit": True,
                    "agentbill.from_cache": True,
                    "agentbill.tokens_saved": tokens_saved,
                    "agentbill.cost_saved": cost_saved,
                    "gen_ai.usage.input_tokens": 0,
                    "gen_ai.usage.output_tokens": 0,
                })
                span.set_status(0)
                span.end()
                self.tracer.flush_sync()
                
                if isinstance(cached_response, dict):
                    cached_response["_agentbill_cached"] = True
                    return DictToObject(cached_response)
                return cached_response
            
            # Phase 2: Execute AI call
            try:
                response = original_generate_content(*args, **kwargs)
                latency = (time.time() - start_time) * 1000
                
                # Phase 3: Track actual usage via OTEL span (no _track_usage call)
                input_tokens = 0
                output_tokens = 0
                response_content = ""
                if hasattr(response, 'usage_metadata'):
                    input_tokens = response.usage_metadata.prompt_token_count
                    output_tokens = response.usage_metadata.candidates_token_count
                if hasattr(response, 'text'):
                    response_content = response.text
                
                # v9.1.0: Cost calculated server-side from token counts in span
                
                span.set_attributes({
                    # OTEL GenAI compliant attributes
                    "gen_ai.usage.input_tokens": input_tokens,
                    "gen_ai.usage.output_tokens": output_tokens,
                    "gen_ai.usage.total_tokens": response.usage_metadata.total_token_count if hasattr(response, 'usage_metadata') else 0,
                    # Backward compatibility
                    "response.prompt_tokens": input_tokens,
                    "response.completion_tokens": output_tokens,
                    "latency_ms": latency
                })
                span.set_status(0)
                
                if self.config.get("debug"):
                    print(f"[AgentBill] ✓ Google AI call: {input_tokens}in/{output_tokens}out tokens (cost calculated server-side)")
                
                # v8.7.0: Cache AI response for semantic caching
                try:
                    prompt_text = json.dumps(content, separators=(',', ':'), default=str) if isinstance(content, (list, dict)) else str(content)
                    prompt_hash = hashlib.sha256(prompt_text.encode()).hexdigest()
                    self._cache_response(model, prompt_hash, response_content, input_tokens, output_tokens, 0, prompt_text)
                except Exception as cache_err:
                    if self.config.get("debug"):
                        print(f"[AgentBill] Cache population failed: {cache_err}")
                
                return response
            except Exception as e:
                span.set_status(1, str(e))
                raise
            finally:
                span.end()
                # Auto-flush spans to prevent data loss
                self.tracer.flush_sync()
        
        client.generate_content = tracked_generate_content
        return client
    
    def wrap_perplexity(self, client: Any) -> Any:
        """Wrap Perplexity client with Cost Guard protection"""
        from .perplexity_wrapper import wrap_perplexity
        return wrap_perplexity(self, client)
    
    def wrap_ollama(self, client: Any) -> Any:
        """Wrap Ollama client with usage tracking (local, no API costs)"""
        from .ollama_wrapper import wrap_ollama
        return wrap_ollama(self, client)
    
    # track_signal() REMOVED in v9.0.0 (hard-removed in v10.0.0). Use agentbill.signal() instead.

    
    def track_conversion(self, event_type: str, event_value: float, signal_id: Optional[str] = None, 
                         session_id: Optional[str] = None, attribution_window_hours: int = 24,
                         currency: str = "USD", metadata: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Track a conversion event for prompt profitability analysis.
        
        v8.0.0: Migrated from /track-conversion to /otel-collector (unified OTEL model).
        
        Args:
            event_type: Type of conversion (e.g., 'purchase', 'signup')
            event_value: Revenue amount from the conversion
            signal_id: Optional UUID linking to specific AI signal/prompt
            session_id: Optional session identifier
            attribution_window_hours: Time window for attribution (default: 24 hours)
            currency: Currency code (default: 'USD')
            metadata: Optional additional data
            
        Returns:
            Dict with success status and trace/span IDs
        """
        from .signal_builder import _emit_signal
        
        conversion_metadata = metadata or {}
        if signal_id:
            conversion_metadata["signal_id"] = signal_id
        if attribution_window_hours != 24:
            conversion_metadata["attribution_window_hours"] = attribution_window_hours
        
        return _emit_signal(
            event_name=f"conversion_{event_type}",
            config=self.config,
            revenue=event_value,
            event_type=event_type,
            event_value=event_value,
            currency=currency,
            session_id=session_id,
            metadata=conversion_metadata if conversion_metadata else None,
        )
    
    async def flush(self):
        """Flush pending telemetry data"""
        await self.tracer.flush()
