"""
Span conversion utilities for OTel ↔ Risicare.

Converts OpenTelemetry ReadableSpan objects to Risicare Span objects,
extracting gen_ai.* semantic conventions into LLMAttributes.

All opentelemetry.* imports are inside function bodies to ensure
zero import-time dependency on the OTel SDK.
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from risicare_core import Span

logger = logging.getLogger(__name__)


# =============================================================================
# OTel SpanKind → Risicare SpanKind
# =============================================================================

# OTel SpanKind enum values:
# INTERNAL=0, SERVER=1, CLIENT=2, PRODUCER=3, CONSUMER=4
_OTEL_KIND_MAP: Dict[int, str] = {
    0: "internal",
    1: "server",
    2: "client",
    3: "producer",
    4: "consumer",
}


# =============================================================================
# Public API
# =============================================================================

def convert_otel_to_risicare(otel_span: Any) -> "Span":
    """
    Convert an OpenTelemetry ReadableSpan to a Risicare Span.

    This function handles all OTel SDK types internally and extracts
    gen_ai.* semantic conventions into Risicare's structured LLMAttributes.

    Args:
        otel_span: An opentelemetry.sdk.trace.ReadableSpan instance.

    Returns:
        A Risicare Span with all fields populated from the OTel span.
    """
    from risicare_core.types.base import SpanKind, SpanStatus, SemanticPhase
    from risicare_core.types.spans import (
        Span,
        SpanEvent,
        SpanLink,
        ExceptionInfo,
        LLMAttributes,
    )

    # --- IDs ---
    ctx = otel_span.context
    trace_id = format(ctx.trace_id, "032x")
    span_id = format(ctx.span_id, "016x")

    parent_span_id: Optional[str] = None
    parent = getattr(otel_span, "parent", None)
    if parent is not None:
        parent_sid = getattr(parent, "span_id", None)
        if parent_sid is not None:
            try:
                parent_span_id = format(parent_sid, "016x")
            except (TypeError, ValueError):
                # parent_sid may not be an integer in all OTel implementations.
                # Only keep it if it's a valid 16-char hex string; otherwise drop.
                fallback = str(parent_sid)
                if len(fallback) == 16 and all(c in "0123456789abcdef" for c in fallback.lower()):
                    parent_span_id = fallback.lower()
                else:
                    parent_span_id = None

    # --- Timing ---
    start_time = _nanos_to_datetime(otel_span.start_time)
    end_time = _nanos_to_datetime(otel_span.end_time) if otel_span.end_time else None

    # --- Kind ---
    otel_kind_value = getattr(otel_span.kind, "value", 0) if otel_span.kind else 0
    kind_str = _OTEL_KIND_MAP.get(otel_kind_value, "internal")
    kind = SpanKind(kind_str)

    # --- Status ---
    status = SpanStatus.UNSET
    status_message: Optional[str] = None
    if otel_span.status:
        otel_status_code = getattr(otel_span.status, "status_code", None)
        if otel_status_code is not None:
            code_value = getattr(otel_status_code, "value", otel_status_code)
            if code_value == 1:
                status = SpanStatus.OK
            elif code_value == 2:
                status = SpanStatus.ERROR
        status_message = getattr(otel_span.status, "description", None)

    # --- Attributes ---
    raw_attrs: Dict[str, Any] = dict(otel_span.attributes) if otel_span.attributes else {}

    # Extract LLM attributes from gen_ai.* conventions
    llm = _extract_llm_attributes(raw_attrs)

    # Extract Risicare-specific attributes
    agent_id = _pop_str(raw_attrs, "agent.id")
    session_id = _pop_str(raw_attrs, "session.id")
    phase_str = _pop_str(raw_attrs, "risicare.semantic_phase")
    semantic_phase: Optional[SemanticPhase] = None
    if phase_str:
        try:
            semantic_phase = SemanticPhase(phase_str)
        except ValueError:
            pass

    # If gen_ai.system present and kind is generic, upgrade to llm_call
    if llm and llm.provider and kind == SpanKind.INTERNAL:
        kind = SpanKind.LLM_CALL

    # --- Events ---
    events: List[SpanEvent] = []
    exceptions: List[ExceptionInfo] = []
    for otel_event in (otel_span.events or []):
        event_attrs = dict(otel_event.attributes) if otel_event.attributes else {}
        event_ts = _nanos_to_datetime(otel_event.timestamp) if otel_event.timestamp else start_time

        # OTel exception events → Risicare ExceptionInfo
        if otel_event.name == "exception":
            exc_type = event_attrs.get("exception.type", "Exception")
            exc_msg = event_attrs.get("exception.message", "")
            exc_stack = event_attrs.get("exception.stacktrace", "")
            exceptions.append(ExceptionInfo(
                type=str(exc_type),
                message=str(exc_msg),
                stacktrace=str(exc_stack),
                timestamp=event_ts,
                escaped=True,
            ))
        else:
            events.append(SpanEvent(
                name=otel_event.name,
                timestamp=event_ts,
                attributes=event_attrs,
            ))

    # --- Links ---
    links: List[SpanLink] = []
    for otel_link in (otel_span.links or []):
        link_ctx = otel_link.context
        if link_ctx:
            link_attrs = dict(otel_link.attributes) if otel_link.attributes else {}
            links.append(SpanLink(
                trace_id=format(link_ctx.trace_id, "032x"),
                span_id=format(link_ctx.span_id, "016x"),
                attributes=link_attrs,
            ))

    # --- Build Span ---
    return Span(
        trace_id=trace_id,
        span_id=span_id,
        name=otel_span.name,
        parent_span_id=parent_span_id,
        kind=kind,
        start_time=start_time,
        end_time=end_time,
        status=status,
        status_message=status_message,
        attributes=raw_attrs,
        events=events,
        links=links,
        exceptions=exceptions,
        session_id=session_id,
        agent_id=agent_id,
        semantic_phase=semantic_phase,
        llm=llm,
    )


# =============================================================================
# Internal Helpers
# =============================================================================

def _nanos_to_datetime(nanos: int) -> datetime:
    """Convert nanoseconds since epoch to datetime."""
    if nanos <= 0:
        return datetime.now(timezone.utc)
    return datetime.fromtimestamp(nanos / 1_000_000_000, tz=timezone.utc)


def _pop_str(attrs: Dict[str, Any], key: str) -> Optional[str]:
    """Pop a string attribute, returning None if absent."""
    value = attrs.pop(key, None)
    return str(value) if value is not None else None


def _extract_llm_attributes(attrs: Dict[str, Any]) -> Optional[Any]:
    """
    Extract gen_ai.* semantic conventions from OTel attributes
    into a Risicare LLMAttributes object.

    Pops consumed keys from the attrs dict to avoid duplication.
    Returns None if no gen_ai.* attributes are found.
    """
    from risicare_core.types.spans import LLMAttributes

    provider = _pop_str(attrs, "gen_ai.system")
    model = _pop_str(attrs, "gen_ai.response.model") or _pop_str(attrs, "gen_ai.request.model")
    prompt_tokens = _pop_int(attrs, "gen_ai.usage.prompt_tokens")
    completion_tokens = _pop_int(attrs, "gen_ai.usage.completion_tokens")
    total_tokens = _pop_int(attrs, "gen_ai.usage.total_tokens")
    cost_usd = _pop_float(attrs, "gen_ai.usage.cost")
    temperature = _pop_float(attrs, "gen_ai.request.temperature")
    max_tokens = _pop_int(attrs, "gen_ai.request.max_tokens")
    stop_reason = _pop_str(attrs, "gen_ai.response.finish_reason")

    # Only create LLMAttributes if at least one field is present
    if not any([provider, model, prompt_tokens, completion_tokens, total_tokens]):
        return None

    return LLMAttributes(
        model=model,
        provider=provider,
        prompt_tokens=prompt_tokens,
        completion_tokens=completion_tokens,
        total_tokens=total_tokens,
        cost_usd=cost_usd,
        temperature=temperature,
        max_tokens=max_tokens,
        stop_reason=stop_reason,
    )


def _pop_int(attrs: Dict[str, Any], key: str) -> Optional[int]:
    """Pop an integer attribute."""
    value = attrs.pop(key, None)
    if value is None:
        return None
    try:
        return int(value)
    except (ValueError, TypeError):
        return None


def _pop_float(attrs: Dict[str, Any], key: str) -> Optional[float]:
    """Pop a float attribute."""
    value = attrs.pop(key, None)
    if value is None:
        return None
    try:
        return float(value)
    except (ValueError, TypeError):
        return None
