import asyncio
import os
from typing import Optional, Union

# Check if connector should be disabled
# Accept common truthy values: true, 1, yes, on (case-insensitive)
CONNECTOR_DISABLED = os.getenv("RHESIS_CONNECTOR_DISABLED", "false").lower() in (
    "true",
    "1",
    "yes",
    "on",
)


class DisabledClient:
    """
    No-op client implementation used when RHESIS_CONNECTOR_DISABLED is enabled.

    This client accepts all initialization parameters and method calls but
    performs no actual operations. It's used to allow code to run without
    connector/observability overhead in test and CI environments.

    Enabled with: RHESIS_CONNECTOR_DISABLED=true|1|yes|on (case-insensitive)

    When DisabledClient is active:
    - @endpoint and @observe decorators return the original function unmodified
    - No telemetry initialization occurs
    - No connector manager is created
    - All method calls are no-ops
    """

    def __init__(self, *args, **kwargs):
        """Accept any initialization parameters and register as default client."""
        from rhesis.sdk import decorators

        decorators._register_default_client(self)
        import logging

        logger = logging.getLogger(__name__)
        logger.info("✅ DisabledClient initialized successfully")

    @property
    def is_disabled(self) -> bool:
        """Return True to indicate this is a disabled client."""
        return True

    def __getattr__(self, name):
        """
        Handle any method call as a no-op.

        This ensures all Client methods work without needing to explicitly
        implement each one in DisabledClient.

        Returns:
            A function that accepts any arguments and returns None
        """

        def noop(*args, **kwargs):
            return None

        return noop

    @property
    def base_url(self) -> str:
        """Return empty string for base_url property."""
        return ""

    @property
    def project_id(self) -> Optional[str]:
        """Return None for project_id property."""
        return None

    @property
    def environment(self) -> str:
        """Return empty string for environment property."""
        return ""

    def connect(self) -> None:
        """No-op when connector is disabled; returns immediately."""
        return


class RhesisClient:
    """
    Rhesis client with observability and telemetry capabilities.

    This is the main user-facing client for applications that need:
    - OpenTelemetry tracing (@observe decorator)
    - Remote function execution (@endpoint decorator)
    - Automatic instrumentation

    Users should create this via `RhesisClient.from_environment()` for
    automatic configuration from environment variables.

    Example:
        ```python
        from rhesis.sdk import RhesisClient

        # Recommended: environment-based initialization
        client = RhesisClient.from_environment()

        # Or explicit configuration
        client = RhesisClient(
            api_key="your-key",
            project_id="your-project",
            environment="production"
        )
        ```
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        project_id: Optional[str] = None,
        environment: Optional[str] = None,
    ):
        """
        Initialize the Rhesis observability client.

        Args:
            api_key: Optional API key. If not provided, will try to get it from
                    module level variable or environment variable.
            base_url: Optional base URL. If not provided, will try to get it from
                     module level variable or environment variable.
            project_id: Optional project ID for remote endpoint testing. If not provided,
                       will try to get from RHESIS_PROJECT_ID environment variable.
            environment: Optional environment name. If not provided, will try to get
                        from RHESIS_ENVIRONMENT environment variable (default: "development").
        """
        from rhesis.sdk.config import get_api_key, get_base_url

        # API configuration
        self.api_key = api_key if api_key is not None else get_api_key()
        self._base_url = base_url if base_url is not None else get_base_url()

        # Observability configuration
        self.project_id = project_id or os.getenv("RHESIS_PROJECT_ID")
        self.environment = environment or os.getenv("RHESIS_ENVIRONMENT", "development")

        # Lazy connector (not initialized yet)
        self._connector_manager = None

        # Initialize OpenTelemetry tracer provider for @observe decorator
        self._init_telemetry()

        # Create tracer instance for direct tracing
        self._init_tracer()

        # Automatically register as default client for decorators
        self._register_as_default()

    @classmethod
    def from_environment(cls) -> Union["RhesisClient", DisabledClient]:
        """
        Create a RhesisClient from environment variables.

        This is the recommended way to initialize the client in applications.
        Returns a DisabledClient if RHESIS_CONNECTOR_DISABLED is set or if
        required credentials (RHESIS_PROJECT_ID, RHESIS_API_KEY) are missing.

        Environment Variables:
            RHESIS_CONNECTOR_DISABLED: Set to 'true' to disable the connector
            RHESIS_PROJECT_ID: Required project ID
            RHESIS_API_KEY: Required API key
            RHESIS_ENVIRONMENT: Optional, defaults to 'development'
            RHESIS_BASE_URL: Optional, defaults to 'http://localhost:8080'

        Returns:
            RhesisClient or DisabledClient instance
        """
        import logging

        logger = logging.getLogger(__name__)

        if CONNECTOR_DISABLED:
            logger.info("Connector explicitly disabled (RHESIS_CONNECTOR_DISABLED=true)")
            return DisabledClient()

        return cls(
            project_id=os.getenv("RHESIS_PROJECT_ID"),
            api_key=os.getenv("RHESIS_API_KEY"),
            environment=os.getenv("RHESIS_ENVIRONMENT", "development"),
            base_url=os.getenv("RHESIS_BASE_URL", "http://localhost:8080"),
        )

    def _init_telemetry(self) -> None:
        """
        Initialize OpenTelemetry tracer provider.

        Raises:
            RuntimeError: If telemetry initialization fails
        """
        import logging

        logger = logging.getLogger(__name__)

        try:
            from rhesis.sdk.telemetry.provider import get_tracer_provider

            # Initialize OTEL provider with current client config
            provider = get_tracer_provider(
                service_name="rhesis-sdk",
                api_key=self.api_key,
                base_url=self._base_url,
                project_id=self.project_id or "unknown",
                environment=self.environment,
            )

            # Verify provider was initialized correctly
            if provider is None:
                raise RuntimeError("TracerProvider initialization returned None")

            # Log successful initialization
            logger.info(
                f"✅ Telemetry initialized successfully\n"
                f"   Project: {self.project_id or 'unknown'}\n"
                f"   Environment: {self.environment}\n"
                f"   Endpoint: {self._base_url}/telemetry/traces\n"
                f"   Note: Traces are batched and exported every 5 seconds"
            )

        except ImportError as e:
            logger.error(f"❌ Failed to import telemetry modules: {e}")
            raise RuntimeError(
                f"Telemetry initialization failed: Missing dependencies. "
                f"Make sure opentelemetry-sdk is installed. Error: {e}"
            ) from e

        except Exception as e:
            logger.error(
                f"❌ Failed to initialize telemetry: {e}\n"
                f"   API Key: {'SET' if self.api_key else 'NOT SET'}\n"
                f"   Base URL: {self._base_url}\n"
                f"   Project ID: {self.project_id or 'NOT SET'}"
            )
            raise RuntimeError(
                f"Telemetry initialization failed: {e}. "
                f"Check your API key, base URL, and backend connectivity."
            ) from e

    def _init_tracer(self) -> None:
        """
        Initialize Tracer instance for direct tracing.

        This allows @endpoint decorated functions to be traced even when
        the connector manager is not active (e.g., direct HTTP calls).
        """
        from rhesis.sdk.telemetry import Tracer

        self._tracer = Tracer(
            api_key=self.api_key,
            project_id=self.project_id or "unknown",
            environment=self.environment,
            base_url=self._base_url,
        )

    def _register_as_default(self) -> None:
        """Register this client as the default for decorators."""
        # Import here to avoid circular dependency
        from rhesis.sdk import decorators

        decorators._register_default_client(self)

    def _ensure_connector(self):
        """Lazy-initialize connector when first needed."""
        if self._connector_manager is None:
            if not self.project_id:
                raise RuntimeError(
                    "@endpoint requires project_id parameter or "
                    "RHESIS_PROJECT_ID environment variable"
                )
            from rhesis.sdk.connector.manager import ConnectorManager

            self._connector_manager = ConnectorManager(
                api_key=self.api_key,
                project_id=self.project_id,
                environment=self.environment,
                base_url=self._base_url,
            )
            self._connector_manager.initialize()
        return self._connector_manager

    def register_endpoint(self, name: str, func, metadata: dict) -> None:
        """
        Register a function as a remotely callable endpoint.

        Args:
            name: Endpoint function name
            func: Function callable
            metadata: Additional metadata
        """
        connector = self._ensure_connector()  # Lazy init
        connector.register_function(name, func, metadata)

    async def _run_async(self) -> None:
        """
        Run the connector (WebSocket) and block until shutdown.

        Ensures the connector is initialized and the connection task is started,
        then waits indefinitely. On cancellation or interrupt, shuts down cleanly.
        """
        self._ensure_connector()
        _green = "\033[92m"
        _dim = "\033[2m"
        _reset = "\033[0m"
        print(f"{_green}Connector running.{_reset} {_dim}Press Ctrl+C to stop.{_reset}")
        self._connector_manager._ensure_connection()
        shutdown_event = asyncio.Event()
        try:
            await shutdown_event.wait()
        except asyncio.CancelledError:
            pass
        finally:
            await self._connector_manager.shutdown()

    def connect(self) -> None:
        """
        Block until interrupted (e.g. Ctrl+C); run the connector and receive
        execute-test messages over WebSocket.

        For sync scripts that only define endpoints and do not run a web server.
        Raises RuntimeError if called from a context that already has a running
        event loop (e.g. Jupyter); use an async entry point or run the connector
        as a task in that loop instead.
        """
        try:
            asyncio.get_running_loop()
        except RuntimeError:
            # No running loop: safe to call asyncio.run()
            pass
        else:
            raise RuntimeError(
                "connect() is for sync scripts only. You already have a "
                "running event loop; run the connector as a task in that loop "
                "or use an async entry point instead."
            )
        try:
            asyncio.run(self._run_async())
        except KeyboardInterrupt:
            print("Stopped.")
            return
