"""Connection session management for sqlit.

This module provides a ConnectionSession class that owns the lifecycle
of database connections and SSH tunnels, ensuring proper cleanup.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING, Any, cast

if TYPE_CHECKING:
    from sqlit.domains.connections.domain.config import ConnectionConfig
    from sqlit.domains.connections.providers.model import DatabaseProvider

    from .executor import DatabaseExecutor


class ConnectionSession:
    """A database connection session with automatic cleanup.

    This class encapsulates a database connection and optional SSH tunnel,
    providing context manager support for guaranteed resource cleanup.

    Usage:
        with ConnectionSession.create(config) as session:
            result = session.provider.query_executor.execute_query(session.connection, query)

    Or for long-lived connections (like in the TUI):
        session = ConnectionSession.create(config)
        # ... use session ...
        session.close()  # Must be called explicitly

    Attributes:
        connection: The raw database connection object.
        provider: The database provider for this connection.
        config: The original connection configuration.
        has_tunnel: Whether this session has an active SSH tunnel.
    """

    def __init__(
        self,
        connection: Any,
        provider: DatabaseProvider,
        config: ConnectionConfig,
        tunnel: Any | None = None,
    ):
        """Initialize a connection session.

        Args:
            connection: The database connection object.
            provider: The database provider instance.
            config: The connection configuration.
            tunnel: Optional SSH tunnel object.
        """
        self._connection = connection
        self._provider = provider
        self._config = config
        self._tunnel = tunnel
        self._closed = False
        self._executor: DatabaseExecutor | None = None

    @classmethod
    def create(
        cls,
        config: ConnectionConfig,
        provider_factory: Callable[[str], DatabaseProvider] | None = None,
        tunnel_factory: Callable[[ConnectionConfig], tuple[Any, str, int]] | None = None,
    ) -> ConnectionSession:
        """Create a new connection session.

        This factory method handles SSH tunnel creation (if enabled) and
        establishes the database connection.

        Args:
            config: Connection configuration.
        provider_factory: Optional factory for creating providers.
            Defaults to get_provider from the provider catalog.
            tunnel_factory: Optional factory for creating SSH tunnels.
                Defaults to create_ssh_tunnel from the connection tunnel module.

        Returns:
            A new ConnectionSession instance.

        Raises:
            ValueError: If SSH key file is not found.
            ImportError: If required database driver is not installed.
            Any database-specific connection errors.
        """
        from sqlit.domains.connections.app.tunnel import create_ssh_tunnel
        from sqlit.domains.connections.providers.adapter_provider import build_adapter_provider
        from sqlit.domains.connections.providers.catalog import get_provider, get_provider_schema, get_provider_spec
        from sqlit.domains.connections.providers.config_service import normalize_connection_config
        from sqlit.domains.connections.providers.model import DatabaseProvider

        get_provider_fn = provider_factory or get_provider
        create_tunnel_fn = tunnel_factory or create_ssh_tunnel

        config = normalize_connection_config(config)

        tunnel, host, port = create_tunnel_fn(config)

        # Adjust config for tunnel if created
        if tunnel:
            connect_config = config.with_endpoint(host=host, port=str(port))
        else:
            connect_config = config

        # Get provider and connect
        provider_candidate = get_provider_fn(config.db_type)
        if isinstance(provider_candidate, DatabaseProvider):
            provider = provider_candidate
        elif hasattr(provider_candidate, "connection_factory"):
            provider = cast(DatabaseProvider, provider_candidate)
        else:
            adapter = provider_candidate
            spec = get_provider_spec(config.db_type)
            schema = get_provider_schema(config.db_type)
            provider = build_adapter_provider(spec, schema, adapter)
        connection = provider.connection_factory.connect(connect_config)
        try:
            provider.post_connect(connection, config)
        except Exception:
            pass

        return cls(connection, provider, config, tunnel)

    @property
    def connection(self) -> Any:
        """Get the raw database connection object."""
        return self._connection

    @property
    def provider(self) -> DatabaseProvider:
        """Get the database provider."""
        return self._provider

    @property
    def adapter(self) -> Any:
        """Compatibility alias for the underlying adapter."""
        return self._provider.connection_factory

    @property
    def config(self) -> ConnectionConfig:
        """Get the connection configuration."""
        return self._config

    @property
    def tunnel(self) -> Any | None:
        """Get the SSH tunnel object if present."""
        return self._tunnel

    @property
    def has_tunnel(self) -> bool:
        """Check if this session has an active SSH tunnel."""
        return self._tunnel is not None

    @property
    def is_closed(self) -> bool:
        """Check if this session has been closed."""
        return self._closed

    @property
    def executor(self) -> DatabaseExecutor:
        """Get or create the database executor for serialized operations.

        The executor is lazily created on first access. All database operations
        should go through this executor to ensure thread-safe access.

        Returns:
            The DatabaseExecutor for this session.

        Raises:
            RuntimeError: If the session has been closed.
        """
        if self._closed:
            raise RuntimeError("Cannot get executor for closed session")
        if self._executor is None:
            from .executor import DatabaseExecutor

            self._executor = DatabaseExecutor(self)
        return self._executor

    def switch_database(self, database: str) -> None:
        """Switch to a different database without recreating the session.

        This is used for databases like PostgreSQL that don't support
        cross-database queries. It closes the current connection and
        opens a new one to the specified database, reusing the SSH tunnel.

        Args:
            database: The database name to switch to.

        Raises:
            RuntimeError: If the session has been closed.
            Any database-specific connection errors.
        """
        if self._closed:
            raise RuntimeError("Cannot switch database on closed session")

        # Create new config with the database
        new_config = self._config.with_endpoint(database=database)

        # Determine connection config (use tunnel if present)
        if self._tunnel:
            # Reuse tunnel - get local bind address
            local_host, local_port = self._tunnel.local_bind_address
            connect_config = new_config.with_endpoint(host=local_host, port=str(local_port))
        else:
            connect_config = new_config

        # Close old connection (but keep tunnel)
        if self._connection is not None:
            try:
                close_fn = getattr(self._connection, "close", None)
                if callable(close_fn):
                    close_fn()
            except Exception:
                pass

        # Open new connection
        self._connection = self._provider.connection_factory.connect(connect_config)
        self._config = new_config

    def close(self) -> None:
        """Close the session and release all resources.

        This method is idempotent - calling it multiple times is safe.
        It shuts down the executor first (to stop pending operations),
        then closes the database connection, then stops the SSH tunnel.
        Exceptions during cleanup are silently caught to ensure all
        resources are released.
        """
        if self._closed:
            return

        # Shutdown executor first (don't wait for pending operations)
        if self._executor is not None:
            try:
                self._executor.shutdown(wait=False)
            except Exception:
                pass
            self._executor = None

        # Close database connection
        if self._connection is not None:
            try:
                close_fn = getattr(self._connection, "close", None)
                if callable(close_fn):
                    close_fn()
            except Exception:
                pass
            self._connection = None

        # Stop SSH tunnel
        if self._tunnel is not None:
            try:
                self._tunnel.stop()
            except Exception:
                pass
            self._tunnel = None

        self._closed = True

    def __enter__(self) -> ConnectionSession:
        """Enter the context manager."""
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Exit the context manager, ensuring cleanup."""
        self.close()

    def __del__(self) -> None:
        """Destructor to catch unclosed sessions (best-effort cleanup)."""
        if not self._closed:
            self.close()
