"""SQL Handler Module.

This module provides a comprehensive SQL database management interface for the JL Automation platform.
It implements a singleton pattern with connection pooling, retry logic, and both asynchronous and
synchronous database operations.

Key Features:
    - Singleton pattern for efficient resource management
    - Connection pooling with configurable min/max sizes
    - Automatic retry logic for transient database errors
    - Both async and sync query execution methods
    - Health check functionality
    - Proper resource cleanup and context management

Example:
    Basic usage:
        sql_manager = SqlManager(config)
        
        # Async query execution
        result = await sql_manager.get_async("SELECT * FROM users WHERE id = ?", "id,name,email", (user_id,))
        
        # Sync query execution (for compatibility)
        result = sql_manager.get_sync("SELECT * FROM users", "id,name,email")
"""

import pandas as pd
from .appconfig_handler import Configurations
import aioodbc
import pyodbc
import logging
from typing import Optional
import asyncio
from contextlib import asynccontextmanager

class SqlManager:
    """Singleton SQL database manager with connection pooling and retry logic.
    
    This class provides a centralized interface for database operations with built-in
    connection pooling, automatic retries, and both synchronous and asynchronous methods.
    The singleton pattern ensures efficient resource utilization across the application.
    
    Class Attributes:
        _instance (Optional[SqlManager]): Singleton instance.
        _lock (asyncio.Lock): Thread-safe lock for singleton creation.
        _pool (Optional[aioodbc.Pool]): Database connection pool.
        _max_retries (int): Maximum number of retry attempts for failed operations.
        _retry_delay (int): Base delay in seconds between retry attempts.
        
    Instance Attributes:
        connection_string (str): Database connection string.
        min_size (int): Minimum number of connections in the pool.
        max_size (int): Maximum number of connections in the pool.
        initialized (bool): Flag indicating if the instance is initialized.
    """
    
    _instance = None
    _lock = asyncio.Lock()
    _pool: Optional[aioodbc.Pool] = None
    _max_retries = 3
    _retry_delay = 1  # seconds
    
    def __new__(cls, config: Configurations):
        """Create or return the singleton instance of SqlManager.
        
        Args:
            config (Configurations): Configuration object containing database settings.
            
        Returns:
            SqlManager: The singleton instance.
        """
        if cls._instance is None:
            cls._instance = super(SqlManager, cls).__new__(cls)
            cls._instance.connection_string = config.JobLogicAutomationDb
            cls._instance.min_size = 1
            cls._instance.max_size = 10
            cls._instance.initialized = True
        return cls._instance

    def __init__(self, config: Configurations):
        """Initialize the SqlManager instance.
        
        Args:
            config (Configurations): Configuration object (used only for singleton creation).
            
        Note:
            This method only runs once due to the singleton pattern implementation.
        """
        pass
            
    async def _get_pool(self) -> aioodbc.Pool:
        """Get or create the database connection pool with retry mechanism.
        
        Returns:
            aioodbc.Pool: The database connection pool instance.
            
        Raises:
            Exception: If connection pool creation fails after all retry attempts.
        """
        if self._pool is None:
            async with self._lock:
                if self._pool is None:
                    try:
                        self._pool = await aioodbc.create_pool(
                            dsn=self.connection_string,
                            minsize=self.min_size,
                            maxsize=self.max_size,
                            autocommit=True
                        )
                        logging.info("SQL connection pool created")
                    except Exception as e:
                        logging.error(f"Failed to create connection pool: {e}")
                        raise
        return self._pool

    async def _execute_with_retry(self, operation):
        """Execute database operation with retry logic for transient failures.
        
        Args:
            operation: Async function that performs the database operation.
            
        Returns:
            Any: Result of the database operation.
            
        Raises:
            pyodbc.Error: If all retry attempts fail.
        """
        for attempt in range(self._max_retries):
            try:
                pool = await self._get_pool()
                async with pool.acquire() as conn:
                    async with conn.cursor() as cursor:
                        return await operation(cursor)
            except pyodbc.Error as e:
                if attempt == self._max_retries - 1:
                    logging.error(f"Final retry attempt failed: {e}")
                    raise
                logging.warning(f"Attempt {attempt + 1} failed, retrying: {e}")
                await asyncio.sleep(self._retry_delay * (attempt + 1))
                # Reset pool on connection errors
                self._pool = None

    @asynccontextmanager
    async def get_connection(self):
        """Async context manager for getting a connection from the pool.
        
        Yields:
            aioodbc.Connection: Database connection from the pool.
            
        Example:
            async with sql_manager.get_connection() as conn:
                async with conn.cursor() as cursor:
                    await cursor.execute("SELECT * FROM table")
        """
        pool = await self._get_pool()
        conn = await pool.acquire()
        try:
            yield conn
        finally:
            await pool.release(conn)

    async def get_async(self, query: str, header: str, params: tuple = None) -> pd.DataFrame:
        """Execute SELECT query asynchronously and return results as DataFrame.
        
        Args:
            query (str): SQL SELECT query to execute.
            header (str): Comma-separated column names for the result DataFrame.
            params (Optional[Tuple]): Parameters for parameterized queries.
            
        Returns:
            pd.DataFrame: Query results as a pandas DataFrame.
            
        Example:
            result = await sql_manager.get_async(
                "SELECT id, name FROM users WHERE active = ?",
                "id,name",
                (True,)
            )
        """
        async def _operation(cursor):
            await cursor.execute(query, params or ())
            rows = await cursor.fetchall()
            columns = [col.strip() for col in header.split(',')]
            data = [dict(zip(columns, row)) for row in rows]
            return pd.DataFrame(data)

        return await self._execute_with_retry(_operation)

    async def execute_async(self, query: str, params: tuple = None) -> None:
        """Execute non-SELECT query asynchronously (INSERT, UPDATE, DELETE).
        
        Args:
            query (str): SQL query to execute.
            params (Optional[Tuple]): Parameters for parameterized queries.
            
        Example:
            await sql_manager.execute_async(
                "UPDATE users SET active = ? WHERE id = ?",
                (False, user_id)
            )
        """
        async def _operation(cursor):
            await cursor.execute(query, params or ())

        await self._execute_with_retry(_operation)

    def get_sync(self, query: str, header: str, params: tuple = None) -> pd.DataFrame:
        """Execute SELECT query synchronously and return results as DataFrame.
        
        This method is provided for backward compatibility with synchronous code.
        For new development, prefer the async methods.
        
        Args:
            query (str): SQL SELECT query to execute.
            header (str): Comma-separated column names for the result DataFrame.
            params (Optional[Tuple]): Parameters for parameterized queries.
            
        Returns:
            pd.DataFrame: Query results as a pandas DataFrame.
            
        Example:
            result = sql_manager.get_sync("SELECT * FROM users", "id,name,email")
        """
        with pyodbc.connect(self.connection_string) as conn:
            with conn.cursor() as cursor:
                cursor.execute(query, params or ())
                rows = cursor.fetchall()
                columns = [col.strip() for col in header.split(',')]
                data = [dict(zip(columns, row)) for row in rows]
                return pd.DataFrame(data)

    async def check_health(self) -> bool:
        """Check if database connection is healthy.
        
        Returns:
            bool: True if the database connection is healthy, False otherwise.
        """
        try:
            await self.execute_async("SELECT 1")
            return True
        except Exception as e:
            logging.error(f"Health check failed: {e}")
            return False

    async def close(self):
        """Close the database connection pool and clean up resources."""
        if self._pool:
            await self._pool.close()
            self._pool = None
            logging.info("SQL connection pool closed")

    async def __aenter__(self):
        """Async context manager entry.
        
        Returns:
            SqlManager: Self reference for context manager usage.
        """
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit with cleanup.
        
        Args:
            exc_type: Exception type if an exception occurred.
            exc_val: Exception value if an exception occurred.
            exc_tb: Exception traceback if an exception occurred.
        """
        await self.close()

    def __del__(self):
        """Ensure resources are cleaned up when the object is destroyed."""
        if self._pool and not self._pool.closed:
            asyncio.create_task(self.close())