"""
OTLP (OpenTelemetry Protocol) exporter for Risicare SDK.

Exports Risicare spans to any OTLP/HTTP-compatible backend (Jaeger,
Zipkin, Grafana Tempo, OTel Collector) by converting to OTLP JSON format.

Usage:
    from risicare.exporters.otlp import OTLPExporter

    exporter = OTLPExporter(endpoint="http://localhost:4318/v1/traces")
    exporter.export(spans)

Or via init():
    import risicare
    risicare.init(api_key="rsk-...", otlp_endpoint="http://localhost:4318")
"""

from __future__ import annotations

import gzip
import json
import logging
import sys
import threading
import time
import urllib.request
import urllib.error
from typing import Any, Dict, List, Optional, TYPE_CHECKING

from risicare.exporters.base import ExportResult, SpanExporter

if TYPE_CHECKING:
    from risicare_core import Span

logger = logging.getLogger(__name__)


# =============================================================================
# SpanKind Mapping: Risicare → OTLP
# =============================================================================

# OTLP SpanKind integers:
# 0 = UNSPECIFIED, 1 = INTERNAL, 2 = SERVER, 3 = CLIENT, 4 = PRODUCER, 5 = CONSUMER
_KIND_TO_OTLP: Dict[str, int] = {
    # Standard OTel kinds
    "internal": 1,
    "server": 2,
    "client": 3,
    "producer": 4,
    "consumer": 5,
    # Agent-specific → INTERNAL (most natural mapping for internal operations)
    "agent": 1,
    "decision": 1,
    "think": 1,
    "decide": 1,
    "observe": 1,
    "reflect": 1,
    "coordination": 1,
    # LLM/retrieval → CLIENT (outbound calls to external services)
    "llm_call": 3,
    "retrieval": 3,
    "embedding": 3,
    # Delegation/message → PRODUCER (sending work to other agents)
    "delegation": 4,
    "message": 4,
    # Tool calls → CLIENT (calling external tools/APIs)
    "tool_call": 3,
}

# OTLP StatusCode: 0 = UNSET, 1 = OK, 2 = ERROR
_STATUS_TO_OTLP: Dict[str, int] = {
    "unset": 0,
    "ok": 1,
    "error": 2,
}


# =============================================================================
# OTLP Attribute Helpers
# =============================================================================

def _make_otlp_attribute(key: str, value: Any) -> Optional[Dict[str, Any]]:
    """Convert a key-value pair to OTLP attribute format."""
    if value is None:
        return None
    if isinstance(value, str):
        return {"key": key, "value": {"stringValue": value}}
    if isinstance(value, bool):
        return {"key": key, "value": {"boolValue": value}}
    if isinstance(value, int):
        return {"key": key, "value": {"intValue": str(value)}}
    if isinstance(value, float):
        return {"key": key, "value": {"doubleValue": value}}
    if isinstance(value, (list, tuple)):
        # Convert list to array value
        arr_values = []
        for item in value:
            if isinstance(item, str):
                arr_values.append({"stringValue": item})
            elif isinstance(item, bool):
                arr_values.append({"boolValue": item})
            elif isinstance(item, int):
                arr_values.append({"intValue": str(item)})
            elif isinstance(item, float):
                arr_values.append({"doubleValue": item})
        if arr_values:
            return {"key": key, "value": {"arrayValue": {"values": arr_values}}}
    # Fallback: serialize as string
    return {"key": key, "value": {"stringValue": str(value)}}


def _datetime_to_nanos(dt: Any) -> str:
    """Convert datetime to nanoseconds since epoch as string."""
    return str(int(dt.timestamp() * 1_000_000_000))


# =============================================================================
# OTLPExporter
# =============================================================================

class OTLPExporter(SpanExporter):
    """
    Exporter that sends spans in OTLP/HTTP JSON format.

    Converts Risicare spans to the OpenTelemetry Protocol format and sends
    them to any OTLP-compatible endpoint (OTel Collector, Jaeger, Tempo, etc).

    Features:
    - OTLP/HTTP JSON format (application/json)
    - Circuit breaker (3 consecutive failures → 30s cooldown)
    - Retry with exponential backoff
    - gzip compression
    - Risicare → gen_ai.* semantic convention mapping
    """

    def __init__(
        self,
        endpoint: str = "http://localhost:4318/v1/traces",
        headers: Optional[Dict[str, str]] = None,
        timeout_seconds: float = 10.0,
        max_retries: int = 3,
        compress: bool = True,
        service_name: Optional[str] = None,
        service_version: Optional[str] = None,
        environment: Optional[str] = None,
    ):
        """
        Initialize the OTLP exporter.

        Args:
            endpoint: OTLP/HTTP traces endpoint URL.
                      Default: http://localhost:4318/v1/traces
            headers: Additional HTTP headers (e.g., auth tokens).
            timeout_seconds: HTTP request timeout.
            max_retries: Maximum retry attempts.
            compress: Whether to gzip-compress payloads.
            service_name: Service name for resource attributes.
            service_version: Service version for resource attributes.
            environment: Deployment environment for resource attributes.
        """
        # Ensure endpoint ends with /v1/traces
        if not endpoint.endswith("/v1/traces"):
            endpoint = endpoint.rstrip("/") + "/v1/traces"

        self._endpoint = endpoint
        self._headers = headers or {}
        self._timeout = timeout_seconds
        self._max_retries = max_retries
        self._compress = compress
        self._service_name = service_name
        self._service_version = service_version
        self._environment = environment

        # httpx client (lazy init)
        self._client: Any = None
        self._use_httpx = True

        # Circuit breaker state (protected by _cb_lock for thread safety — P2-4)
        self._cb_lock = threading.Lock()
        self._consecutive_failures = 0
        self._circuit_open_until = 0.0
        self._max_failures = 3
        self._cooldown_seconds = 30.0

        # Build resource attributes once
        self._resource_attrs = self._build_resource_attributes()

    @property
    def name(self) -> str:
        """Exporter name for logging."""
        return "otlp"

    def _build_resource_attributes(self) -> List[Dict[str, Any]]:
        """Build OTLP resource attributes from configuration."""
        attrs: List[Dict[str, Any]] = []

        # SDK identification (always present)
        attrs.append({"key": "telemetry.sdk.name", "value": {"stringValue": "risicare-sdk"}})
        attrs.append({"key": "telemetry.sdk.version", "value": {"stringValue": "0.1.0"}})
        attrs.append({"key": "telemetry.sdk.language", "value": {"stringValue": "python"}})

        # Service metadata
        if self._service_name:
            attrs.append({"key": "service.name", "value": {"stringValue": self._service_name}})
        else:
            attrs.append({"key": "service.name", "value": {"stringValue": "unknown_service"}})

        if self._service_version:
            attrs.append({
                "key": "service.version",
                "value": {"stringValue": self._service_version},
            })

        if self._environment:
            attrs.append({
                "key": "deployment.environment",
                "value": {"stringValue": self._environment},
            })

        return attrs

    def _get_client(self) -> Any:
        """Get or create httpx client (lazy initialization)."""
        if self._client is None:
            try:
                import httpx
                try:
                    self._client = httpx.Client(
                        timeout=self._timeout,
                        http2=True,
                        follow_redirects=True,
                    )
                except Exception:
                    # h2 package not installed — fall back to HTTP/1.1
                    self._client = httpx.Client(
                        timeout=self._timeout,
                        follow_redirects=True,
                    )
            except ImportError:
                self._use_httpx = False
                logger.debug("httpx not available, falling back to urllib")
        return self._client

    def export(self, spans: List["Span"]) -> ExportResult:
        """
        Export spans in OTLP/HTTP JSON format.

        Args:
            spans: List of Risicare spans to export.

        Returns:
            ExportResult.SUCCESS or ExportResult.FAILURE.
        """
        if not spans:
            return ExportResult.SUCCESS

        # Circuit breaker check (P2-4: thread-safe)
        with self._cb_lock:
            if self._consecutive_failures >= self._max_failures:
                if time.monotonic() < self._circuit_open_until:
                    return ExportResult.FAILURE
                # Cooldown elapsed — try again
                self._consecutive_failures = 0

        # Convert spans to OTLP payload (outside lock — may be slow)
        try:
            payload = self._build_otlp_payload(spans)
        except Exception as e:
            logger.error("Failed to build OTLP payload: %s", e)
            return ExportResult.FAILURE

        # Serialize
        body = json.dumps(payload).encode("utf-8")

        # Compress if enabled
        content_encoding = None
        if self._compress and len(body) > 512:
            body = gzip.compress(body)
            content_encoding = "gzip"

        # Send with retry
        last_error = None
        retry_delay = 0.1
        for attempt in range(self._max_retries):
            try:
                success = self._send(body, content_encoding)
                if success:
                    with self._cb_lock:
                        self._consecutive_failures = 0
                    # P2-7: Self-observability
                    try:
                        from risicare_core.observability import export_success_total

                        export_success_total.inc({"exporter": "otlp"})
                    except Exception:
                        pass
                    return ExportResult.SUCCESS
            except Exception as e:
                last_error = e

            if attempt < self._max_retries - 1:
                time.sleep(retry_delay)
                retry_delay *= 2

        # All retries failed (P2-4: thread-safe)
        with self._cb_lock:
            self._consecutive_failures += 1
            if self._consecutive_failures >= self._max_failures:
                self._circuit_open_until = time.monotonic() + self._cooldown_seconds
                logger.warning(
                    "OTLP exporter circuit breaker opened after %d failures. "
                    "Cooldown: %.0fs. Last error: %s",
                    self._consecutive_failures,
                    self._cooldown_seconds,
                    last_error,
                )

        # P2-7: Self-observability
        try:
            from risicare_core.observability import export_failure_total

            export_failure_total.inc({"exporter": "otlp"})
        except Exception:
            pass

        return ExportResult.FAILURE

    def _send(self, body: bytes, content_encoding: Optional[str]) -> bool:
        """Send the serialized payload. Returns True on success."""
        headers = {
            "Content-Type": "application/json",
            **self._headers,
        }
        if content_encoding:
            headers["Content-Encoding"] = content_encoding

        if self._use_httpx:
            client = self._get_client()
            if client is not None:
                response = client.post(
                    self._endpoint,
                    content=body,
                    headers=headers,
                )
                if response.status_code < 300:
                    return True
                if response.status_code == 429:
                    # Rate limited — respect Retry-After if present
                    retry_after = response.headers.get("Retry-After")
                    if retry_after:
                        try:
                            time.sleep(min(float(retry_after), 10.0))
                        except ValueError:
                            pass
                logger.debug(
                    "OTLP export failed: HTTP %d %s",
                    response.status_code,
                    response.text[:200] if response.text else "",
                )
                return False

        # Fallback to urllib
        req = urllib.request.Request(
            self._endpoint,
            data=body,
            headers=headers,
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=self._timeout) as resp:
                return resp.status < 300
        except urllib.error.HTTPError as e:
            logger.debug("OTLP export failed: HTTP %d", e.code)
            return False
        except urllib.error.URLError as e:
            logger.debug("OTLP export failed: %s", e.reason)
            return False

    def _build_otlp_payload(self, spans: List["Span"]) -> Dict[str, Any]:
        """
        Build the full OTLP ExportTraceServiceRequest JSON structure.

        Structure:
        {
          "resourceSpans": [{
            "resource": { "attributes": [...] },
            "scopeSpans": [{
              "scope": { "name": "risicare-sdk", "version": "0.1.0" },
              "spans": [...]
            }]
          }]
        }
        """
        otlp_spans = [self._convert_span(span) for span in spans]

        return {
            "resourceSpans": [{
                "resource": {
                    "attributes": list(self._resource_attrs),
                },
                "scopeSpans": [{
                    "scope": {
                        "name": "risicare-sdk",
                        "version": "0.1.0",
                    },
                    "spans": otlp_spans,
                }],
            }],
        }

    def _convert_span(self, span: "Span") -> Dict[str, Any]:
        """Convert a Risicare Span to OTLP span format."""
        # Core fields
        otlp_span: Dict[str, Any] = {
            "traceId": span.trace_id,
            "spanId": span.span_id,
            "name": span.name,
            "kind": _KIND_TO_OTLP.get(span.kind.value, 1),
            "startTimeUnixNano": _datetime_to_nanos(span.start_time),
        }

        # Parent span
        if span.parent_span_id:
            otlp_span["parentSpanId"] = span.parent_span_id
        else:
            otlp_span["parentSpanId"] = ""

        # End time
        if span.end_time:
            otlp_span["endTimeUnixNano"] = _datetime_to_nanos(span.end_time)
        else:
            otlp_span["endTimeUnixNano"] = "0"

        # Status
        status_code = _STATUS_TO_OTLP.get(span.status.value, 0)
        otlp_span["status"] = {"code": status_code}
        if span.status_message:
            otlp_span["status"]["message"] = span.status_message

        # Attributes
        otlp_span["attributes"] = self._convert_attributes(span)

        # Events
        otlp_span["events"] = self._convert_events(span)

        # Links
        otlp_span["links"] = self._convert_links(span)

        return otlp_span

    def _convert_attributes(self, span: "Span") -> List[Dict[str, Any]]:
        """Convert span attributes + Risicare fields to OTLP attributes."""
        attrs: List[Dict[str, Any]] = []

        # LLM attributes → gen_ai.* semantic conventions
        llm = span.llm
        if llm:
            if llm.provider:
                attrs.append({"key": "gen_ai.system", "value": {"stringValue": llm.provider}})
            if llm.model:
                attrs.append({
                    "key": "gen_ai.response.model",
                    "value": {"stringValue": llm.model},
                })
            if llm.prompt_tokens is not None:
                attrs.append({
                    "key": "gen_ai.usage.prompt_tokens",
                    "value": {"intValue": str(llm.prompt_tokens)},
                })
            if llm.completion_tokens is not None:
                attrs.append({
                    "key": "gen_ai.usage.completion_tokens",
                    "value": {"intValue": str(llm.completion_tokens)},
                })
            if llm.total_tokens is not None:
                attrs.append({
                    "key": "gen_ai.usage.total_tokens",
                    "value": {"intValue": str(llm.total_tokens)},
                })
            if llm.cost_usd is not None:
                attrs.append({
                    "key": "gen_ai.usage.cost",
                    "value": {"doubleValue": llm.cost_usd},
                })
            if llm.temperature is not None:
                attrs.append({
                    "key": "gen_ai.request.temperature",
                    "value": {"doubleValue": llm.temperature},
                })
            if llm.max_tokens is not None:
                attrs.append({
                    "key": "gen_ai.request.max_tokens",
                    "value": {"intValue": str(llm.max_tokens)},
                })
            if llm.stop_reason:
                attrs.append({
                    "key": "gen_ai.response.finish_reason",
                    "value": {"stringValue": llm.stop_reason},
                })

        # Preserve Risicare span kind for round-trip fidelity.
        # Standard OTel kinds (internal, server, client, producer, consumer)
        # don't need this — only Risicare-specific kinds that lose semantics
        # when mapped to the 5 standard OTLP kind integers.
        _STANDARD_OTEL_KINDS = {"internal", "server", "client", "producer", "consumer"}
        kind_value = span.kind.value if hasattr(span.kind, "value") else str(span.kind)
        if kind_value not in _STANDARD_OTEL_KINDS:
            attrs.append({
                "key": "risicare.span_kind",
                "value": {"stringValue": kind_value},
            })

        # Risicare-specific fields → namespaced attributes
        if span.agent_id:
            attrs.append({"key": "agent.id", "value": {"stringValue": span.agent_id}})
        if span.session_id:
            attrs.append({"key": "session.id", "value": {"stringValue": span.session_id}})
        if span.semantic_phase:
            attrs.append({
                "key": "risicare.semantic_phase",
                "value": {"stringValue": span.semantic_phase.value},
            })

        # Generic span attributes
        for key, value in span.attributes.items():
            attr = _make_otlp_attribute(key, value)
            if attr:
                attrs.append(attr)

        return attrs

    def _convert_events(self, span: "Span") -> List[Dict[str, Any]]:
        """Convert span events to OTLP event format."""
        events: List[Dict[str, Any]] = []

        for event in span.events:
            otlp_event: Dict[str, Any] = {
                "name": event.name,
                "timeUnixNano": _datetime_to_nanos(event.timestamp),
            }
            if event.attributes:
                otlp_attrs = []
                for key, value in event.attributes.items():
                    attr = _make_otlp_attribute(key, value)
                    if attr:
                        otlp_attrs.append(attr)
                otlp_event["attributes"] = otlp_attrs
            else:
                otlp_event["attributes"] = []
            events.append(otlp_event)

        # Convert exceptions to OTel exception events
        for exc in span.exceptions:
            exc_attrs = [
                {"key": "exception.type", "value": {"stringValue": exc.type}},
                {"key": "exception.message", "value": {"stringValue": exc.message}},
            ]
            if exc.stacktrace:
                exc_attrs.append({
                    "key": "exception.stacktrace",
                    "value": {"stringValue": exc.stacktrace},
                })
            exc_time = exc.timestamp if exc.timestamp else span.start_time
            events.append({
                "name": "exception",
                "timeUnixNano": _datetime_to_nanos(exc_time),
                "attributes": exc_attrs,
            })

        return events

    def _convert_links(self, span: "Span") -> List[Dict[str, Any]]:
        """Convert span links to OTLP link format."""
        links: List[Dict[str, Any]] = []

        for link in span.links:
            otlp_link: Dict[str, Any] = {
                "traceId": link.trace_id,
                "spanId": link.span_id,
            }
            if link.attributes:
                otlp_attrs = []
                for key, value in link.attributes.items():
                    attr = _make_otlp_attribute(key, value)
                    if attr:
                        otlp_attrs.append(attr)
                otlp_link["attributes"] = otlp_attrs
            else:
                otlp_link["attributes"] = []
            links.append(otlp_link)

        return links

    def shutdown(self) -> None:
        """Shutdown the exporter and close HTTP client."""
        if self._client is not None:
            try:
                self._client.close()
            except Exception:
                pass
            self._client = None
