"""Utilities for safe span attribute handling."""

import logging
from typing import Any, Dict
from opentelemetry.trace import Span

logger = logging.getLogger(__name__)


def safe_set_span_attribute(span: Span, key: str, value: Any) -> None:
    """
    Safely set an attribute on a span, filtering out None values and invalid types.

    Args:
        span: The span to set the attribute on
        key: The attribute key
        value: The attribute value
    """
    if value is None:
        logger.debug(f"Skipping None value for span attribute '{key}'")
        return

    # Special handling for gen_ai.response.model to prevent OpenTelemetry errors
    # Never set this to None - it causes encoding errors in OpenTelemetry exporters
    if key == "gen_ai.response.model":
        if value is None:
            logger.debug(
                "Skipping None value for gen_ai.response.model to prevent OpenTelemetry encoding errors"
            )
            return
        # Ensure it's a string
        if not isinstance(value, str):
            value = str(value) if value is not None else None
            if value is None:
                logger.debug(
                    "Skipping None value for gen_ai.response.model after conversion"
                )
                return

    # OpenTelemetry only accepts certain types for attribute values
    # Valid types: bool, str, bytes, int, float, or sequences of these
    if isinstance(value, (bool, str, bytes, int, float)):
        span.set_attribute(key, value)
    elif isinstance(value, (list, tuple)):
        # For sequences, ensure all elements are valid types
        try:
            valid_sequence = []
            for item in value:
                if item is not None and isinstance(
                    item, (bool, str, bytes, int, float)
                ):
                    valid_sequence.append(item)
                elif item is not None:
                    # Convert to string if not a valid type
                    valid_sequence.append(str(item))

            if valid_sequence:  # Only set if we have valid items
                span.set_attribute(key, valid_sequence)
        except Exception as e:
            logger.warning(f"Failed to set sequence attribute '{key}': {e}")
            # Fallback to string representation
            span.set_attribute(key, str(value))
    else:
        # Convert other types to string
        try:
            string_value = str(value)
            span.set_attribute(key, string_value)
        except Exception as e:
            logger.warning(f"Failed to convert attribute '{key}' to string: {e}")


def safe_set_span_attributes(span: Span, attributes: Dict[str, Any]) -> None:
    """
    Safely set multiple attributes on a span.

    Args:
        span: The span to set attributes on
        attributes: Dictionary of key-value pairs to set as attributes
    """
    for key, value in attributes.items():
        safe_set_span_attribute(span, key, value)


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Unified Trace Architecture: Framework-Specific Span Mapping (PROD-278)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# Mapping of external framework span names to unified Klira span names
# Format: {original_span_name: klira_unified_name}
FRAMEWORK_SPAN_MAPPING: Dict[str, str] = {
    # OpenAI Agents SDK spans
    "openai.agents.run": "klira.agent.run",
    "openai.agents.tool_call": "klira.tool.call",
    "openai.agents.handoff": "klira.agent.handoff",
    "openai.chat.completion": "klira.llm.openai.completion",
    "openai.response": "klira.llm.openai.response",
    # LangChain spans
    "langchain.chain": "klira.workflow.chain",
    "langchain.agent": "klira.agent.run",
    "langchain.tool": "klira.tool.call",
    "langchain.llm": "klira.llm.langchain",
    "langchain.chat_model": "klira.llm.langchain",
    # CrewAI spans
    "crewai.crew": "klira.workflow.crew",
    "crewai.agent": "klira.agent.run",
    "crewai.task": "klira.task.run",
    "crewai.tool": "klira.tool.call",
    # LlamaIndex spans
    "llamaindex.query": "klira.workflow.query",
    "llamaindex.retrieval": "klira.retrieval",
    "llamaindex.synthesis": "klira.synthesis",
    "llamaindex.llm": "klira.llm.llamaindex",
    # Traceloop auto-instrumented spans (suppressed when external tracing disabled)
    "openai.chat": "klira.llm.openai.completion",
    "anthropic.completion": "klira.llm.anthropic.completion",
    "anthropic.messages": "klira.llm.anthropic.messages",
    "google.generativeai": "klira.llm.gemini",
}

# Span names that should be suppressed (not mapped, just ignored)
# These are typically internal framework spans that don't add value
SUPPRESSED_SPAN_NAMES: set = {
    "openai._base_client.request",
    "httpx.request",
    "httpcore.request",
}


def map_span_name(original_name: str) -> str:
    """
    Map an external framework span name to the unified Klira naming convention.

    This function is part of the Unified Trace Architecture (PROD-278) that ensures
    consistent span naming across all supported frameworks.

    Args:
        original_name: The original span name from the framework

    Returns:
        The mapped Klira span name, or the original if no mapping exists
    """
    # Check if this span should be suppressed
    if original_name in SUPPRESSED_SPAN_NAMES:
        logger.debug(f"Span '{original_name}' is suppressed")
        return ""  # Empty string indicates suppression

    # Check for exact mapping
    if original_name in FRAMEWORK_SPAN_MAPPING:
        mapped_name = FRAMEWORK_SPAN_MAPPING[original_name]
        logger.debug(f"Mapped span '{original_name}' -> '{mapped_name}'")
        return mapped_name

    # Check for prefix-based mapping (e.g., "openai.agents.run.123" -> "klira.agent.run")
    for pattern, klira_name in FRAMEWORK_SPAN_MAPPING.items():
        if original_name.startswith(pattern):
            logger.debug(
                f"Mapped span '{original_name}' -> '{klira_name}' (prefix match)"
            )
            return klira_name

    # No mapping found, return original
    return original_name


def should_suppress_span(span_name: str) -> bool:
    """
    Check if a span should be suppressed based on its name.

    Args:
        span_name: The span name to check

    Returns:
        True if the span should be suppressed, False otherwise
    """
    return span_name in SUPPRESSED_SPAN_NAMES


def get_klira_span_name(framework: str, operation: str, provider: str = "") -> str:
    """
    Generate a standardized Klira span name.

    This follows the Unified Trace Architecture naming convention:
    - Workflow: klira.workflow.{name}
    - Agent: klira.agent.{operation}
    - Tool: klira.tool.{operation}
    - Task: klira.task.{operation}
    - LLM: klira.llm.{provider}.{operation}
    - Compliance: klira.compliance.{outcome}

    Args:
        framework: The framework type (workflow, agent, tool, task, llm, compliance)
        operation: The operation being performed
        provider: Optional provider name (for LLM spans)

    Returns:
        The standardized Klira span name
    """
    if framework == "llm" and provider:
        return f"klira.{framework}.{provider}.{operation}"
    return f"klira.{framework}.{operation}"
