# pylint: disable=broad-exception-caught
"""
The __init__.py module for the openLIT package.
This module sets up the openLIT configuration and instrumentation for various
large language models (LLMs).
"""

from typing import Dict
import logging
import os
from importlib.util import find_spec
from functools import wraps
from contextlib import contextmanager
import requests


# Import internal modules for setting up tracing and fetching pricing info.
from opentelemetry import trace as t
from opentelemetry.trace import SpanKind, Status, StatusCode, Span
from AICORELibrary.openlit.semcov import SemanticConvetion
from AICORELibrary.openlit.otel.tracing import setup_tracing
from AICORELibrary.openlit.otel.metrics import setup_meter
from AICORELibrary.openlit.__helpers import fetch_pricing_info, get_env_variable


# Instrumentors for various large language models.
from AICORELibrary.openlit.instrumentation.openai import OpenAIInstrumentor
from AICORELibrary.openlit.instrumentation.anthropic import AnthropicInstrumentor
from AICORELibrary.openlit.instrumentation.cohere import CohereInstrumentor
from AICORELibrary.openlit.instrumentation.mistral import MistralInstrumentor
from AICORELibrary.openlit.instrumentation.bedrock import BedrockInstrumentor
from AICORELibrary.openlit.instrumentation.vertexai import VertexAIInstrumentor
from AICORELibrary.openlit.instrumentation.groq import GroqInstrumentor
from AICORELibrary.openlit.instrumentation.ollama import OllamaInstrumentor
from AICORELibrary.openlit.instrumentation.gpt4all import GPT4AllInstrumentor
from AICORELibrary.openlit.instrumentation.elevenlabs import ElevenLabsInstrumentor
from AICORELibrary.openlit.instrumentation.vllm import VLLMInstrumentor
from AICORELibrary.openlit.instrumentation.google_ai_studio import GoogleAIStudioInstrumentor
from AICORELibrary.openlit.instrumentation.reka import RekaInstrumentor
from AICORELibrary.openlit.instrumentation.premai import PremAIInstrumentor
from AICORELibrary.openlit.instrumentation.assemblyai import AssemblyAIInstrumentor
from AICORELibrary.openlit.instrumentation.azure_ai_inference import AzureAIInferenceInstrumentor
from AICORELibrary.openlit.instrumentation.langchain import LangChainInstrumentor
from AICORELibrary.openlit.instrumentation.llamaindex import LlamaIndexInstrumentor
from AICORELibrary.openlit.instrumentation.haystack import HaystackInstrumentor
from AICORELibrary.openlit.instrumentation.embedchain import EmbedChainInstrumentor
from AICORELibrary.openlit.instrumentation.mem0 import Mem0Instrumentor
from AICORELibrary.openlit.instrumentation.chroma import ChromaInstrumentor
from AICORELibrary.openlit.instrumentation.pinecone import PineconeInstrumentor
from AICORELibrary.openlit.instrumentation.qdrant import QdrantInstrumentor
from AICORELibrary.openlit.instrumentation.milvus import MilvusInstrumentor
from AICORELibrary.openlit.instrumentation.astra import AstraInstrumentor
from AICORELibrary.openlit.instrumentation.transformers import TransformersInstrumentor
from AICORELibrary.openlit.instrumentation.litellm import LiteLLMInstrumentor
from AICORELibrary.openlit.instrumentation.together import TogetherInstrumentor
from AICORELibrary.openlit.instrumentation.crewai import CrewAIInstrumentor
from AICORELibrary.openlit.instrumentation.ag2 import AG2Instrumentor
from AICORELibrary.openlit.instrumentation.multion import MultiOnInstrumentor
from AICORELibrary.openlit.instrumentation.dynamiq import DynamiqInstrumentor
from AICORELibrary.openlit.instrumentation.phidata import PhidataInstrumentor
from AICORELibrary.openlit.instrumentation.julep import JulepInstrumentor
from AICORELibrary.openlit.instrumentation.ai21 import AI21Instrumentor
from AICORELibrary.openlit.instrumentation.controlflow import ControlFlowInstrumentor
from AICORELibrary.openlit.instrumentation.crawl4ai import Crawl4AIInstrumentor
from AICORELibrary.openlit.instrumentation.firecrawl import FireCrawlInstrumentor
from AICORELibrary.openlit.instrumentation.letta import LettaInstrumentor
from AICORELibrary.openlit.instrumentation.gpu import GPUInstrumentor
import AICORELibrary.openlit.guard
import AICORELibrary.openlit.evals

# Set up logging for error and information messages.
logger = logging.getLogger(__name__)


class OpenlitConfig:
    """
    A Singleton Configuration class for openLIT.

    This class maintains a single instance of configuration settings including
    environment details, application name, and tracing information throughout the openLIT package.

    Attributes:
        environment (str): Deployment environment of the application.
        application_name (str): Name of the application using openLIT.
        pricing_info (Dict[str, Any]): Pricing information.
        tracer (Optional[Any]): Tracer instance for OpenTelemetry.
        otlp_endpoint (Optional[str]): Endpoint for OTLP.
        otlp_headers (Optional[Dict[str, str]]): Headers for OTLP.
        disable_batch (bool): Flag to disable batch span processing in tracing.
        trace_content (bool): Flag to enable or disable tracing of content.
    """

    _instance = None

    def __new__(cls):
        """Ensures that only one instance of the configuration exists."""
        if cls._instance is None:
            cls._instance = super(OpenlitConfig, cls).__new__(cls)
            cls.reset_to_defaults()
        return cls._instance

    @classmethod
    def reset_to_defaults(cls):
        """Resets configuration to default values."""
        cls.environment = "default"
        cls.application_name = "default"
        cls.pricing_info = {}
        cls.tracer = None
        cls.metrics_dict = {}
        cls.otlp_endpoint = None
        cls.otlp_headers = None
        cls.disable_batch = False
        cls.trace_content = True
        cls.disable_metrics = False

    @classmethod
    def update_config(
        cls,
        environment,
        application_name,
        tracer,
        otlp_endpoint,
        otlp_headers,
        disable_batch,
        trace_content,
        metrics_dict,
        disable_metrics,
        pricing_json,
    ):
        """
        Updates the configuration based on provided parameters.

        Args:
            environment (str): Deployment environment.
            application_name (str): Application name.
            tracer: Tracer instance.
            meter: Metric Instance
            otlp_endpoint (str): OTLP endpoint.
            otlp_headers (Dict[str, str]): OTLP headers.
            disable_batch (bool): Disable batch span processing flag.
            trace_content (bool): Enable or disable content tracing.
            pricing_json(str): path or url to the pricing json file
        """
        cls.environment = environment
        cls.application_name = application_name
        cls.pricing_info = fetch_pricing_info(pricing_json)
        cls.tracer = tracer
        cls.metrics_dict = metrics_dict
        cls.otlp_endpoint = otlp_endpoint
        cls.otlp_headers = otlp_headers
        cls.disable_batch = disable_batch
        cls.trace_content = trace_content
        cls.disable_metrics = disable_metrics

def module_exists(module_name):
    """Check if nested modules exist, addressing the dot notation issue."""
    parts = module_name.split(".")
    for i in range(1, len(parts) + 1):
        if find_spec(".".join(parts[:i])) is None:
            return False
    return True

def instrument_if_available(
    instrumentor_name,
    instrumentor_instance,
    config,
    disabled_instrumentors,
    module_name_map,
):
    """Instruments the specified instrumentor if its library is available."""
    if instrumentor_name in disabled_instrumentors:
        logger.info("Instrumentor %s is disabled", instrumentor_name)
        return

    module_name = module_name_map.get(instrumentor_name)

    if not module_name:
        logger.error("No module mapping for %s", instrumentor_name)
        return

    try:
        if module_exists(module_name):
            instrumentor_instance.instrument(
                environment=config.environment,
                application_name=config.application_name,
                tracer=config.tracer,
                pricing_info=config.pricing_info,
                trace_content=config.trace_content,
                metrics_dict=config.metrics_dict,
                disable_metrics=config.disable_metrics,
            )
        else:
            # pylint: disable=line-too-long
            logger.info("Library for %s (%s) not found. Skipping instrumentation", instrumentor_name, module_name)
    except Exception as e:
        logger.error("Failed to instrument %s: %s", instrumentor_name, e)


def init(environment="default", application_name="default", tracer=None, otlp_endpoint=None,
         otlp_headers=None, disable_batch=False, trace_content=True, disabled_instrumentors=None,
         meter=None, disable_metrics=False, pricing_json=None, collect_gpu_stats=False):
    """
    Initializes the openLIT configuration and setups tracing.

    This function sets up the openLIT environment with provided configurations
    and initializes instrumentors for tracing.

    Args:
        environment (str): Deployment environment.
        application_name (str): Application name.
        tracer: Tracer instance (Optional).
        meter: OpenTelemetry Metrics Instance (Optional).
        otlp_endpoint (str): OTLP endpoint for exporter (Optional).
        otlp_headers (Dict[str, str]): OTLP headers for exporter (Optional).
        disable_batch (bool): Flag to disable batch span processing (Optional).
        trace_content (bool): Flag to trace content (Optional).
        disabled_instrumentors (List[str]): Optional. List of instrumentor names to disable.
        disable_metrics (bool): Flag to disable metrics (Optional).
        pricing_json(str): File path or url to the pricing json (Optional).
        collect_gpu_stats (bool): Flag to enable or disable GPU metrics collection.
    """
    disabled_instrumentors = disabled_instrumentors if disabled_instrumentors else []
    logger.info("Starting openLIT initialization...")

    module_name_map = {
        "openai": "openai",
        "anthropic": "anthropic",
        "cohere": "cohere",
        "mistral": "mistralai",
        "bedrock": "boto3",
        "vertexai": "vertexai",
        "groq": "groq",
        "ollama": "ollama",
        "gpt4all": "gpt4all",
        "elevenlabs": "elevenlabs",
        "vllm": "vllm",
        "google-ai-studio": "google.generativeai",
        "azure-ai-inference": "azure.ai.inference",
        "langchain": "langchain",
        "llama_index": "llama_index",
        "haystack": "haystack",
        "embedchain": "embedchain",
        "mem0": "mem0",
        "chroma": "chromadb",
        "pinecone": "pinecone",
        "qdrant": "qdrant_client",
        "milvus": "pymilvus",
        "transformers": "transformers",
        "litellm": "litellm",
        "crewai": "crewai",
        "ag2": "ag2",
        "autogen": "autogen",
        "pyautogen": "pyautogen",
        "multion": "multion",
        "dynamiq": "dynamiq",
        "phidata": "phi",
        "reka-api": "reka",
        "premai": "premai",
        "julep": "julep",
        "astra": "astrapy",
        "ai21": "ai21",
        "controlflow": "controlflow",
        "assemblyai": "assemblyai",
        "crawl4ai": "crawl4ai",
        "firecrawl": "firecrawl",
        "letta": "letta",
        "together": "together",
    }

    invalid_instrumentors = [
        name for name in disabled_instrumentors if name not in module_name_map
    ]
    for invalid_name in invalid_instrumentors:
        logger.warning("Invalid instrumentor name detected and ignored: '%s'", invalid_name)

    try:
        # Retrieve or create the single configuration instance.
        config = OpenlitConfig()

        # Setup tracing based on the provided or default configuration.
        tracer = setup_tracing(
            application_name=application_name,
            environment=environment,
            tracer=tracer,
            otlp_endpoint=otlp_endpoint,
            otlp_headers=otlp_headers,
            disable_batch=disable_batch,
        )

        if not tracer:
            logger.error("openLIT tracing setup failed. Tracing will not be available.")
            return

        # Setup meter and receive metrics_dict instead of meter.
        metrics_dict, err = setup_meter(
            application_name=application_name,
            environment=environment,
            meter=meter,
            otlp_endpoint=otlp_endpoint,
            otlp_headers=otlp_headers,
        )

        if err:
            logger.error("OpenLIT metrics setup failed. Metrics will not be available: %s", err)
            return

        # Update global configuration with the provided settings.
        config.update_config(
            environment,
            application_name,
            tracer,
            otlp_endpoint,
            otlp_headers,
            disable_batch,
            trace_content,
            metrics_dict,
            disable_metrics,
            pricing_json,
        )

        # Map instrumentor names to their instances
        instrumentor_instances = {
            "openai": OpenAIInstrumentor(),
            "anthropic": AnthropicInstrumentor(),
            "cohere": CohereInstrumentor(),
            "mistral": MistralInstrumentor(),
            "bedrock": BedrockInstrumentor(),
            "vertexai": VertexAIInstrumentor(),
            "groq": GroqInstrumentor(),
            "ollama": OllamaInstrumentor(),
            "gpt4all": GPT4AllInstrumentor(),
            "elevenlabs": ElevenLabsInstrumentor(),
            "vllm": VLLMInstrumentor(),
            "google-ai-studio": GoogleAIStudioInstrumentor(),
            "azure-ai-inference": AzureAIInferenceInstrumentor(),
            "langchain": LangChainInstrumentor(),
            "llama_index": LlamaIndexInstrumentor(),
            "haystack": HaystackInstrumentor(),
            "embedchain": EmbedChainInstrumentor(),
            "mem0": Mem0Instrumentor(),
            "chroma": ChromaInstrumentor(),
            "pinecone": PineconeInstrumentor(),
            "qdrant": QdrantInstrumentor(),
            "milvus": MilvusInstrumentor(),
            "transformers": TransformersInstrumentor(),
            "litellm": LiteLLMInstrumentor(),
            "crewai": CrewAIInstrumentor(),
            "ag2": AG2Instrumentor(),
            "multion": MultiOnInstrumentor(),
            "autogen": AG2Instrumentor(),
            "pyautogen": AG2Instrumentor(),
            "dynamiq": DynamiqInstrumentor(),
            "phidata": PhidataInstrumentor(),
            "reka-api": RekaInstrumentor(),
            "premai": PremAIInstrumentor(),
            "julep": JulepInstrumentor(),
            "astra": AstraInstrumentor(),
            "ai21": AI21Instrumentor(),
            "controlflow": ControlFlowInstrumentor(),
            "assemblyai": AssemblyAIInstrumentor(),
            "crawl4ai": Crawl4AIInstrumentor(),
            "firecrawl": FireCrawlInstrumentor(),
            "letta": LettaInstrumentor(),
            "together": TogetherInstrumentor(),
        }

        # Initialize and instrument only the enabled instrumentors
        for name, instrumentor in instrumentor_instances.items():
            instrument_if_available(name, instrumentor, config,
            disabled_instrumentors, module_name_map)

        if not disable_metrics and collect_gpu_stats:
            GPUInstrumentor().instrument(
                environment=config.environment,
                application_name=config.application_name,
            )
    except Exception as e:
        logger.error("Error during openLIT initialization: %s", e)

def get_prompt(url=None, name=None, api_key=None, prompt_id=None,
    version=None, should_compile=None, variables=None, meta_properties=None):
    """
    Retrieve and returns the prompt from OpenLIT Prompt Hub
    """

    # Validate and set the base URL
    url = get_env_variable(
        'OPENLIT_URL',
        url,
        'Missing OpenLIT URL: Provide as arg or set OPENLIT_URL env var.'
    )

    # Validate and set the API key
    api_key = get_env_variable(
        'OPENLIT_API_KEY', 
        api_key,
        'Missing API key: Provide as arg or set OPENLIT_API_KEY env var.'
    )

    # Construct the API endpoint
    endpoint = url + "/api/prompt/get-compiled"

    # Prepare the payload
    payload = {
        'name': name,
        'promptId': prompt_id,
        'version': version,
        'shouldCompile': should_compile,
        'variables': variables,
        'metaProperties': meta_properties,
        'source': 'python-sdk'
    }

    # Remove None values from payload
    payload = {k: v for k, v in payload.items() if v is not None}

    # Prepare headers
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }

    try:
        # Make the POST request to the API with headers
        response = requests.post(endpoint, json=payload, headers=headers, timeout=120)

        # Check if the response is successful
        response.raise_for_status()

        # Return the JSON response
        return response.json()
    except requests.RequestException as error:
        logger.error("Error fetching prompt: '%s'", error)
        return None

def get_secrets(url=None, api_key=None, key=None, tags=None, should_set_env=None):
    """
    Retrieve & returns the secrets from OpenLIT Vault & sets all to env is should_set_env is True
    """

    # Validate and set the base URL
    url = get_env_variable(
        'OPENLIT_URL',
        url,
        'Missing OpenLIT URL: Provide as arg or set OPENLIT_URL env var.'
    )

    # Validate and set the API key
    api_key = get_env_variable(
        'OPENLIT_API_KEY', 
        api_key,
        'Missing API key: Provide as arg or set OPENLIT_API_KEY env var.'
    )

    # Construct the API endpoint
    endpoint = url + "/api/vault/get-secrets"

    # Prepare the payload
    payload = {
        'key': key,
        'tags': tags,
        'source': 'python-sdk'
    }

    # Remove None values from payload
    payload = {k: v for k, v in payload.items() if v is not None}

    # Prepare headers
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }

    try:
        # Make the POST request to the API with headers
        response = requests.post(endpoint, json=payload, headers=headers, timeout=120)

        # Check if the response is successful
        response.raise_for_status()

        # Return the JSON response
        vault_response = response.json()

        res = vault_response.get('res', [])

        if should_set_env is True:
            for token, value in res.items():
                os.environ[token] = str(value)
        return vault_response
    except requests.RequestException as error:
        logger.error("Error fetching secrets: '%s'", error)
        return None

def trace(wrapped):
    """
    Generates a telemetry wrapper for messages to collect metrics.
    """

    @wraps(wrapped)
    def wrapper(*args, **kwargs):
        __trace = t.get_tracer_provider()
        with __trace.get_tracer(__name__).start_as_current_span(
            name=wrapped.__name__,
            kind=SpanKind.CLIENT,
        ) as span:
            try:
                response = wrapped(*args, **kwargs)
                span.set_attribute(
                    SemanticConvetion.GEN_AI_CONTENT_COMPLETION, response
                )
                span.set_status(Status(StatusCode.OK))

            except Exception as e:
                response = None
                span.record_exception(e)
                span.set_status(status=Status(StatusCode.ERROR), description=e)
                logging.error("Error in %s: %s", wrapped.__name__, e, exc_info=True)

            # Adding function arguments as metadata
            try:
                span.set_attribute("function.args", str(args))
                span.set_attribute("function.kwargs", str(kwargs))
                span.set_attribute(
                    SemanticConvetion.GEN_AI_APPLICATION_NAME,
                    OpenlitConfig.application_name,
                )
                span.set_attribute(
                    SemanticConvetion.GEN_AI_ENVIRONMENT, OpenlitConfig.environment
                )

            except Exception as meta_exception:
                logging.error(
                    "Failed to set metadata for %s: %s",
                    wrapped.__name__,
                    meta_exception,
                    exc_info=True,
                )

            return response

    return wrapper


class TracedSpan:
    """
    A wrapper class for an OpenTelemetry span that provides helper methods
    for setting result and metadata attributes on the span.

    Attributes:
        _span (Span): The underlying OpenTelemetry span.
    """

    def __init__(self, span):
        """
        Initializes the TracedSpan with the given span.

        Params:
            span (Span): The OpenTelemetry span to be wrapped.
        """

        self._span: Span = span

    def set_result(self, result):
        """
        Sets the result attribute on the underlying span.

        Params:
            result: The result to be set as an attribute on the span.
        """

        self._span.set_attribute(SemanticConvetion.GEN_AI_CONTENT_COMPLETION, result)

    def set_metadata(self, metadata: Dict):
        """
        Sets multiple attributes on the underlying span.

        Params:
            metadata (Dict): A dictionary of attributes to be set on the span.
        """

        self._span.set_attributes(attributes=metadata)

    def __enter__(self):
        """
        Enters the context of the TracedSpan, returning itself.

        Returns:
            TracedSpan: The instance of TracedSpan.
        """

        return self

    def __exit__(self, _exc_type, _exc_val, _exc_tb):
        """
        Exits the context of the TracedSpan by ending the underlying span.
        """

        self._span.end()


@contextmanager
def start_trace(name: str):
    """
    A context manager that starts a new trace and provides a TracedSpan
    for usage within the context.

    Params:
        name (str): The name of the span.

    Yields:
        TracedSpan: The wrapped span for trace operations.
    """

    __trace = t.get_tracer_provider()
    with __trace.get_tracer(__name__).start_as_current_span(
        name,
        kind=SpanKind.CLIENT,
    ) as span:
        yield TracedSpan(span)
