# createsonline/performance/connection_pool.py
"""
Connection pooling for CREATESONLINE.

Provides both sync and async connection pools for databases,
HTTP connections, and generic resources.
"""

import time
import asyncio
import threading
from collections import deque
from typing import Any, Callable, Optional


class ConnectionPool:
    """
    Thread-safe connection pool for synchronous connections.
    
    Usage::
    
        pool = ConnectionPool(
            create_fn=lambda: sqlite3.connect("app.db"),
            max_size=10,
        )
        
        conn = pool.acquire()
        try:
            cursor = conn.cursor()
            cursor.execute("SELECT * FROM users")
        finally:
            pool.release(conn)
        
        # Or use context manager:
        with pool.connection() as conn:
            conn.execute("...")
    """
    
    def __init__(self, create_fn: Callable = None, close_fn: Callable = None,
                 validate_fn: Callable = None, max_size: int = 10,
                 min_size: int = 1, max_idle_time: float = 300.0,
                 acquire_timeout: float = 30.0):
        self.create_fn = create_fn
        self.close_fn = close_fn or (lambda c: getattr(c, 'close', lambda: None)())
        self.validate_fn = validate_fn
        self.max_size = max_size
        self.min_size = min_size
        self.max_idle_time = max_idle_time
        self.acquire_timeout = acquire_timeout
        
        self._pool: deque = deque()          # (connection, last_used_time)
        self._in_use: set = set()
        self._lock = threading.Lock()
        self._semaphore = threading.Semaphore(max_size)
        
        self._total_created = 0
        self._total_reused = 0
        self._total_closed = 0
        
        # Pre-warm pool
        if create_fn:
            for _ in range(min_size):
                conn = self._create_connection()
                if conn is not None:
                    self._pool.append((conn, time.time()))
    
    def _create_connection(self) -> Any:
        """Create a new connection."""
        if self.create_fn is None:
            raise RuntimeError("No create_fn provided to ConnectionPool")
        conn = self.create_fn()
        self._total_created += 1
        return conn
    
    def _validate(self, conn) -> bool:
        """Check if a connection is still valid."""
        if self.validate_fn:
            try:
                return self.validate_fn(conn)
            except Exception:
                return False
        return True
    
    def acquire(self, timeout: float = None) -> Any:
        """
        Acquire a connection from the pool.
        
        Returns a connection object. Must be returned via release().
        """
        timeout = timeout or self.acquire_timeout
        
        if not self._semaphore.acquire(timeout=timeout):
            raise TimeoutError(
                f"Could not acquire connection within {timeout}s "
                f"(pool size: {self.max_size})"
            )
        
        with self._lock:
            # Try to reuse an idle connection
            now = time.time()
            while self._pool:
                conn, last_used = self._pool.popleft()
                
                # Check idle timeout
                if now - last_used > self.max_idle_time:
                    self._close_connection(conn)
                    continue
                
                # Validate connection
                if self._validate(conn):
                    self._in_use.add(id(conn))
                    self._total_reused += 1
                    return conn
                else:
                    self._close_connection(conn)
            
            # No idle connections available — create a new one
            conn = self._create_connection()
            self._in_use.add(id(conn))
            return conn
    
    def release(self, conn):
        """Return a connection to the pool."""
        with self._lock:
            conn_id = id(conn)
            if conn_id in self._in_use:
                self._in_use.discard(conn_id)
                
                if self._validate(conn) and len(self._pool) < self.max_size:
                    self._pool.append((conn, time.time()))
                else:
                    self._close_connection(conn)
            
            self._semaphore.release()
    
    def _close_connection(self, conn):
        """Close a connection."""
        try:
            self.close_fn(conn)
        except Exception:
            pass
        self._total_closed += 1
    
    def connection(self):
        """Context manager for acquiring/releasing connections."""
        return _ConnectionContextSync(self)
    
    def close_all(self):
        """Close all connections in the pool."""
        with self._lock:
            while self._pool:
                conn, _ = self._pool.popleft()
                self._close_connection(conn)
    
    @property
    def size(self) -> int:
        return len(self._pool) + len(self._in_use)
    
    @property
    def idle_count(self) -> int:
        return len(self._pool)
    
    @property
    def active_count(self) -> int:
        return len(self._in_use)
    
    @property
    def stats(self) -> dict:
        return {
            'pool_size': self.size,
            'idle': self.idle_count,
            'active': self.active_count,
            'max_size': self.max_size,
            'total_created': self._total_created,
            'total_reused': self._total_reused,
            'total_closed': self._total_closed,
        }
    
    def __repr__(self):
        return f"ConnectionPool(idle={self.idle_count}, active={self.active_count})"
    
    def __del__(self):
        self.close_all()


class _ConnectionContextSync:
    def __init__(self, pool):
        self._pool = pool
        self._conn = None
    
    def __enter__(self):
        self._conn = self._pool.acquire()
        return self._conn
    
    def __exit__(self, *args):
        if self._conn is not None:
            self._pool.release(self._conn)
            self._conn = None


class AsyncConnectionPool:
    """
    Async connection pool for asyncio-based applications.
    
    Usage::
    
        pool = AsyncConnectionPool(
            create_fn=lambda: aiosqlite.connect("app.db"),
            max_size=10,
        )
        await pool.initialize()
        
        async with pool.connection() as conn:
            await conn.execute("SELECT 1")
        
        await pool.close_all()
    """
    
    def __init__(self, create_fn: Callable = None, close_fn: Callable = None,
                 validate_fn: Callable = None, max_size: int = 10,
                 min_size: int = 1, max_idle_time: float = 300.0,
                 acquire_timeout: float = 30.0):
        self.create_fn = create_fn
        self.close_fn = close_fn
        self.validate_fn = validate_fn
        self.max_size = max_size
        self.min_size = min_size
        self.max_idle_time = max_idle_time
        self.acquire_timeout = acquire_timeout
        
        self._pool: deque = deque()
        self._in_use: set = set()
        self._semaphore: Optional[asyncio.Semaphore] = None
        self._lock: Optional[asyncio.Lock] = None
        
        self._total_created = 0
        self._total_reused = 0
    
    async def initialize(self):
        """Initialize the async pool (call once at startup)."""
        self._semaphore = asyncio.Semaphore(self.max_size)
        self._lock = asyncio.Lock()
        
        if self.create_fn:
            for _ in range(self.min_size):
                conn = await self._create_connection()
                if conn is not None:
                    self._pool.append((conn, time.time()))
    
    async def _create_connection(self) -> Any:
        if self.create_fn is None:
            raise RuntimeError("No create_fn provided")
        conn = self.create_fn()
        if asyncio.iscoroutine(conn):
            conn = await conn
        self._total_created += 1
        return conn
    
    async def _close_connection(self, conn):
        if self.close_fn:
            result = self.close_fn(conn)
            if asyncio.iscoroutine(result):
                await result
        elif hasattr(conn, 'close'):
            result = conn.close()
            if asyncio.iscoroutine(result):
                await result
    
    async def acquire(self, timeout: float = None) -> Any:
        timeout = timeout or self.acquire_timeout
        if self._semaphore is None:
            await self.initialize()
        
        try:
            await asyncio.wait_for(self._semaphore.acquire(), timeout=timeout)
        except asyncio.TimeoutError:
            raise TimeoutError(f"Could not acquire connection within {timeout}s")
        
        async with self._lock:
            now = time.time()
            while self._pool:
                conn, last_used = self._pool.popleft()
                if now - last_used > self.max_idle_time:
                    await self._close_connection(conn)
                    continue
                self._in_use.add(id(conn))
                self._total_reused += 1
                return conn
            
            conn = await self._create_connection()
            self._in_use.add(id(conn))
            return conn
    
    async def release(self, conn):
        async with self._lock:
            self._in_use.discard(id(conn))
            if len(self._pool) < self.max_size:
                self._pool.append((conn, time.time()))
            else:
                await self._close_connection(conn)
            self._semaphore.release()
    
    def connection(self):
        return _ConnectionContextAsync(self)
    
    async def close_all(self):
        if self._lock:
            async with self._lock:
                while self._pool:
                    conn, _ = self._pool.popleft()
                    await self._close_connection(conn)
    
    @property
    def stats(self) -> dict:
        return {
            'idle': len(self._pool),
            'active': len(self._in_use),
            'max_size': self.max_size,
            'total_created': self._total_created,
            'total_reused': self._total_reused,
        }
    
    def __repr__(self):
        return f"AsyncConnectionPool(idle={len(self._pool)}, active={len(self._in_use)})"


class _ConnectionContextAsync:
    def __init__(self, pool):
        self._pool = pool
        self._conn = None
    
    async def __aenter__(self):
        self._conn = await self._pool.acquire()
        return self._conn
    
    async def __aexit__(self, *args):
        if self._conn is not None:
            await self._pool.release(self._conn)
            self._conn = None
