"""Comprehensive SQL helper library for database operations with multi-database support."""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_utils_sql.ipynb.

# %% auto #0
__all__ = ['logger', 'get_db_type', 'validate_params', 'run_id', 'insert_only', 'bulk_insert_only', 'upsert', 'bulk_upsert',
           'get_by_id', 'update_record', 'delete_record', 'bulk_delete', 'with_transaction', 'paginate_sql',
           'batch_execute', 'to_cents', 'from_cents']

# %% ../nbs/02_utils_sql.ipynb #91320ee4
from fastsql import *
from sqlalchemy import text
import os
import re
import logging
from typing import List, Dict, Any, Optional
from contextlib import contextmanager
from nbdev.showdoc import show_doc

# Module-level logger - configured by app via configure_logging()
logger = logging.getLogger(__name__)

# %% ../nbs/02_utils_sql.ipynb #36a597d2
def get_db_type():
    """Get database type from environment variable"""
    return os.getenv("DB_TYPE", "SQLITE").upper()

# %% ../nbs/02_utils_sql.ipynb #2ef1cca0
def _extract_params(sql: str) -> List[str]:
    """Extract parameter names from SQL string (e.g., :param_name)"""
    return re.findall(r':(\w+)', sql)

def validate_params(sql: str, params: Dict[str, Any]) -> None:
    """Validate that all required parameters are provided"""
    required = _extract_params(sql)
    provided = set(params.keys()) if params else set()
    missing = set(required) - provided
    
    if missing:
        raise ValueError(f"Missing required parameters: {', '.join(missing)}")

def run_id(db: Database, registry: Dict[str, str], query_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
    """Execute a query by ID from a query registry."""
    # Get query from registry
    if query_id not in registry:
        raise ValueError(f"Query ID '{query_id}' not found in registry")
    
    sql = registry[query_id]
    params = params or {}
    
    # Validate parameters
    validate_params(sql, params)
    
    # Execute query
    try:
        result = db.conn.execute(text(sql), params)
        return result
    except Exception as e:
        logger.error(f"Query '{query_id}' failed: {e}", exc_info=True)
        raise Exception(f"Failed to execute query '{query_id}': {str(e)}") from e


# %% ../nbs/02_utils_sql.ipynb #51a96e11
def insert_only(db: Database, table_name: str, record: Dict[str, Any], conflict_cols: List[str], auto_commit: bool = True) -> None:
    """Insert a single record only if it doesn't exist (ignores conflicts)."""
    db_type = get_db_type()
    columns = list(record.keys())
    placeholders = [f":{col}" for col in columns]
    
    if db_type == "POSTGRESQL":
        # ON CONFLICT DO NOTHING
        conflict_clause = f"ON CONFLICT ({', '.join(conflict_cols)}) DO NOTHING"
        sql = f"""
            INSERT INTO {table_name} ({', '.join(columns)})
            VALUES ({', '.join(placeholders)})
            {conflict_clause}
        """
    else:  # SQLite
        # INSERT OR IGNORE
        sql = f"""
            INSERT OR IGNORE INTO {table_name} ({', '.join(columns)})
            VALUES ({', '.join(placeholders)})
        """
    
    try:
        db.conn.execute(text(sql), record)
        if auto_commit:
            db.conn.commit()
    except Exception as e:
        logger.error(f"Insert into '{table_name}' failed: {e}", exc_info=True)
        if auto_commit:
            db.conn.rollback()
        raise Exception(f"Failed to insert record into {table_name}: {str(e)}") from e

def bulk_insert_only(db: Database, table_name: str, records: List[Dict[str, Any]], conflict_cols: List[str], auto_commit: bool = True) -> None:
    """Insert multiple records, skipping conflicts (optimized batch operation)."""
    if not records:
        return
    
    db_type = get_db_type()
    columns = list(records[0].keys())
    placeholders = [f":{col}" for col in columns]
    
    if db_type == "POSTGRESQL":
        conflict_clause = f"ON CONFLICT ({', '.join(conflict_cols)}) DO NOTHING"
        sql = f"""
            INSERT INTO {table_name} ({', '.join(columns)})
            VALUES ({', '.join(placeholders)})
            {conflict_clause}
        """
    else:  # SQLite
        sql = f"""
            INSERT OR IGNORE INTO {table_name} ({', '.join(columns)})
            VALUES ({', '.join(placeholders)})
        """
    
    try:
        for record in records:
            db.conn.execute(text(sql), record)
        if auto_commit:
            db.conn.commit()
    except Exception as e:
        logger.error(f"Bulk insert into '{table_name}' failed: {e}", exc_info=True)
        if auto_commit:
            db.conn.rollback()
        raise Exception(f"Failed to bulk insert into {table_name}: {str(e)}") from e

# %% ../nbs/02_utils_sql.ipynb #dddbca24
def upsert(db: Database, table_name: str, record: Dict[str, Any], 
           conflict_cols: List[str], update_cols: Optional[List[str]] = None, auto_commit: bool = True) -> None:
    """Insert a record or update if it exists (upsert)."""
    db_type = get_db_type()
    columns = list(record.keys())
    placeholders = [f":{col}" for col in columns]
    
    # Determine which columns to update
    if update_cols is None:
        update_cols = [col for col in columns if col not in conflict_cols]
    
    if db_type == "POSTGRESQL":
        # ON CONFLICT DO UPDATE
        update_set = ', '.join([f"{col} = EXCLUDED.{col}" for col in update_cols])
        sql = f"""
            INSERT INTO {table_name} ({', '.join(columns)})
            VALUES ({', '.join(placeholders)})
            ON CONFLICT ({', '.join(conflict_cols)}) 
            DO UPDATE SET {update_set}
        """
    else:  # SQLite
        # INSERT OR REPLACE
        sql = f"""
            INSERT OR REPLACE INTO {table_name} ({', '.join(columns)})
            VALUES ({', '.join(placeholders)})
        """
    
    try:
        db.conn.execute(text(sql), record)
        if auto_commit:
            db.conn.commit()
    except Exception as e:
        logger.error(f"Upsert into '{table_name}' failed: {e}", exc_info=True)
        if auto_commit:
            db.conn.rollback()
        raise Exception(f"Failed to upsert record into {table_name}: {str(e)}") from e

def bulk_upsert(db: Database, table_name: str, records: List[Dict[str, Any]], 
                conflict_cols: List[str], update_cols: Optional[List[str]] = None, auto_commit: bool = True) -> None:
    """Insert or update multiple records (optimized batch operation)."""
    if not records:
        return
    
    db_type = get_db_type()
    columns = list(records[0].keys())
    placeholders = [f":{col}" for col in columns]
    
    # Determine which columns to update
    if update_cols is None:
        update_cols = [col for col in columns if col not in conflict_cols]
    
    if db_type == "POSTGRESQL":
        update_set = ', '.join([f"{col} = EXCLUDED.{col}" for col in update_cols])
        sql = f"""
            INSERT INTO {table_name} ({', '.join(columns)})
            VALUES ({', '.join(placeholders)})
            ON CONFLICT ({', '.join(conflict_cols)}) 
            DO UPDATE SET {update_set}
        """
    else:  # SQLite
        sql = f"""
            INSERT OR REPLACE INTO {table_name} ({', '.join(columns)})
            VALUES ({', '.join(placeholders)})
        """
    
    try:
        for record in records:
            db.conn.execute(text(sql), record)
        if auto_commit:
            db.conn.commit()
    except Exception as e:
        logger.error(f"Bulk upsert into '{table_name}' failed: {e}", exc_info=True)
        if auto_commit:
            db.conn.rollback()
        raise Exception(f"Failed to bulk upsert into {table_name}: {str(e)}") from e

# %% ../nbs/02_utils_sql.ipynb #71ef6dac
def get_by_id(db: Database, table_name: str, id_value: Any, id_col: str = "id") -> Any:
    """Get a single record by ID."""
    sql = f"SELECT * FROM {table_name} WHERE {id_col} = :id_value"
    try:
        result = db.conn.execute(text(sql), {"id_value": id_value})
        return result
    except Exception as e:
        logger.error(f"Get by ID from '{table_name}' failed: {e}", exc_info=True)
        raise Exception(f"Failed to get record from {table_name}: {str(e)}") from e

def update_record(db: Database, table_name: str, id_value: Any, 
                  id_col: str = "id", auto_commit: bool = True, **updates) -> None:
    """Update a single record by ID."""
    if not updates:
        return
    
    set_clause = ', '.join([f"{col} = :{col}" for col in updates.keys()])
    sql = f"UPDATE {table_name} SET {set_clause} WHERE {id_col} = :id_value"
    
    params = {**updates, "id_value": id_value}
    
    try:
        db.conn.execute(text(sql), params)
        if auto_commit:
            db.conn.commit()
    except Exception as e:
        logger.error(f"Update record in '{table_name}' failed: {e}", exc_info=True)
        if auto_commit:
            db.conn.rollback()
        raise Exception(f"Failed to update record in {table_name}: {str(e)}") from e

def delete_record(db: Database, table_name: str, id_value: Any, id_col: str = "id", auto_commit: bool = True) -> None:
    """Delete a single record by ID."""
    sql = f"DELETE FROM {table_name} WHERE {id_col} = :id_value"
    
    try:
        db.conn.execute(text(sql), {"id_value": id_value})
        if auto_commit:
            db.conn.commit()
    except Exception as e:
        logger.error(f"Delete from '{table_name}' failed: {e}", exc_info=True)
        if auto_commit:
            db.conn.rollback()
        raise Exception(f"Failed to delete record from {table_name}: {str(e)}") from e

def bulk_delete(db: Database, table_name: str, id_list: List[Any], id_col: str = "id", auto_commit: bool = True) -> None:
    """Delete multiple records by ID list."""
    if not id_list:
        return
    
    # Create placeholder for IN clause
    placeholders = ', '.join([f":id_{i}" for i in range(len(id_list))])
    sql = f"DELETE FROM {table_name} WHERE {id_col} IN ({placeholders})"
    
    # Build params dict
    params = {f"id_{i}": val for i, val in enumerate(id_list)}
    
    try:
        db.conn.execute(text(sql), params)
        if auto_commit:
            db.conn.commit()
    except Exception as e:
        logger.error(f"Bulk delete from '{table_name}' failed: {e}", exc_info=True)
        if auto_commit:
            db.conn.rollback()
        raise Exception(f"Failed to bulk delete from {table_name}: {str(e)}") from e

# %% ../nbs/02_utils_sql.ipynb #ab3fdf8e
@contextmanager
def with_transaction(db: Database):
    """Context manager for safe transaction handling with auto-rollback on error."""
    try:
        yield db
        db.conn.commit()
    except Exception as e:
        db.conn.rollback()
        raise e

def paginate_sql(sql: str, page: int, page_size: int) -> str:
    """Add LIMIT/OFFSET pagination to a SQL query."""
    offset = (page - 1) * page_size
    return f"{sql.rstrip(';')} LIMIT {page_size} OFFSET {offset}"

def batch_execute(db: Database, operation_func, items: List[Any], batch_size: int = 100) -> None:
    """Execute an operation on items in batches with commits after each batch."""
    for i in range(0, len(items), batch_size):
        batch = items[i:i + batch_size]
        try:
            for item in batch:
                operation_func(db, item)
            db.conn.commit()
        except Exception as e:
            logger.error(f"Batch execution failed at batch {i//batch_size + 1}: {e}", exc_info=True)
            db.conn.rollback()
            raise Exception(f"Batch execution failed at batch {i//batch_size + 1}: {str(e)}") from e

# %% ../nbs/02_utils_sql.ipynb #dba19bea
def to_cents(dollars: str | float | None) -> int | None:
    """Convert dollar amount to integer cents for database storage."""
    if dollars is None or dollars == '':
        return None
    try:
        return int(float(dollars) * 100)
    except (ValueError, TypeError):
        return None


def from_cents(cents: int | None) -> str:
    """Convert integer cents to formatted dollar string for display."""
    if cents is None:
        return "$0.00"
    dollars = cents / 100
    if dollars < 0:
        return f"-${abs(dollars):,.2f}"
    return f"${dollars:,.2f}"
