import base64
import os
import json  # <--- NEW
from typing import TYPE_CHECKING, Any, Union
from urllib.parse import quote

from litellm._logging import verbose_logger
from litellm.integrations.arize import _utils
from litellm.types.integrations.langfuse_otel import (
    LangfuseOtelConfig,
    LangfuseSpanAttributes,
)

if TYPE_CHECKING:
    from opentelemetry.trace import Span as _Span

    from litellm.integrations.opentelemetry import (
        OpenTelemetryConfig as _OpenTelemetryConfig,
    )
    from litellm.types.integrations.arize import Protocol as _Protocol

    Protocol = _Protocol
    OpenTelemetryConfig = _OpenTelemetryConfig
    Span = Union[_Span, Any]
else:
    Protocol = Any
    OpenTelemetryConfig = Any
    Span = Any


LANGFUSE_CLOUD_EU_ENDPOINT = "https://cloud.langfuse.com/api/public/otel"
LANGFUSE_CLOUD_US_ENDPOINT = "https://us.cloud.langfuse.com/api/public/otel"



class LangfuseOtelLogger:
    @staticmethod
    def set_langfuse_otel_attributes(span: Span, kwargs, response_obj):
        """
        Sets OpenTelemetry span attributes for Langfuse observability.
        Uses the same attribute setting logic as Arize Phoenix for consistency.
        """
        _utils.set_attributes(span, kwargs, response_obj)

        #########################################################
        # Set Langfuse specific attributes eg Langfuse Environment
        #########################################################
        LangfuseOtelLogger._set_langfuse_specific_attributes(
            span=span,
            kwargs=kwargs
        )
        return

    @staticmethod
    def _extract_langfuse_metadata(kwargs: dict) -> dict:
        """
        Extracts Langfuse metadata from the standard LiteLLM kwargs structure.

        1. Reads kwargs["litellm_params"]["metadata"] if present and is a dict.
        2. Enriches it with any `langfuse_*` request-header params via the
           existing LangFuseLogger.add_metadata_from_header helper so that proxy
           users get identical behaviour across vanilla and OTEL integrations.
        """
        litellm_params = kwargs.get("litellm_params", {}) or {}
        metadata = litellm_params.get("metadata") or {}
        # Ensure we only work with dicts
        if metadata is None or not isinstance(metadata, dict):
            metadata = {}

        # Re-use header extraction logic from the vanilla logger if available
        try:
            from litellm.integrations.langfuse.langfuse import (
                LangFuseLogger as _LFLogger,
            )

            metadata = _LFLogger.add_metadata_from_header(litellm_params, metadata)  # type: ignore
        except Exception:
            # Fallback silently if import fails; header enrichment just won't happen
            pass

        return metadata

    @staticmethod
    def _set_langfuse_specific_attributes(span: Span, kwargs):
        """
        Sets Langfuse specific metadata attributes onto the OTEL span.

        All keys supported by the vanilla Langfuse integration are mapped to
        OTEL-safe attribute names defined in LangfuseSpanAttributes.  Complex
        values (lists/dicts) are serialised to JSON strings for OTEL
        compatibility.
        """
        from litellm.integrations.arize._utils import safe_set_attribute

        # 1) Environment variable override
        langfuse_environment = os.environ.get("LANGFUSE_TRACING_ENVIRONMENT")
        if langfuse_environment:
            safe_set_attribute(
                span,
                LangfuseSpanAttributes.LANGFUSE_ENVIRONMENT.value,
                langfuse_environment,
            )

        # 2) Dynamic metadata from kwargs / headers
        metadata = LangfuseOtelLogger._extract_langfuse_metadata(kwargs)

        # Mapping from metadata key -> OTEL attribute enum
        mapping = {
            "generation_name": LangfuseSpanAttributes.GENERATION_NAME,
            "generation_id": LangfuseSpanAttributes.GENERATION_ID,
            "parent_observation_id": LangfuseSpanAttributes.PARENT_OBSERVATION_ID,
            "version": LangfuseSpanAttributes.GENERATION_VERSION,
            "mask_input": LangfuseSpanAttributes.MASK_INPUT,
            "mask_output": LangfuseSpanAttributes.MASK_OUTPUT,
            "trace_user_id": LangfuseSpanAttributes.TRACE_USER_ID,
            "session_id": LangfuseSpanAttributes.SESSION_ID,
            "tags": LangfuseSpanAttributes.TAGS,
            "trace_name": LangfuseSpanAttributes.TRACE_NAME,
            "trace_id": LangfuseSpanAttributes.TRACE_ID,
            "trace_metadata": LangfuseSpanAttributes.TRACE_METADATA,
            "trace_version": LangfuseSpanAttributes.TRACE_VERSION,
            "trace_release": LangfuseSpanAttributes.TRACE_RELEASE,
            "existing_trace_id": LangfuseSpanAttributes.EXISTING_TRACE_ID,
            "update_trace_keys": LangfuseSpanAttributes.UPDATE_TRACE_KEYS,
            "debug_langfuse": LangfuseSpanAttributes.DEBUG_LANGFUSE,
        }

        for key, enum_attr in mapping.items():
            if key in metadata and metadata[key] is not None:
                value = metadata[key]
                # Lists / dicts must be stringified for OTEL
                if isinstance(value, (list, dict)):
                    try:
                        value = json.dumps(value)
                    except Exception:
                        value = str(value)
                safe_set_attribute(span, enum_attr.value, value)

    @staticmethod
    def get_langfuse_otel_config() -> LangfuseOtelConfig:
        """
        Retrieves the Langfuse OpenTelemetry configuration based on environment variables.

        Environment Variables:
            LANGFUSE_PUBLIC_KEY: Required. Langfuse public key for authentication.
            LANGFUSE_SECRET_KEY: Required. Langfuse secret key for authentication.
            LANGFUSE_HOST: Optional. Custom Langfuse host URL. Defaults to US cloud.

        Returns:
            LangfuseOtelConfig: A Pydantic model containing Langfuse OTEL configuration.

        Raises:
            ValueError: If required keys are missing.
        """
        public_key = os.environ.get("LANGFUSE_PUBLIC_KEY", None)
        secret_key = os.environ.get("LANGFUSE_SECRET_KEY", None)

        if not public_key or not secret_key:
            raise ValueError(
                "LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set for Langfuse OpenTelemetry integration."
            )

        # Determine endpoint - default to US cloud
        langfuse_host = os.environ.get("LANGFUSE_HOST", None)

        if langfuse_host:
            # If LANGFUSE_HOST is provided, construct OTEL endpoint from it
            if not langfuse_host.startswith("http"):
                langfuse_host = "https://" + langfuse_host
            endpoint = f"{langfuse_host.rstrip('/')}/api/public/otel"
            verbose_logger.debug(f"Using Langfuse OTEL endpoint from host: {endpoint}")
        else:
            # Default to US cloud endpoint
            endpoint = LANGFUSE_CLOUD_US_ENDPOINT
            verbose_logger.debug(f"Using Langfuse US cloud endpoint: {endpoint}")

        # Create Basic Auth header
        auth_string = f"{public_key}:{secret_key}"
        auth_header = base64.b64encode(auth_string.encode()).decode()
        # URL encode the entire header value as required by OpenTelemetry specification
        otlp_auth_headers = f"Authorization={quote(f'Basic {auth_header}')}"

        # Set standard OTEL environment variables
        os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint
        os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = otlp_auth_headers

        return LangfuseOtelConfig(
            otlp_auth_headers=otlp_auth_headers, protocol="otlp_http"
        )
