import logging
import threading
from typing import Callable, Any

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class ConnectionPool:
    """
    A class that implements a connection pool for efficient connection management.
    """

    def __init__(self, max_connections: int, connection_factory: Callable[[], Any]):
        """
        Initializes the connection pool.

        Args:
            max_connections: The maximum number of connections in the pool.
            connection_factory: A function that creates a new connection.
        """
        self._max_connections = max_connections
        self._connection_factory = connection_factory
        self._connections = []
        self._lock = threading.Lock()
        self._num_active_connections = 0
        logging.info(f"Connection pool initialized with max connections: {max_connections}")

    def get_connection(self) -> Any:
        """
        Retrieves a connection from the pool. Creates a new connection if none are available and the maximum number of connections has not been reached.

        Returns:
            A connection object.
        """
        with self._lock:
            if self._connections:
                logging.info("Reusing existing connection from pool.")
                return self._connections.pop()

            if self._num_active_connections < self._max_connections:
                try:
                    connection = self._connection_factory()
                    self._num_active_connections += 1
                    logging.info("Creating a new connection.")
                    return connection
                except Exception as e:
                    logging.error(f"Error creating connection: {e}")
                    raise

            logging.warning("Maximum connections reached. No connections available.")
            return None  # Or raise an exception if that's more appropriate

    def release_connection(self, connection: Any) -> None:
        """
        Releases a connection back to the pool.

        Args:
            connection: The connection object to release.
        """
        if connection is None:
            logging.warning("Attempting to release a None connection. Ignoring.")
            return

        with self._lock:
            if len(self._connections) < self._max_connections:
                self._connections.append(connection)
                logging.info("Connection released back to the pool.")
            else:
                try:
                    connection.close()  # Or whatever the appropriate method is
                    self._num_active_connections -= 1
                    logging.info("Connection closed as pool is full.")
                except Exception as e:
                    logging.error(f"Error closing connection: {e}")
            
    def __enter__(self):
        """Context management for with statement."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Cleanup resources upon exiting context."""
        with self._lock:
            for connection in self._connections:
                try:
                    connection.close()
                    self._num_active_connections -= 1
                except Exception as e:
                    logging.error(f"Error closing connection during cleanup: {e}")
            self._connections = []