import datetime
import hashlib
import json
import logging
import os
import requests
from functools import wraps
from typing import Optional

from fastapi import HTTPException, Response
from fastapi.security import OAuth2PasswordBearer
from redis import Redis

date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}'
logger = logging.getLogger('discord_app')

# date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}'
# log_level = logger.info if os.environ.get('LOG_LEVEL') == 'INFO' else 'WARN'
# logging.basicConfig(
#     filename=f'logs/database/{date}.log',
#     format='%(asctime)s - sba-database - %(levelname)s - %(message)s',
#     level=log_level
# )

# Redis configuration
REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.environ.get('REDIS_PORT', '6379'))
REDIS_DB = int(os.environ.get('REDIS_DB', '0'))
CACHE_ENABLED = os.environ.get('CACHE_ENABLED', 'true').lower() == 'true'

# Initialize Redis client with connection error handling
if not CACHE_ENABLED:
    logger.info("Caching disabled via CACHE_ENABLED=false")
    redis_client = None
else:
    try:
        redis_client = Redis(
            host=REDIS_HOST,
            port=REDIS_PORT,
            db=REDIS_DB,
            decode_responses=True,
            socket_connect_timeout=5,
            socket_timeout=5
        )
        # Test connection
        redis_client.ping()
        logger.info(f"Redis connected successfully at {REDIS_HOST}:{REDIS_PORT}")
    except Exception as e:
        logger.warning(f"Redis connection failed: {e}. Caching will be disabled.")
        redis_client = None

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
priv_help = False if not os.environ.get('PRIVATE_IN_SCHEMA') else os.environ.get('PRIVATE_IN_SCHEMA').upper()
PRIVATE_IN_SCHEMA = True if priv_help == 'TRUE' else False


def valid_token(token):
    return token == os.environ.get('API_TOKEN')


def update_season_batting_stats(player_ids, season, db_connection):
    """
    Update season batting stats for specific players in a given season.
    Recalculates stats from stratplay data and upserts into seasonbattingstats table.
    """
    
    if not player_ids:
        logger.warning("update_season_batting_stats called with empty player_ids list")
        return
    
    # Convert single player_id to list for consistency
    if isinstance(player_ids, int):
        player_ids = [player_ids]
    
    logger.info(f"Updating season batting stats for {len(player_ids)} players in season {season}")
    
    try:
        # SQL query to recalculate and upsert batting stats
        query = """
        WITH batting_stats AS (
            SELECT
                p.id AS player_id,
                p.name,
                p.sbaplayer_id,
                p.team_id AS player_team_id,
                t.abbrev AS player_team_abbrev,
                sg.season,
                
                -- Counting statistics (summed from StratPlays)
                SUM(sp.pa) AS pa,
                SUM(sp.ab) AS ab,
                SUM(sp.run) AS run,
                SUM(sp.hit) AS hit,
                SUM(sp.double) AS double,
                SUM(sp.triple) AS triple,
                SUM(sp.homerun) AS homerun,
                SUM(sp.rbi) AS rbi,
                SUM(sp.bb) AS bb,
                SUM(sp.so) AS so,
                SUM(sp.bphr) AS bphr,
                SUM(sp.bpfo) AS bpfo,
                SUM(sp.bp1b) AS bp1b,
                SUM(sp.bplo) AS bplo,
                SUM(sp.gidp) AS gidp,
                SUM(sp.hbp) AS hbp,
                SUM(sp.sac) AS sac,
                SUM(sp.ibb) AS ibb,
                
                -- Calculated statistics using formulas
                CASE
                    WHEN SUM(sp.ab) > 0
                    THEN ROUND(SUM(sp.hit)::DECIMAL / SUM(sp.ab), 3)
                    ELSE 0.000
                END AS avg,
                
                CASE
                    WHEN SUM(sp.pa) > 0
                    THEN ROUND((SUM(sp.hit) + SUM(sp.bb) + SUM(sp.hbp) + SUM(sp.ibb))::DECIMAL / SUM(sp.pa), 3)
                    ELSE 0.000
                END AS obp,
                
                CASE
                    WHEN SUM(sp.ab) > 0
                    THEN ROUND((SUM(sp.hit) + SUM(sp.double) + 2 * SUM(sp.triple) + 3 * 
                    SUM(sp.homerun))::DECIMAL / SUM(sp.ab), 3)
                    ELSE 0.000
                END AS slg,
                
                CASE
                    WHEN SUM(sp.pa) > 0 AND SUM(sp.ab) > 0
                    THEN ROUND(
                        ((SUM(sp.hit) + SUM(sp.bb) + SUM(sp.hbp) + SUM(sp.ibb))::DECIMAL / SUM(sp.pa)) +
                        ((SUM(sp.hit) + SUM(sp.double) + 2 * SUM(sp.triple) + 3 * 
                        SUM(sp.homerun))::DECIMAL / SUM(sp.ab)), 3)
                    ELSE 0.000
                END AS ops,
                
                -- wOBA calculation (simplified version)
                CASE
                    WHEN SUM(sp.pa) > 0
                    THEN ROUND((0.690 * SUM(sp.bb) + 0.722 * SUM(sp.hbp) + 0.888 * (SUM(sp.hit) - 
                    SUM(sp.double) - SUM(sp.triple) - SUM(sp.homerun)) + 
                    1.271 * SUM(sp.double) + 1.616 * SUM(sp.triple) + 2.101 * 
                    SUM(sp.homerun))::DECIMAL / SUM(sp.pa), 3)
                    ELSE 0.000
                END AS woba,
                
                CASE
                    WHEN SUM(sp.pa) > 0
                    THEN ROUND(SUM(sp.so)::DECIMAL / SUM(sp.pa) * 100, 1)
                    ELSE 0.0
                END AS k_pct
                
            FROM stratplay sp
            JOIN stratgame sg ON sg.id = sp.game_id
            JOIN player p ON p.id = sp.batter_id
            JOIN team t ON t.id = p.team_id
            WHERE sg.season = %s AND p.id = ANY(%s)
            GROUP BY p.id, p.name, p.sbaplayer_id, p.team_id, t.abbrev, sg.season
        ),
        running_stats AS (
            SELECT
                sp.runner_id AS player_id,
                sg.season,
                SUM(sp.sb) AS sb,
                SUM(sp.cs) AS cs
            FROM stratplay sp
            JOIN stratgame sg ON sg.id = sp.game_id
            WHERE sg.season = %s AND sp.runner_id IS NOT NULL AND sp.runner_id = ANY(%s)
            GROUP BY sp.runner_id, sg.season
        )
        INSERT INTO seasonbattingstats (
            player_id, sbaplayer_id, team_id, season, name, player_team_id, player_team_abbrev,
            pa, ab, run, hit, double, triple, homerun, rbi, bb, so, bphr, bpfo, bp1b, bplo, gidp, hbp, sac, ibb,
            avg, obp, slg, ops, woba, k_pct, sb, cs
        )
        SELECT
            bs.player_id, bs.sbaplayer_id, bs.player_team_id, bs.season, bs.name, bs.player_team_id, bs.player_team_abbrev,
            bs.pa, bs.ab, bs.run, bs.hit, bs.double, bs.triple, bs.homerun, bs.rbi, bs.bb, bs.so, 
            bs.bphr, bs.bpfo, bs.bp1b, bs.bplo, bs.gidp, bs.hbp, bs.sac, bs.ibb,
            bs.avg, bs.obp, bs.slg, bs.ops, bs.woba, bs.k_pct,
            COALESCE(rs.sb, 0) AS sb,
            COALESCE(rs.cs, 0) AS cs
        FROM batting_stats bs
        LEFT JOIN running_stats rs ON bs.player_id = rs.player_id AND bs.season = rs.season
        ON CONFLICT (player_id, season)
        DO UPDATE SET
            sbaplayer_id = EXCLUDED.sbaplayer_id,
            team_id = EXCLUDED.team_id,
            name = EXCLUDED.name,
            player_team_id = EXCLUDED.player_team_id,
            player_team_abbrev = EXCLUDED.player_team_abbrev,
            pa = EXCLUDED.pa,
            ab = EXCLUDED.ab,
            run = EXCLUDED.run,
            hit = EXCLUDED.hit,
            double = EXCLUDED.double,
            triple = EXCLUDED.triple,
            homerun = EXCLUDED.homerun,
            rbi = EXCLUDED.rbi,
            bb = EXCLUDED.bb,
            so = EXCLUDED.so,
            bphr = EXCLUDED.bphr,
            bpfo = EXCLUDED.bpfo,
            bp1b = EXCLUDED.bp1b,
            bplo = EXCLUDED.bplo,
            gidp = EXCLUDED.gidp,
            hbp = EXCLUDED.hbp,
            sac = EXCLUDED.sac,
            ibb = EXCLUDED.ibb,
            avg = EXCLUDED.avg,
            obp = EXCLUDED.obp,
            slg = EXCLUDED.slg,
            ops = EXCLUDED.ops,
            woba = EXCLUDED.woba,
            k_pct = EXCLUDED.k_pct,
            sb = EXCLUDED.sb,
            cs = EXCLUDED.cs;
        """
        
        # Execute the query with parameters using the passed database connection
        db_connection.execute_sql(query, [season, player_ids, season, player_ids])
        
        logger.info(f"Successfully updated season batting stats for {len(player_ids)} players in season {season}")
        
    except Exception as e:
        logger.error(f"Error updating season batting stats: {e}")
        raise


def update_season_pitching_stats(player_ids, season, db_connection):
    """
    Update season pitching stats for specific players in a given season.
    Recalculates stats from stratplay and decision data and upserts into seasonpitchingstats table.
    """
    
    if not player_ids:
        logger.warning("update_season_pitching_stats called with empty player_ids list")
        return
    
    # Convert single player_id to list for consistency
    if isinstance(player_ids, int):
        player_ids = [player_ids]
    
    logger.info(f"Updating season pitching stats for {len(player_ids)} players in season {season}")
    
    try:
        # SQL query to recalculate and upsert pitching stats
        query = """
        WITH pitching_stats AS (
            SELECT
                p.id AS player_id,
                p.name,
                p.sbaplayer_id,
                p.team_id AS player_team_id,
                t.abbrev AS player_team_abbrev,
                sg.season,
                
                -- Counting statistics (summed from StratPlays)
                SUM(sp.pa) AS tbf,
                SUM(sp.ab) AS ab,
                SUM(sp.run) AS run,
                SUM(sp.e_run) AS e_run,
                SUM(sp.hit) AS hits,
                SUM(sp.double) AS double,
                SUM(sp.triple) AS triple,
                SUM(sp.homerun) AS homerun,
                SUM(sp.bb) AS bb,
                SUM(sp.so) AS so,
                SUM(sp.hbp) AS hbp,
                SUM(sp.sac) AS sac,
                SUM(sp.ibb) AS ibb,
                SUM(sp.gidp) AS gidp,
                SUM(sp.sb) AS sb,
                SUM(sp.cs) AS cs,
                SUM(sp.bphr) AS bphr,
                SUM(sp.bpfo) AS bpfo,
                SUM(sp.bp1b) AS bp1b,
                SUM(sp.bplo) AS bplo,
                SUM(sp.wild_pitch) AS wp,
                SUM(sp.balk) AS balk,
                SUM(sp.outs) AS outs,
                COALESCE(SUM(sp.wpa), 0) AS wpa,
                COALESCE(SUM(sp.re24_primary), 0) AS re24,
                
                -- Calculated statistics using formulas
                CASE
                    WHEN SUM(sp.outs) > 0
                    THEN ROUND((SUM(sp.e_run) * 27)::DECIMAL / SUM(sp.outs), 2)
                    ELSE 0.00
                END AS era,
                
                CASE
                    WHEN SUM(sp.outs) > 0
                    THEN ROUND(((SUM(sp.bb) + SUM(sp.hit) + SUM(sp.ibb)) * 3)::DECIMAL / SUM(sp.outs), 2)
                    ELSE 0.00
                END AS whip,
                
                CASE
                    WHEN SUM(sp.ab) > 0
                    THEN ROUND(SUM(sp.hit)::DECIMAL / SUM(sp.ab), 3)
                    ELSE 0.000
                END AS avg,
                
                CASE
                    WHEN SUM(sp.pa) > 0
                    THEN ROUND((SUM(sp.hit) + SUM(sp.bb) + SUM(sp.hbp) + SUM(sp.ibb))::DECIMAL / SUM(sp.pa), 3)
                    ELSE 0.000
                END AS obp,
                
                CASE
                    WHEN SUM(sp.ab) > 0
                    THEN ROUND((SUM(sp.hit) + SUM(sp.double) + 2 * SUM(sp.triple) + 3 * 
                    SUM(sp.homerun))::DECIMAL / SUM(sp.ab), 3)
                    ELSE 0.000
                END AS slg,
                
                CASE
                    WHEN SUM(sp.pa) > 0 AND SUM(sp.ab) > 0
                    THEN ROUND(
                        ((SUM(sp.hit) + SUM(sp.bb) + SUM(sp.hbp) + SUM(sp.ibb))::DECIMAL / SUM(sp.pa)) +
                        ((SUM(sp.hit) + SUM(sp.double) + 2 * SUM(sp.triple) + 3 * 
                        SUM(sp.homerun))::DECIMAL / SUM(sp.ab)), 3)
                    ELSE 0.000
                END AS ops,
                
                -- wOBA calculation (same as batting)
                CASE
                    WHEN SUM(sp.pa) > 0
                    THEN ROUND((0.690 * SUM(sp.bb) + 0.722 * SUM(sp.hbp) + 0.888 * (SUM(sp.hit) - 
                    SUM(sp.double) - SUM(sp.triple) - SUM(sp.homerun)) + 
                    1.271 * SUM(sp.double) + 1.616 * SUM(sp.triple) + 2.101 * 
                    SUM(sp.homerun))::DECIMAL / SUM(sp.pa), 3)
                    ELSE 0.000
                END AS woba,
                
                -- Rate stats
                CASE
                    WHEN SUM(sp.outs) > 0
                    THEN ROUND((SUM(sp.hit) * 9)::DECIMAL / (SUM(sp.outs) / 3.0), 1)
                    ELSE 0.0
                END AS hper9,
                
                CASE
                    WHEN SUM(sp.outs) > 0
                    THEN ROUND((SUM(sp.so) * 9)::DECIMAL / (SUM(sp.outs) / 3.0), 1)
                    ELSE 0.0
                END AS kper9,
                
                CASE
                    WHEN SUM(sp.outs) > 0
                    THEN ROUND((SUM(sp.bb) * 9)::DECIMAL / (SUM(sp.outs) / 3.0), 1)
                    ELSE 0.0
                END AS bbper9,
                
                CASE
                    WHEN SUM(sp.bb) > 0
                    THEN ROUND(SUM(sp.so)::DECIMAL / SUM(sp.bb), 2)
                    ELSE 0.0
                END AS kperbb
                
            FROM stratplay sp
            JOIN stratgame sg ON sg.id = sp.game_id
            JOIN player p ON p.id = sp.pitcher_id
            JOIN team t ON t.id = p.team_id
            WHERE sg.season = %s AND p.id = ANY(%s) AND sp.pitcher_id IS NOT NULL
            GROUP BY p.id, p.name, p.sbaplayer_id, p.team_id, t.abbrev, sg.season
        ),
        decision_stats AS (
            SELECT
                d.pitcher_id AS player_id,
                sg.season,
                SUM(d.win) AS win,
                SUM(d.loss) AS loss,
                SUM(d.hold) AS hold,
                SUM(d.is_save) AS saves,
                SUM(d.b_save) AS bsave,
                SUM(d.irunners) AS ir,
                SUM(d.irunners_scored) AS irs,
                SUM(d.is_start::INTEGER) AS gs,
                COUNT(d.game_id) AS games
            FROM decision d
            JOIN stratgame sg ON sg.id = d.game_id
            WHERE sg.season = %s AND d.pitcher_id = ANY(%s)
            GROUP BY d.pitcher_id, sg.season
        )
        INSERT INTO seasonpitchingstats (
            player_id, sbaplayer_id, team_id, season, name, player_team_id, player_team_abbrev,
            tbf, outs, games, gs, win, loss, hold, saves, bsave, ir, irs,
            ab, run, e_run, hits, double, triple, homerun, bb, so, hbp, sac, ibb, gidp, sb, cs,
            bphr, bpfo, bp1b, bplo, wp, balk,
            wpa, era, whip, avg, obp, slg, ops, woba, hper9, kper9, bbper9, kperbb, 
            lob_2outs, rbipercent, re24
        )
        SELECT
            ps.player_id, ps.sbaplayer_id, ps.player_team_id, ps.season, ps.name, ps.player_team_id, ps.player_team_abbrev,
            ps.tbf, ps.outs, COALESCE(ds.games, 0), COALESCE(ds.gs, 0), 
            COALESCE(ds.win, 0), COALESCE(ds.loss, 0), COALESCE(ds.hold, 0), 
            COALESCE(ds.saves, 0), COALESCE(ds.bsave, 0), COALESCE(ds.ir, 0), COALESCE(ds.irs, 0),
            ps.ab, ps.run, ps.e_run, ps.hits, ps.double, ps.triple, ps.homerun, ps.bb, ps.so,
            ps.hbp, ps.sac, ps.ibb, ps.gidp, ps.sb, ps.cs,
            ps.bphr, ps.bpfo, ps.bp1b, ps.bplo, ps.wp, ps.balk,
            ps.wpa * -1, ps.era, ps.whip, ps.avg, ps.obp, ps.slg, ps.ops, ps.woba, 
            ps.hper9, ps.kper9, ps.bbper9, ps.kperbb,
            0.0, 0.0, COALESCE(ps.re24 * -1, 0.0)
        FROM pitching_stats ps
        LEFT JOIN decision_stats ds ON ps.player_id = ds.player_id AND ps.season = ds.season
        ON CONFLICT (player_id, season)
        DO UPDATE SET
            sbaplayer_id = EXCLUDED.sbaplayer_id,
            team_id = EXCLUDED.team_id,
            name = EXCLUDED.name,
            player_team_id = EXCLUDED.player_team_id,
            player_team_abbrev = EXCLUDED.player_team_abbrev,
            tbf = EXCLUDED.tbf,
            outs = EXCLUDED.outs,
            games = EXCLUDED.games,
            gs = EXCLUDED.gs,
            win = EXCLUDED.win,
            loss = EXCLUDED.loss,
            hold = EXCLUDED.hold,
            saves = EXCLUDED.saves,
            bsave = EXCLUDED.bsave,
            ir = EXCLUDED.ir,
            irs = EXCLUDED.irs,
            ab = EXCLUDED.ab,
            run = EXCLUDED.run,
            e_run = EXCLUDED.e_run,
            hits = EXCLUDED.hits,
            double = EXCLUDED.double,
            triple = EXCLUDED.triple,
            homerun = EXCLUDED.homerun,
            bb = EXCLUDED.bb,
            so = EXCLUDED.so,
            hbp = EXCLUDED.hbp,
            sac = EXCLUDED.sac,
            ibb = EXCLUDED.ibb,
            gidp = EXCLUDED.gidp,
            sb = EXCLUDED.sb,
            cs = EXCLUDED.cs,
            bphr = EXCLUDED.bphr,
            bpfo = EXCLUDED.bpfo,
            bp1b = EXCLUDED.bp1b,
            bplo = EXCLUDED.bplo,
            wp = EXCLUDED.wp,
            balk = EXCLUDED.balk,
            wpa = EXCLUDED.wpa,
            era = EXCLUDED.era,
            whip = EXCLUDED.whip,
            avg = EXCLUDED.avg,
            obp = EXCLUDED.obp,
            slg = EXCLUDED.slg,
            ops = EXCLUDED.ops,
            woba = EXCLUDED.woba,
            hper9 = EXCLUDED.hper9,
            kper9 = EXCLUDED.kper9,
            bbper9 = EXCLUDED.bbper9,
            kperbb = EXCLUDED.kperbb,
            lob_2outs = EXCLUDED.lob_2outs,
            rbipercent = EXCLUDED.rbipercent,
            re24 = EXCLUDED.re24;
        """
        
        # Execute the query with parameters using the passed database connection
        db_connection.execute_sql(query, [season, player_ids, season, player_ids])
        
        logger.info(f"Successfully updated season pitching stats for {len(player_ids)} players in season {season}")
        
    except Exception as e:
        logger.error(f"Error updating season pitching stats: {e}")
        raise


def send_webhook_message(message: str) -> bool:
    """
    Send a message to Discord via webhook.
    
    Args:
        message: The message content to send
        
    Returns:
        bool: True if successful, False otherwise
    """
    webhook_url = "https://discord.com/api/webhooks/1408811717424840876/7RXG_D5IqovA3Jwa9YOobUjVcVMuLc6cQyezABcWuXaHo5Fvz1en10M7J43o3OJ3bzGW"
    
    try:
        payload = {
            "content": message
        }
        
        response = requests.post(webhook_url, json=payload, timeout=10)
        response.raise_for_status()
        
        logger.info(f"Webhook message sent successfully: {message[:100]}...")
        return True
        
    except requests.exceptions.RequestException as e:
        logger.error(f"Failed to send webhook message: {e}")
        return False
    except Exception as e:
        logger.error(f"Unexpected error sending webhook message: {e}")
        return False


def cache_result(ttl: int = 300, key_prefix: str = "api", normalize_params: bool = True):
    """
    Decorator to cache function results in Redis with parameter normalization.
    
    Args:
        ttl: Time to live in seconds (default: 5 minutes)
        key_prefix: Prefix for cache keys (default: "api")
        normalize_params: Remove None/empty values to reduce cache variations (default: True)
    
    Usage:
        @cache_result(ttl=600, key_prefix="stats")
        async def get_player_stats(player_id: int, season: Optional[int] = None):
            # expensive operation
            return stats
            
        # These will use the same cache entry when normalize_params=True:
        # get_player_stats(123, None) and get_player_stats(123)
    """
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # Skip caching if Redis is not available
            if redis_client is None:
                return await func(*args, **kwargs)
                
            try:
                # Normalize parameters to reduce cache variations
                normalized_kwargs = kwargs.copy()
                if normalize_params:
                    # Remove None values and empty collections
                    normalized_kwargs = {
                        k: v for k, v in kwargs.items() 
                        if v is not None and v != [] and v != "" and v != {}
                    }
                
                # Generate more readable cache key
                args_str = "_".join(str(arg) for arg in args if arg is not None)
                kwargs_str = "_".join([
                    f"{k}={v}" for k, v in sorted(normalized_kwargs.items())
                ])
                
                # Combine args and kwargs for cache key
                key_parts = [key_prefix, func.__name__]
                if args_str:
                    key_parts.append(args_str)
                if kwargs_str:
                    key_parts.append(kwargs_str)
                
                cache_key = ":".join(key_parts)
                
                # Truncate very long cache keys to prevent Redis key size limits
                if len(cache_key) > 200:
                    cache_key = f"{key_prefix}:{func.__name__}:{hash(cache_key)}"
                
                # Try to get from cache
                cached_result = redis_client.get(cache_key)
                if cached_result is not None:
                    logger.debug(f"Cache hit for key: {cache_key}")
                    return json.loads(cached_result)
                
                # Cache miss - execute function
                logger.debug(f"Cache miss for key: {cache_key}")
                result = await func(*args, **kwargs)
                
                # Skip caching for Response objects (like CSV downloads) as they can't be properly serialized
                if not isinstance(result, Response):
                    # Store in cache with TTL
                    redis_client.setex(
                        cache_key, 
                        ttl, 
                        json.dumps(result, default=str, ensure_ascii=False)
                    )
                else:
                    logger.debug(f"Skipping cache for Response object from {func.__name__}")
                
                return result
                
            except Exception as e:
                # If caching fails, log error and continue without caching
                logger.error(f"Cache error for {func.__name__}: {e}")
                return await func(*args, **kwargs)
                
        return wrapper
    return decorator


def invalidate_cache(pattern: str = "*"):
    """
    Invalidate cache entries matching a pattern.
    
    Args:
        pattern: Redis pattern to match keys (default: "*" for all)
        
    Usage:
        invalidate_cache("stats:*")  # Clear all stats cache
        invalidate_cache("api:get_player_*")  # Clear specific player cache
    """
    if redis_client is None:
        logger.warning("Cannot invalidate cache: Redis not available")
        return 0
        
    try:
        keys = redis_client.keys(pattern)
        if keys:
            deleted = redis_client.delete(*keys)
            logger.info(f"Invalidated {deleted} cache entries matching pattern: {pattern}")
            return deleted
        else:
            logger.debug(f"No cache entries found matching pattern: {pattern}")
            return 0
    except Exception as e:
        logger.error(f"Error invalidating cache: {e}")
        return 0


def get_cache_stats() -> dict:
    """
    Get Redis cache statistics.
    
    Returns:
        dict: Cache statistics including memory usage, key count, etc.
    """
    if redis_client is None:
        return {"status": "unavailable", "message": "Redis not connected"}
    
    try:
        info = redis_client.info()
        return {
            "status": "connected",
            "memory_used": info.get("used_memory_human", "unknown"),
            "total_keys": redis_client.dbsize(),
            "connected_clients": info.get("connected_clients", 0),
            "uptime_seconds": info.get("uptime_in_seconds", 0)
        }
    except Exception as e:
        logger.error(f"Error getting cache stats: {e}")
        return {"status": "error", "message": str(e)}


def add_cache_headers(
    max_age: int = 300, 
    cache_type: str = "public",
    vary: Optional[str] = None,
    etag: bool = False
):
    """
    Decorator to add HTTP cache headers to FastAPI responses.
    
    Args:
        max_age: Cache duration in seconds (default: 5 minutes)
        cache_type: "public", "private", or "no-cache" (default: "public")
        vary: Vary header value (e.g., "Accept-Encoding, Authorization")
        etag: Whether to generate ETag based on response content
        
    Usage:
        @add_cache_headers(max_age=1800, cache_type="public")
        async def get_static_data():
            return {"data": "static content"}
            
        @add_cache_headers(max_age=60, cache_type="private", vary="Authorization")
        async def get_user_data():
            return {"data": "user specific"}
    """
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            result = await func(*args, **kwargs)
            
            # Handle different response types
            if isinstance(result, Response):
                response = result
            elif isinstance(result, dict) or isinstance(result, list):
                # Convert to Response with JSON content
                response = Response(
                    content=json.dumps(result, default=str, ensure_ascii=False),
                    media_type="application/json"
                )
            else:
                # Handle other response types
                response = Response(content=str(result))
            
            # Build Cache-Control header
            cache_control_parts = [cache_type]
            if cache_type != "no-cache" and max_age > 0:
                cache_control_parts.append(f"max-age={max_age}")
            
            response.headers["Cache-Control"] = ", ".join(cache_control_parts)
            
            # Add Vary header if specified
            if vary:
                response.headers["Vary"] = vary
                
            # Add ETag if requested
            if etag and (hasattr(result, '__dict__') or isinstance(result, (dict, list))):
                content_hash = hashlib.md5(
                    json.dumps(result, default=str, sort_keys=True).encode()
                ).hexdigest()
                response.headers["ETag"] = f'"{content_hash}"'
            
            # Add Last-Modified header with current time for dynamic content
            response.headers["Last-Modified"] = datetime.datetime.now(datetime.timezone.utc).strftime(
                "%a, %d %b %Y %H:%M:%S GMT"
            )
            
            return response
            
        return wrapper
    return decorator


def handle_db_errors(func):
    """
    Decorator to handle database connection errors and transaction rollbacks.
    Ensures proper cleanup of database connections and provides consistent error handling.
    Includes comprehensive logging with function context, timing, and stack traces.
    """
    @wraps(func)
    async def wrapper(*args, **kwargs):
        import time
        import traceback
        from .db_engine import db  # Import here to avoid circular imports
        
        start_time = time.time()
        func_name = f"{func.__module__}.{func.__name__}"
        
        # Sanitize arguments for logging (exclude sensitive data)
        safe_args = []
        safe_kwargs = {}
        
        try:
            # Log sanitized arguments (avoid logging tokens, passwords, etc.)
            for arg in args:
                if hasattr(arg, '__dict__') and hasattr(arg, 'url'):  # FastAPI Request object
                    safe_args.append(f"Request({getattr(arg, 'method', 'UNKNOWN')} {getattr(arg, 'url', 'unknown')})")
                else:
                    safe_args.append(str(arg)[:100])  # Truncate long values
            
            for key, value in kwargs.items():
                if key.lower() in ['token', 'password', 'secret', 'key']:
                    safe_kwargs[key] = '[REDACTED]'
                else:
                    safe_kwargs[key] = str(value)[:100]  # Truncate long values
            
            logger.info(f"Starting {func_name} - args: {safe_args}, kwargs: {safe_kwargs}")
            
            result = await func(*args, **kwargs)
            
            elapsed_time = time.time() - start_time
            logger.info(f"Completed {func_name} successfully in {elapsed_time:.3f}s")
            
            return result
            
        except Exception as e:
            elapsed_time = time.time() - start_time
            error_trace = traceback.format_exc()
            
            logger.error(f"Database error in {func_name} after {elapsed_time:.3f}s")
            logger.error(f"Function args: {safe_args}")
            logger.error(f"Function kwargs: {safe_kwargs}")
            logger.error(f"Exception: {str(e)}")
            logger.error(f"Full traceback:\n{error_trace}")
            
            try:
                logger.info(f"Attempting database rollback for {func_name}")
                db.rollback()
                logger.info(f"Database rollback successful for {func_name}")
            except Exception as rollback_error:
                logger.error(f"Rollback failed in {func_name}: {rollback_error}")
            finally:
                try:
                    db.close()
                    logger.info(f"Database connection closed for {func_name}")
                except Exception as close_error:
                    logger.error(f"Error closing database connection in {func_name}: {close_error}")
            
            raise HTTPException(status_code=500, detail=f'Database error in {func_name}: {str(e)}')
    
    return wrapper
