"""
Games thread component for managing game-related operations
"""

import time
import json
import logging
import threading
from datetime import datetime, timedelta, date
from typing import Optional, Dict, Any, List

from .thread_manager import ThreadedComponent
from .message_bus import MessageBus, Message, MessageType, MessageBuilder
from ..database.manager import DatabaseManager
from ..database.models import MatchModel, MatchStatus, BetDetailModel, MatchOutcomeModel, GameConfigModel, ExtractionAssociationModel, PersistentRedistributionAdjustmentModel, MatchTemplateModel, MatchOutcomeTemplateModel
from ..utils.timezone_utils import get_today_venue_date

logger = logging.getLogger(__name__)


import random

def extract_balanced_safe(results, odds, current_balance, stake, payouts, house_edge=0.10, sensitivity=0.01, max_drift=0.5):
    """
    results: List of 7 items.
    odds: List of 7 decimal odds.
    current_balance: Negative = Over-distributed ($), Positive = Under-distributed ($).
    stake: The USD amount put into this specific draw.
    house_edge: 0.10 = 10% margin for the house.
    sensitivity: Speed of balance recovery.
    max_drift: Max % a probability can deviate from its 'edged' base.
    """
    import logging
    ext_logger = logging.getLogger(__name__)
    
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] INPUTS: results={results}, odds={odds}")
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] current_balance={current_balance:.2f}, stake={stake:.2f}, payouts={payouts},  house_edge={house_edge:.4f}, sensitivity={sensitivity:.4f}, max_drift={max_drift:.4f}")
    
    # 1. Base Probabilities (1/odds)
    raw_probs = [1.0 / o for o in odds]
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 1 - Raw probabilities (1/odds): {raw_probs}")
    
    # 2. Apply House Edge (Normalize to 1.0 + edge)
    # This ensures that even with a 0 balance, the house wins on average.
    total_raw = sum(raw_probs)
    target_total = 1.0 + house_edge
    edged_probs = [(p / total_raw) * target_total for p in raw_probs]
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 2 - total_raw={total_raw:.4f}, target_total={target_total:.4f}")
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 2 - Edged probabilities: {edged_probs}")
    
    # 3. Adjust for USD Balance & Safety Caps
    final_weights = []
    expected_redistribution = stake*(1-house_edge)
    for i, p in enumerate(edged_probs):
        poutname = list(payouts.keys())[i]
        payout = payouts.get(poutname, 0)
        if type(payout).__name__ == 'list' and len(payout) > 1:
           payout = payout[1]
        #net_change = stake - payout
        net_change = expected_redistribution - payout
        ext_logger.info(f"🔍 [EXTRACT_BALANCED] edge index {i}: outcome {poutname} -> payout {payout} net_change={net_change}")
        
        # Calculate correction factor
        # Negative balance + Negative net_change = Weight reduction
        correction = 1.0 + (current_balance * net_change * sensitivity / 100)
        
        # Apply Safety Cap (max_drift)
        # Prevents the 'edged' probability from swinging too wildly
        lower_bound = p * (1 - max_drift)
        upper_bound = p * (1 + max_drift)
        
        corrected = p * correction
        final_w = max(min(corrected, upper_bound), lower_bound)
        final_weights.append(final_w)
        
        ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 3 - Item {i} ({results[i]}): payout_if_wins={payout:.2f}, net_change={net_change:.2f}, original={p:.6f}, max_drift={max_drift:.6f}, correction={correction:.6f}, correctet={corrected:.6f}, bounds=[{lower_bound:.6f}, {upper_bound:.6f}], final_weight={final_w:.6f}")

    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 3 - Final weights: {final_weights}")
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 3 - Sum of final weights: {sum(final_weights):.6f}")

    # 4. Selection
    selection = random.choices(results, weights=final_weights, k=1)
    selected_odds = odds[results.index(selection[0])]
    pout = payouts.get(selection[0], 0)
    if type(pout).__name__ == 'list' and len(pout) > 1:
       pout = pout[1]
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 4 - SELECTED: {selection[0]} with odds={selected_odds}")
    
    # 5. Result Impact
    expected_redistribution = stake * (1-house_edge)
    balance_impact = expected_redistribution - pout
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 5 - balance_impact = {balance_impact:.2f}")
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Payout: {pout:.2f}")
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] Expected redistribytion: {expected_redistribution:.2f}")
    ext_logger.info(f"🔍 [EXTRACT_BALANCED] OUTPUT: selected={selection[0]}, impact={balance_impact:.2f}")
    
    return selection[0], balance_impact

# --- Usage ---
#res = ["A", "B", "C", "D", "E", "F", "G"]
#odds = [1.5, 2.0, 3.0, 4.5, 6.0, 8.5, 9.5]

# If balance is 0, the 'house_edge' will naturally pull the balance 
# toward positive territory over time.
#current_bal = -50.0 
#bet = 10.0

#winner, impact = extract_balanced_pro(res, odds, current_bal, bet)

def extract_match(balance, uopayin, mpayin, match, cap, payouts, odds):
    """
    Extract match function called before the first extraction.

    Args:
        balance: Accumulated balance across extraction of over-redistribution or under-redistribution
        uopayin: Total payin from UNDER and OVER bets of the match we are extracting
        mpayin: Total payin from all other bets (excluding UNDER/OVER) of the match we are extracting
        match: The match number
        cap: Redistribution percentage
        payouts: Dictionary with calculated payouts for all outcomes
        odds: Dictionary with odds for all outcomes

    The payouts dictionary format:
    {
        "UNDER": calculated_payout,
        "OVER": calculated_payout,
        "RESULTS": {
            "KO2": ["WIN1, KO2", calculated_total_payout],
            "KO1": ["WIN1", calculated_total_payout],
            ...
        }
    }

    The odds dictionary format:
    {
        "UNDER": float_value,
        "OVER": float_value,
        "KO1": float_value,
        "WIN1": float_value,
        ...
    }

    """
    import logging
    logger = logging.getLogger(__name__)
    
    logger.info("=" * 80)
    logger.info("                EXTRACTION - START                        ")
    logger.info("=" * 80)
    logger.info(f"📊 [EXTRACT_MATCH] Match: {match}")
    logger.info(f"📊 [EXTRACT_MATCH] Balance (accumulated shortfall): {balance:.2f}")
    logger.info(f"📊 [EXTRACT_MATCH] UOPayin (UNDER+OVER bets): {uopayin:.2f}")
    logger.info(f"📊 [EXTRACT_MATCH] MPayin (other bets): {mpayin:.2f}")
    logger.info(f"📊 [EXTRACT_MATCH] Total Payin: {uopayin + mpayin:.2f}")
    logger.info(f"📊 [EXTRACT_MATCH] Cap (redistribution %): {cap:.2f}%")
    logger.info(f"📊 [EXTRACT_MATCH] House edge for extraction: {(100-cap)/100:.4f}")
    logger.info(f"📊 [EXTRACT_MATCH] Payouts: {payouts}")
    logger.info(f"📊 [EXTRACT_MATCH] Odds: {odds}")
    logger.info("=" * 80)

    rodds = list(set(odds.keys()) - set(["UNDER", "OVER"]))
    rodds_values = [odds[odd] for odd in rodds]
    logger.info(f"📊 [EXTRACT_MATCH] Result outcomes: {rodds}")
    logger.info(f"📊 [EXTRACT_MATCH] Result odds: {rodds_values}")

    # First extraction: UNDER/OVER
    logger.info("-" * 60)
    logger.info("📊 [EXTRACT_MATCH] EXTRACTION 1: UNDER/OVER")
    logger.info("-" * 60)
    uores, uoimpact  = extract_balanced_safe(['UNDER', 'OVER'], [odds['UNDER'],
    					odds['OVER']], balance, uopayin, payouts,
    					float((100-cap)/100))
    logger.info(f"📊 [EXTRACT_MATCH] UNDER/OVER result: {uores}, impact: {uoimpact:.2f}")

    newbalance = balance+uoimpact
    logger.info(f"📊 [EXTRACT_MATCH] New balance after UO extraction: {newbalance:.2f} (was {balance:.2f} + {uoimpact:.2f})")

    # Only perform second extraction if there are other outcomes besides UNDER/OVER
    if rodds and rodds_values:
        logger.info("-" * 60)
        logger.info("📊 [EXTRACT_MATCH] EXTRACTION 2: RESULT OUTCOMES")
        logger.info("-" * 60)
        mres, mimpact = extract_balanced_safe(rodds, rodds_values, newbalance, mpayin, payouts['RESULTS'], float((100-cap)/100))
        impact = uoimpact + mimpact
        logger.info(f"📊 [EXTRACT_MATCH] Result outcome: {mres}, impact: {mimpact:.2f}")
    else:
        # No other outcomes, just use the UNDER/OVER result and impact
        mres = None
        mimpact = 0
        impact = uoimpact
        logger.info("📊 [EXTRACT_MATCH] No result outcomes, skipping second extraction")

    mres_str = str(mres) if mres is not None else "N/A"
    
    # Calculate what the actual payout will be
    under_payout = payouts.get("UNDER", 0)
    over_payout = payouts.get("OVER", 0)
    result_payout = payouts["RESULTS"][mres_str][1] if mres_str != "N/A" and mres_str in payouts.get("RESULTS", {}) else 0
    total_actual_payout = (under_payout if uores == "UNDER" else over_payout) + result_payout
    
    logger.info("=" * 80)
    logger.info("📊 [EXTRACT_MATCH] EXTRACTION SUMMARY")
    logger.info("=" * 80)
    logger.info(f"📊 [EXTRACT_MATCH] UNDER/OVER winner: {uores}")
    logger.info(f"📊 [EXTRACT_MATCH] Result winner: {mres_str}")
    logger.info(f"📊 [EXTRACT_MATCH] UO impact: {uoimpact:.2f}, M impact: {mimpact:.2f}, Total impact: {impact:.2f}")
    logger.info(f"📊 [EXTRACT_MATCH] Total payin: {uopayin + mpayin:.2f}")
    logger.info(f"📊 [EXTRACT_MATCH] Total actual payout: {total_actual_payout:.2f}")
    logger.info(f"📊 [EXTRACT_MATCH] House profit/loss: {(uopayin + mpayin) - total_actual_payout:.2f}")
    logger.info("=" * 80)
    
    print(f"  ****  EXTRACTED: {uores}, {mres_str}, impact={impact:.2f}, payout={total_actual_payout:.2f}   **** ")

    return uores, mres_str, impact, payouts["RESULTS"][mres_str][0]

class GamesThread(ThreadedComponent):
    """Games thread for handling game operations and monitoring"""

    def __init__(self, name: str, message_bus: MessageBus, db_manager: DatabaseManager):
        super().__init__(name, message_bus)
        self.db_manager = db_manager
        self.current_fixture_id: Optional[str] = None
        self.game_active = False
        self._shutdown_event = threading.Event()
        self.message_queue = None
        self.waiting_for_validation_fixture: Optional[str] = None
        self.pending_today_fixture_id: Optional[str] = None
        self.yesterday_fixture_id: Optional[str] = None  # Track yesterday's fixture for completion
        self._last_orphan_cleanup_time: float = 0.0  # Track last orphan cleanup time
        self._orphan_cleanup_interval: int = 10  # Run orphan cleanup every 10 seconds
        self._cached_today_fixture_id: Optional[str] = None  # Cache for today's fixture to prevent duplicates
        self._today_fixture_cache_time: float = 0.0  # Timestamp when cache was set

    def _get_today_venue_date(self) -> datetime.date:
        """Get today's date in venue timezone (for day change detection)"""
        return get_today_venue_date(self.db_manager)

    def _check_and_handle_day_change(self) -> bool:
        """Check if day has changed and handle continuous flow.
        Returns True if day change was detected, False otherwise.
        Note: No database changes or game state reset - allows continuous flow."""
        try:
            session = self.db_manager.get_session()
            try:
                today = self._get_today_venue_date()

                # Get the current fixture if any
                if not self.current_fixture_id:
                    return False

                # Get matches for current fixture
                current_matches = session.query(MatchModel).filter(
                    MatchModel.fixture_id == self.current_fixture_id,
                    MatchModel.active_status == True
                ).all()

                if not current_matches:
                    return False

                # Check if all matches in current fixture are from a previous day
                all_matches_old_day = True
                for match in current_matches:
                    if match.start_time:
                        # Convert UTC start_time to venue timezone for date comparison
                        from ..utils.timezone_utils import utc_to_venue_datetime
                        venue_start_time = utc_to_venue_datetime(match.start_time, self.db_manager)
                        match_date = venue_start_time.date()
                        if match_date == today:
                            all_matches_old_day = False
                            break

                if all_matches_old_day:
                    logger.info(f"Day change detected! Current fixture {self.current_fixture_id} is from previous day - allowing continuous flow")
                    # Note: No database changes or game state reset - system will continue with existing matches
                    # and create new fixtures with new fixture_id when current fixture is exhausted
                    return True

                return False

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to check/handle day change: {e}")
            return False

    def _check_venue_midnight_passed(self) -> bool:
        """Check if venue timezone midnight has passed since the last fixture creation.
        Returns True if midnight has passed and new fixture should be created, False otherwise."""
        try:
            session = self.db_manager.get_session()
            try:
                # Get the most recent fixture creation time
                latest_fixture = session.query(MatchModel).filter(
                    MatchModel.active_status == True
                ).order_by(MatchModel.created_at.desc()).first()

                if not latest_fixture:
                    logger.debug("No fixtures found - midnight check not applicable")
                    return False

                # Get the creation time of the latest fixture
                latest_fixture_time = latest_fixture.created_at

                # Convert to venue timezone
                from ..utils.timezone_utils import utc_to_venue_datetime
                venue_latest_time = utc_to_venue_datetime(latest_fixture_time, self.db_manager)

                # Get current time in venue timezone
                from ..utils.timezone_utils import get_current_venue_datetime
                current_venue_time = get_current_venue_datetime(self.db_manager)

                # Check if we've passed midnight since the last fixture was created
                # Compare dates - if the current date is different from the fixture creation date
                if current_venue_time.date() > venue_latest_time.date():
                    logger.info(f"Venue midnight passed! Last fixture created on {venue_latest_time.date()}, current date is {current_venue_time.date()}")
                    return True
                else:
                    logger.debug(f"Venue midnight not passed. Last fixture: {venue_latest_time.date()}, Current: {current_venue_time.date()}")
                    return False

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to check venue midnight: {e}")
            return False

    def _is_fixture_from_yesterday(self, fixture_id: str, session) -> bool:
        """Check if the specified fixture is from yesterday.
        
        A fixture is considered 'from yesterday' if the FIRST match of the fixture
        (determined by match_number) has a start_time from yesterday.
        The fixture date is determined by the first match, regardless of whether
        that match is completed or not.
        """
        try:
            if not fixture_id:
                return False

            # Get today's date in venue timezone
            today = self._get_today_venue_date()

            # Get the FIRST match of the fixture (by match_number)
            # This determines the fixture date, regardless of completion status
            first_match = session.query(MatchModel).filter(
                MatchModel.fixture_id == fixture_id,
                MatchModel.active_status == True
            ).order_by(MatchModel.match_number.asc()).first()

            if not first_match:
                logger.debug(f"Fixture {fixture_id} has no matches")
                return False

            if not first_match.start_time:
                logger.debug(f"First match of fixture {fixture_id} has no start_time")
                return False

            # Convert UTC start_time to venue timezone for date comparison
            from ..utils.timezone_utils import utc_to_venue_datetime
            venue_start_time = utc_to_venue_datetime(first_match.start_time, self.db_manager)
            match_date = venue_start_time.date()

            # Check if the first match date is yesterday
            days_diff = (today - match_date).days
            is_yesterday = days_diff == 1
            
            logger.info(f"📅 Fixture {fixture_id} first match #{first_match.match_number} date: {match_date}, today: {today}, days_diff: {days_diff}, is_yesterday: {is_yesterday}")
            
            return is_yesterday

        except Exception as e:
            logger.error(f"Failed to check if fixture {fixture_id} is from yesterday: {e}")
            return False

    def _create_new_fixture_for_continuation(self) -> Optional[str]:
        """Create a new fixture for continuation with new fixture_id and match numbers restarting from 1"""
        try:
            session = self.db_manager.get_session()
            try:
                # Create new fixture from match templates with match numbers starting from 1
                logger.info("Creating new fixture for continuation from match templates")

                # Select random match templates (aim for 5 matches)
                template_matches = self._select_random_match_templates(5, session)
                if template_matches:
                    fixture_id = self._create_new_fixture_from_templates_for_continuation(template_matches, session)
                    if fixture_id:
                        logger.info(f"Created new fixture {fixture_id} for continuation with {len(template_matches)} matches from templates")
                        return fixture_id
                    else:
                        logger.warning("Failed to create new fixture from templates for continuation")
                        return None

                # If no templates available, try old completed matches
                logger.info("No match templates available for continuation, trying to create fixture from old completed matches")
                old_matches = self._select_random_completed_matches(5, session)
                if old_matches:
                    fixture_id = self._create_new_fixture_from_old_matches_for_continuation(old_matches, session)
                    if fixture_id:
                        logger.info(f"Created new fixture {fixture_id} for continuation with {len(old_matches)} matches from old matches")
                        return fixture_id
                    else:
                        logger.warning("Failed to create new fixture from old matches for continuation")
                        return None

                # No matches available at all
                logger.warning("No match templates or old completed matches found for continuation - cannot create new fixture")
                return None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to create new fixture for continuation: {e}")
            return None

    def _create_new_fixture_at_midnight(self) -> Optional[str]:
        """Create a new fixture at midnight with new fixture_id and match numbers restarting from 1"""
        try:
            session = self.db_manager.get_session()
            try:
                # Create new fixture from match templates with match numbers starting from 1
                logger.info("Creating new fixture at midnight from match templates")

                # Select random match templates (aim for 5 matches)
                template_matches = self._select_random_match_templates(5, session)
                if template_matches:
                    fixture_id = self._create_new_fixture_from_templates_at_midnight(template_matches, session)
                    if fixture_id:
                        logger.info(f"Created new fixture {fixture_id} at midnight with {len(template_matches)} matches from templates")
                        return fixture_id
                    else:
                        logger.warning("Failed to create new fixture from templates at midnight")
                        return None

                # If no templates available, try old completed matches
                logger.info("No match templates available at midnight, trying to create fixture from old completed matches")
                old_matches = self._select_random_completed_matches(5, session)
                if old_matches:
                    fixture_id = self._create_new_fixture_from_old_matches_at_midnight(old_matches, session)
                    if fixture_id:
                        logger.info(f"Created new fixture {fixture_id} at midnight with {len(old_matches)} matches from old matches")
                        return fixture_id
                    else:
                        logger.warning("Failed to create new fixture from old matches at midnight")
                        return None

                # No matches available at all
                logger.warning("No match templates or old completed matches found at midnight - cannot create new fixture")
                return None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to create new fixture at midnight: {e}")
            return None

    def _cleanup_stale_ingame_matches(self):
        """Clean up any stale 'ingame' matches from previous crashed sessions and old 'bet' fixtures"""
        try:
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone (for day change detection)
                today = self._get_today_venue_date()

                # PART 1: Clean up stale 'ingame' matches from today (existing logic)
                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)

                stale_matches = session.query(MatchModel).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= utc_start,
                    MatchModel.start_time < utc_end,
                    MatchModel.status == 'ingame',
                    MatchModel.active_status == True
                ).all()

                if stale_matches:
                    logger.info(f"Found {len(stale_matches)} stale ingame matches - cleaning up")
                    for match in stale_matches:
                        logger.info(f"Cleaning up stale match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}")
                        match.status = 'pending'
                        match.active_status = False
                    session.commit()
                    logger.info(f"Cleaned up {len(stale_matches)} stale ingame matches")
                else:
                    logger.info("No stale ingame matches found")

                # PART 2: Clean up ALL old 'bet' fixtures from previous days (older than yesterday)
                # Convert yesterday's date range to UTC for database query
                yesterday_start = utc_start - timedelta(days=1)
                yesterday_end = utc_end - timedelta(days=1)

                old_bet_matches = session.query(MatchModel).filter(
                    MatchModel.status == 'bet',
                    MatchModel.active_status == True,
                    # Exclude today's and yesterday's matches to allow cross-day flow
                    ~MatchModel.start_time.between(yesterday_start, utc_end)
                ).all()

                if old_bet_matches:
                    logger.info(f"Found {len(old_bet_matches)} old 'bet' matches from previous days - cancelling them")

                    for match in old_bet_matches:
                        logger.info(f"Cancelling old bet match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}")
                        match.status = 'cancelled'

                        # Cancel/refund associated bets
                        self._cancel_match_bets(match.id, session)

                    session.commit()
                    logger.info(f"Cancelled {len(old_bet_matches)} old bet matches from previous days")
                else:
                    logger.info("No old bet matches from previous days found to cancel")

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to cleanup stale matches: {e}")

    def _cleanup_old_incomplete_matches(self):
        """Close matches from 2+ days ago with 'failed' status.
        
        This method should be called during initialization to handle matches
        that were never completed from 2 or more days ago.
        
        It uses multiple strategies to identify old matches:
        1. Matches with start_time before yesterday (2+ days ago)
        2. Matches with NULL start_time but created_at before yesterday (fallback)
        """
        try:
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone
                today = self._get_today_venue_date()
                
                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                
                # Calculate the start of yesterday (1 day ago) in UTC
                yesterday_start = utc_start - timedelta(days=1)
                
                # Find all incomplete matches from 2+ days ago
                # These are matches that:
                # - Have start_time before yesterday_start (2+ days ago) OR
                # - Have NULL start_time but created_at before yesterday_start
                # - Are NOT in terminal states (done, cancelled, failed, paused)
                # - Have active_status = True
                incomplete_statuses = ['pending', 'scheduled', 'bet', 'ingame']
                
                # Strategy 1: Matches with start_time set and before yesterday
                old_matches_with_start_time = session.query(MatchModel).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time < yesterday_start,
                    MatchModel.status.in_(incomplete_statuses),
                    MatchModel.active_status == True
                ).all()
                
                # Strategy 2: Matches with NULL start_time but created_at before yesterday
                # This catches matches that were synced recently but belong to old fixtures
                old_matches_without_start_time = session.query(MatchModel).filter(
                    MatchModel.start_time.is_(None),
                    MatchModel.created_at < yesterday_start,
                    MatchModel.status.in_(incomplete_statuses),
                    MatchModel.active_status == True
                ).all()
                
                # Combine both sets
                old_incomplete_matches = old_matches_with_start_time + old_matches_without_start_time
                
                # Log detailed information for debugging
                logger.info(f"🔍 Cleanup check: yesterday_start (UTC) = {yesterday_start}")
                logger.info(f"🔍 Found {len(old_matches_with_start_time)} old matches with start_time < yesterday")
                logger.info(f"🔍 Found {len(old_matches_without_start_time)} old matches with NULL start_time but old created_at")
                
                if old_incomplete_matches:
                    logger.info(f"⚠️ Found {len(old_incomplete_matches)} incomplete matches from 2+ days ago - marking as failed")
                    
                    for match in old_incomplete_matches:
                        logger.info(f"Marking old incomplete match {match.match_number} (fixture: {match.fixture_id}, start_time: {match.start_time}, created_at: {match.created_at}) as failed: {match.fighter1_township} vs {match.fighter2_township}")
                        match.status = 'failed'
                        
                        # Cancel/refund associated bets
                        self._cancel_match_bets(match.id, session)
                    
                    session.commit()
                    logger.info(f"✅ Marked {len(old_incomplete_matches)} old incomplete matches as failed")
                else:
                    logger.info("No incomplete matches from 2+ days ago found")
                    
            finally:
                session.close()
                
        except Exception as e:
            logger.error(f"Failed to cleanup old incomplete matches: {e}")

    def _get_yesterday_incomplete_matches(self) -> List[MatchModel]:
        """Get all incomplete matches from yesterday.
        
        Returns a list of matches from yesterday that are not in terminal states.
        
        Uses multiple strategies to identify yesterday's matches:
        1. Matches with start_time in yesterday's date range
        2. Matches with NULL start_time but created_at in yesterday's date range (fallback)
        """
        try:
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone
                today = self._get_today_venue_date()
                
                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)
                
                # Calculate yesterday's date range in UTC
                yesterday_start = utc_start - timedelta(days=1)
                yesterday_end = utc_end - timedelta(days=1)
                
                # Find all incomplete matches from yesterday
                incomplete_statuses = ['pending', 'scheduled', 'bet', 'ingame']
                
                # Strategy 1: Matches with start_time in yesterday's range
                yesterday_matches_with_start_time = session.query(MatchModel).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= yesterday_start,
                    MatchModel.start_time < yesterday_end,
                    MatchModel.status.in_(incomplete_statuses),
                    MatchModel.active_status == True
                ).all()
                
                # Strategy 2: Matches with NULL start_time but created_at in yesterday's range
                yesterday_matches_without_start_time = session.query(MatchModel).filter(
                    MatchModel.start_time.is_(None),
                    MatchModel.created_at >= yesterday_start,
                    MatchModel.created_at < yesterday_end,
                    MatchModel.status.in_(incomplete_statuses),
                    MatchModel.active_status == True
                ).all()
                
                # Combine both sets
                yesterday_matches = yesterday_matches_with_start_time + yesterday_matches_without_start_time
                
                # Log detailed information for debugging
                logger.info(f"🔍 Yesterday check: yesterday_start (UTC) = {yesterday_start}, yesterday_end (UTC) = {yesterday_end}")
                logger.info(f"🔍 Found {len(yesterday_matches_with_start_time)} yesterday matches with start_time in range")
                logger.info(f"🔍 Found {len(yesterday_matches_without_start_time)} yesterday matches with NULL start_time but created_at in range")
                
                if yesterday_matches:
                    logger.info(f"Found {len(yesterday_matches)} incomplete matches from yesterday")
                else:
                    logger.info("No incomplete matches from yesterday found")
                
                return yesterday_matches
                
            finally:
                session.close()
                
        except Exception as e:
            logger.error(f"Failed to get yesterday's incomplete matches: {e}")
            return []

    def _finalize_matches_with_results(self) -> int:
        """Finalize matches that have results set but are still in pending status.
        
        This handles matches from yesterday or today that:
        - Are in 'pending' status
        - Have start_time set but no end_time
        - Have results and/or winning_outcomes already set
        
        NOTE: This method also handles matches with active_status = False, which can
        occur when a match was partially processed but the session crashed or had errors.
        
        For such matches:
        - Set end_time to now
        - Set status to 'done'
        - Set active_status to True (re-activate if it was False)
        - Resolve associated bets based on the results
        
        Returns count of matches finalized.
        """
        try:
            session = self.db_manager.get_session()
            try:
                # Find matches that have results but are still pending
                # Check for matches with result or winning_outcomes set
                # NOTE: We don't filter by active_status here because we want to catch
                # matches that may have been partially processed (active_status = False)
                pending_with_results = session.query(MatchModel).filter(
                    MatchModel.status == 'pending',
                    MatchModel.start_time.isnot(None),
                    MatchModel.end_time.is_(None),
                    (MatchModel.result.isnot(None) | MatchModel.winning_outcomes.isnot(None))
                ).all()
                
                if not pending_with_results:
                    logger.info("No pending matches with results found to finalize")
                    return 0
                
                logger.info(f"🔍 Found {len(pending_with_results)} pending matches with results - finalizing them")
                
                finalized_count = 0
                for match in pending_with_results:
                    logger.info(f"🔍 Finalizing match {match.match_number} (fixture: {match.fixture_id})")
                    logger.info(f"   - result: {match.result}, winning_outcomes: {match.winning_outcomes}")
                    logger.info(f"   - under_over_result: {match.under_over_result}")
                    
                    # Set end_time to now
                    match.end_time = datetime.utcnow()
                    
                    # Set status to done
                    match.status = 'done'
                    
                    # Re-activate if it was False (handles partially processed matches)
                    if match.active_status == False:
                        logger.info(f"🔄 Re-activating match {match.match_number} (active_status was False)")
                        match.active_status = True
                    
                    # Resolve associated bets
                    self._resolve_match_bets(match, session)
                    
                    finalized_count += 1
                    logger.info(f"✅ Match {match.match_number} finalized as 'done'")
                
                if finalized_count > 0:
                    session.commit()
                    logger.info(f"✅ Finalized {finalized_count} pending matches with results")
                
                return finalized_count
                
            finally:
                session.close()
                
        except Exception as e:
            logger.error(f"Failed to finalize matches with results: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")
            return 0

    def _process_today_pending_matches(self) -> List[MatchModel]:
        """Process pending matches in today's fixture.
        
        For each pending match:
        - If no result is set: Change status to 'bet' so it can be played first
        - If result is already set: Finalize as 'done' and resolve associated bets
        
        Returns list of matches that should be played (status changed to 'bet').
        """
        try:
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone
                today = self._get_today_venue_date()
                
                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)
                
                # Find pending matches in today's fixture
                # Strategy 1: Matches with start_time in today's range
                today_pending_with_start_time = session.query(MatchModel).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= utc_start,
                    MatchModel.start_time < utc_end,
                    MatchModel.status == 'pending',
                    MatchModel.active_status == True
                ).all()
                
                # Strategy 2: Matches with NULL start_time but created_at in today's range
                today_pending_without_start_time = session.query(MatchModel).filter(
                    MatchModel.start_time.is_(None),
                    MatchModel.created_at >= utc_start,
                    MatchModel.created_at < utc_end,
                    MatchModel.status == 'pending',
                    MatchModel.active_status == True
                ).all()
                
                # Combine both sets
                today_pending_matches = today_pending_with_start_time + today_pending_without_start_time
                
                logger.info(f"🔍 Today pending check: utc_start = {utc_start}, utc_end = {utc_end}")
                logger.info(f"🔍 Found {len(today_pending_with_start_time)} pending matches with start_time in today's range")
                logger.info(f"🔍 Found {len(today_pending_without_start_time)} pending matches with NULL start_time but created_at in today's range")
                
                matches_to_play = []
                
                for match in today_pending_matches:
                    logger.info(f"🔍 Processing pending match {match.match_number} (fixture: {match.fixture_id})")
                    logger.info(f"   - result: {match.result}, under_over_result: {match.under_over_result}")
                    
                    # Check if match has results already set
                    has_result = match.result is not None or match.under_over_result is not None
                    
                    if has_result:
                        # Match has results - finalize it as done
                        logger.info(f"✅ Match {match.match_number} has results - finalizing as 'done'")
                        match.status = 'done'
                        
                        # Resolve associated bets
                        self._resolve_match_bets(match, session)
                    else:
                        # Match has no results - change status to 'bet' to play first
                        logger.info(f"🎯 Match {match.match_number} has no results - changing status to 'bet'")
                        match.status = 'bet'
                        matches_to_play.append(match)
                
                if today_pending_matches:
                    session.commit()
                    logger.info(f"✅ Processed {len(today_pending_matches)} pending matches: {len(matches_to_play)} set to 'bet', {len(today_pending_matches) - len(matches_to_play)} finalized as 'done'")
                
                return matches_to_play
                
            finally:
                session.close()
                
        except Exception as e:
            logger.error(f"Failed to process today's pending matches: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")
            return []

    def _resolve_match_bets(self, match: MatchModel, session):
        """Resolve bets for a match that is being finalized.
        
        Args:
            match: The match being finalized
            session: Database session
        """
        try:
            # Get all pending bets for this match
            pending_bets = session.query(BetDetailModel).filter(
                BetDetailModel.match_id == match.id,
                BetDetailModel.result == 'pending'
            ).all()
            
            if not pending_bets:
                logger.debug(f"No pending bets to resolve for match {match.id}")
                return
            
            logger.info(f"🔍 Resolving {len(pending_bets)} pending bets for match {match.id}")
            
            # Determine winning outcomes
            winning_outcomes = []
            if match.winning_outcomes:
                try:
                    winning_outcomes = json.loads(match.winning_outcomes)
                except (json.JSONDecodeError, TypeError):
                    pass
            
            # Add under_over_result if set
            if match.under_over_result:
                winning_outcomes.append(match.under_over_result)
            
            # Add main result if set
            if match.result:
                winning_outcomes.append(match.result)
            
            logger.info(f"🔍 Winning outcomes for match {match.id}: {winning_outcomes}")
            
            resolved_win = 0
            resolved_lost = 0
            
            for bet in pending_bets:
                if bet.outcome in winning_outcomes:
                    # Winner
                    try:
                        coefficient = self._get_outcome_coefficient_for_bet(match.id, bet.outcome, session)
                        bet.result = 'win'
                        bet.win_amount = bet.amount * coefficient
                        resolved_win += 1
                        logger.info(f"✅ Bet {bet.id} ({bet.outcome}) resolved as WIN with amount {bet.win_amount:.2f}")
                    except Exception as coeff_e:
                        logger.error(f"Failed to get coefficient for {bet.outcome}: {coeff_e}")
                        bet.result = 'win'
                        bet.win_amount = bet.amount  # Fallback to 1x coefficient
                        resolved_win += 1
                else:
                    # Loser
                    bet.result = 'lost'
                    resolved_lost += 1
                    logger.info(f"❌ Bet {bet.id} ({bet.outcome}) resolved as LOST")
            
            logger.info(f"✅ Resolved {len(pending_bets)} bets for match {match.id}: {resolved_win} wins, {resolved_lost} losses")
            
        except Exception as e:
            logger.error(f"Failed to resolve bets for match {match.id}: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")

    def _get_outcome_coefficient_for_bet(self, match_id: int, outcome: str, session) -> float:
        """Get coefficient for a specific outcome from match outcomes.
        
        Args:
            match_id: The match ID
            outcome: The outcome name (e.g., 'UNDER', 'OVER', 'WIN1')
            session: Database session
            
        Returns:
            Coefficient value, defaults to 1.0 if not found
        """
        try:
            match_outcome = session.query(MatchOutcomeModel).filter(
                MatchOutcomeModel.match_id == match_id,
                MatchOutcomeModel.column_name == outcome
            ).first()
            
            if match_outcome:
                return match_outcome.float_value
            
            logger.warning(f"No coefficient found for outcome {outcome} in match {match_id}, using 1.0")
            return 1.0
            
        except Exception as e:
            logger.error(f"Failed to get coefficient for outcome {outcome}: {e}")
            return 1.0

    def _get_or_create_today_fixture(self) -> Optional[str]:
        """Get existing today's fixture or create a new one.
        
        This method ensures that new matches are created in today's fixture,
        not in yesterday's fixture. It uses multiple strategies to find existing
        today fixtures to prevent creating duplicates.
        
        A cache is used to prevent race conditions where multiple calls might
        create duplicate fixtures.
        """
        try:
            # Check cache first (valid for 60 seconds)
            current_time = time.time()
            if self._cached_today_fixture_id and (current_time - self._today_fixture_cache_time) < 60:
                logger.info(f"Using cached today fixture: {self._cached_today_fixture_id}")
                return self._cached_today_fixture_id
            
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone
                today = self._get_today_venue_date()
                
                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)
                
                # Strategy 1: Check for matches with start_time in today's range
                today_match_with_start_time = session.query(MatchModel).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= utc_start,
                    MatchModel.start_time < utc_end,
                    MatchModel.active_status == True
                ).order_by(MatchModel.created_at.desc()).first()
                
                if today_match_with_start_time:
                    fixture_id = today_match_with_start_time.fixture_id
                    logger.info(f"Found existing today fixture (by start_time): {fixture_id}")
                    # Cache the result
                    self._cached_today_fixture_id = fixture_id
                    self._today_fixture_cache_time = current_time
                    return fixture_id
                
                # Strategy 2: Check for matches created today (using created_at as fallback)
                # This catches matches that were just created and don't have start_time yet
                today_match_by_created = session.query(MatchModel).filter(
                    MatchModel.created_at >= utc_start,
                    MatchModel.created_at < utc_end,
                    MatchModel.active_status == True
                ).order_by(MatchModel.created_at.asc()).first()
                
                if today_match_by_created:
                    fixture_id = today_match_by_created.fixture_id
                    logger.info(f"Found existing today fixture (by created_at): {fixture_id}")
                    # Cache the result
                    self._cached_today_fixture_id = fixture_id
                    self._today_fixture_cache_time = current_time
                    return fixture_id
                
                # No today fixture exists - create a new one
                logger.info("No today fixture found - creating new fixture")
                session.close()
                
                new_fixture_id = self._initialize_new_fixture()
                if new_fixture_id:
                    logger.info(f"Created new today fixture: {new_fixture_id}")
                    # Cache the result
                    self._cached_today_fixture_id = new_fixture_id
                    self._today_fixture_cache_time = current_time
                return new_fixture_id
                
            finally:
                if session.is_active:
                    session.close()
                
        except Exception as e:
            logger.error(f"Failed to get or create today fixture: {e}")
            return None

    def _cancel_match_bets(self, match_id: int, session):
        """Cancel all pending bets for a match"""
        try:
            # Update all pending bets for this match to 'cancelled'
            cancelled_count = session.query(BetDetailModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.result == 'pending'
            ).update({'result': 'cancelled'})

            if cancelled_count > 0:
                logger.info(f"Cancelled {cancelled_count} pending bets for match {match_id}")

        except Exception as e:
            logger.error(f"Failed to cancel bets for match {match_id}: {e}")

    def _ensure_all_bets_resolved(self, match_id: int, result: str):
        """Safety net to ensure ALL pending bets are resolved when match ends.
        
        This method should be called whenever a match status becomes 'done'.
        It checks for any remaining pending bets and resolves them based on
        the match result. This catches any edge cases where bets might remain
        pending due to exceptions, ZIP extraction failures, or other issues.
        
        Args:
            match_id: The ID of the completed match
            result: The result of the match (used for fallback resolution)
        """
        try:
            session = self.db_manager.get_session()
            try:
                # Check for any remaining pending bets
                pending_bets = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.result == 'pending'
                ).all()

                if not pending_bets:
                    logger.debug(f"All bets already resolved for match {match_id}")
                    return

                logger.warning(f"⚠️ [SAFETY NET] Found {len(pending_bets)} pending bets for completed match {match_id} - forcing resolution")

                # Get match details for resolution
                match = session.query(MatchModel).filter_by(id=match_id).first()
                if not match:
                    logger.error(f"[SAFETY NET] Match {match_id} not found - cannot resolve bets")
                    return

                # Parse winning outcomes from match
                winning_outcomes = []
                if match.winning_outcomes:
                    try:
                        winning_outcomes = json.loads(match.winning_outcomes)
                        logger.info(f"[SAFETY NET] Match winning outcomes: {winning_outcomes}")
                    except (json.JSONDecodeError, TypeError) as e:
                        logger.warning(f"[SAFETY NET] Failed to parse winning_outcomes: {e}")

                # Get under/over result
                under_over_result = match.under_over_result if match else None
                logger.info(f"[SAFETY NET] Match under_over_result: {under_over_result}")

                # Resolve each pending bet
                resolved_win = 0
                resolved_lost = 0
                
                for bet in pending_bets:
                    is_winner = False
                    
                    # Check if bet outcome is in winning outcomes
                    if bet.outcome in winning_outcomes:
                        is_winner = True
                        logger.info(f"[SAFETY NET] Bet {bet.id} ({bet.outcome}) matches winning outcome")
                    # Check if bet is the under/over winner
                    elif bet.outcome == under_over_result:
                        is_winner = True
                        logger.info(f"[SAFETY NET] Bet {bet.id} ({bet.outcome}) matches under_over_result")
                    # Check if bet outcome matches the main result
                    elif bet.outcome == result:
                        is_winner = True
                        logger.info(f"[SAFETY NET] Bet {bet.id} ({bet.outcome}) matches main result")

                    if is_winner:
                        # Winner - calculate win amount
                        try:
                            coefficient = self._get_outcome_coefficient(match_id, bet.outcome, session)
                            bet.result = 'win'
                            bet.win_amount = bet.amount * coefficient
                            resolved_win += 1
                            logger.info(f"✅ [SAFETY NET] Force-resolved bet {bet.id} ({bet.outcome}) to WIN with amount {bet.win_amount:.2f}")
                        except Exception as coeff_e:
                            logger.error(f"[SAFETY NET] Failed to get coefficient for {bet.outcome}: {coeff_e}")
                            bet.result = 'win'
                            bet.win_amount = bet.amount  # Fallback to 1x coefficient
                            resolved_win += 1
                    else:
                        # Loser
                        bet.result = 'lost'
                        resolved_lost += 1
                        logger.info(f"❌ [SAFETY NET] Force-resolved bet {bet.id} ({bet.outcome}) to LOST")

                session.commit()
                logger.info(f"✅ [SAFETY NET] Force-resolved {len(pending_bets)} pending bets for match {match_id}: {resolved_win} wins, {resolved_lost} losses")

            finally:
                session.close()

        except Exception as e:
            logger.error(f"❌ [SAFETY NET] Failed to ensure all bets resolved for match {match_id}: {e}")
            import traceback
            logger.error(f"[SAFETY NET] Traceback: {traceback.format_exc()}")

    def _cleanup_orphaned_pending_bets(self):
        """Clean up any pending bets for matches that are already completed.
        
        This should be called during application initialization to resolve
        any orphaned bets from previous sessions that may have crashed or
        had errors during bet resolution.
        """
        try:
            logger.info("Checking for orphaned pending bets on completed matches...")
            session = self.db_manager.get_session()
            try:
                # Find pending bets for completed matches
                orphaned_bets = session.query(BetDetailModel).join(MatchModel).filter(
                    BetDetailModel.result == 'pending',
                    MatchModel.status == 'done',
                    MatchModel.active_status == True
                ).all()

                if not orphaned_bets:
                    logger.info("No orphaned pending bets found")
                    return

                logger.warning(f"⚠️ Found {len(orphaned_bets)} orphaned pending bets for completed matches")

                # Group by match_id for efficient processing
                bets_by_match = {}
                for bet in orphaned_bets:
                    if bet.match_id not in bets_by_match:
                        bets_by_match[bet.match_id] = []
                    bets_by_match[bet.match_id].append(bet)

                # Resolve each match's bets
                for match_id, bets in bets_by_match.items():
                    match = session.query(MatchModel).filter_by(id=match_id).first()
                    if match and match.result:
                        logger.info(f"Resolving {len(bets)} orphaned bets for match {match_id} with result '{match.result}'")
                        # Use the safety net to resolve
                        session.close()  # Close current session
                        self._ensure_all_bets_resolved(match_id, match.result)
                        session = self.db_manager.get_session()  # Reopen for next match
                    else:
                        logger.warning(f"Match {match_id} has no result - cancelling orphaned bets")
                        for bet in bets:
                            bet.result = 'cancelled'
                        session.commit()

                logger.info(f"✅ Completed cleanup of orphaned pending bets")

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to cleanup orphaned pending bets: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")

    def initialize(self) -> bool:
        """Initialize the games thread"""
        try:
            logger.info("Initializing GamesThread...")

            # STEP 1: Close matches from 2+ days ago with 'failed' status
            logger.info("🧹 Step 1: Cleaning up matches from 2+ days ago...")
            self._cleanup_old_incomplete_matches()

            # STEP 2: Clean up any stale 'ingame' matches from previous crashed sessions
            logger.info("🧹 Step 2: Cleaning up stale ingame matches...")
            self._cleanup_stale_ingame_matches()

            # STEP 3: Clean up any orphaned pending bets from previous crashed sessions
            # This ensures bets from matches that completed but had errors are resolved
            logger.info("🧹 Step 3: Checking for orphaned pending bets from previous sessions...")
            self._cleanup_orphaned_pending_bets()

            # STEP 3.5: Finalize pending matches that already have results set
            # This handles matches from yesterday or today that have results but weren't finalized
            logger.info("🧹 Step 3.5: Finalizing pending matches with results...")
            finalized_count = self._finalize_matches_with_results()
            if finalized_count > 0:
                logger.info(f"✅ Finalized {finalized_count} pending matches with results")

            # STEP 4: Check for yesterday's incomplete matches and prepare for continuation
            logger.info("🧹 Step 4: Checking for yesterday's incomplete matches...")
            yesterday_incomplete = self._get_yesterday_incomplete_matches()
            if yesterday_incomplete:
                logger.info(f"📋 Found {len(yesterday_incomplete)} incomplete matches from yesterday - will play these first")
                # Store the yesterday fixture ID for later processing during START_GAME
                self.yesterday_fixture_id = yesterday_incomplete[0].fixture_id
                # Create today's fixture now so it's ready when yesterday's matches complete
                today_fixture = self._get_or_create_today_fixture()
                if today_fixture:
                    self.pending_today_fixture_id = today_fixture
                    logger.info(f"📋 Pre-created today fixture {today_fixture} for after yesterday's matches complete")
            else:
                self.yesterday_fixture_id = None

            # STEP 5: Process pending matches in today's fixture
            logger.info("🧹 Step 5: Processing pending matches in today's fixture...")
            matches_to_play = self._process_today_pending_matches()
            if matches_to_play:
                logger.info(f"🎯 Found {len(matches_to_play)} pending matches set to 'bet' status - will play these first")
                # Store the fixture ID for these matches
                if not self.pending_today_fixture_id:
                    self.pending_today_fixture_id = matches_to_play[0].fixture_id

            # Register with message bus first
            self.message_queue = self.message_bus.register_component(self.name)

            # Subscribe to relevant messages
            self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_start_game)
            self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games)
            self.message_bus.subscribe(self.name, MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message)
            self.message_bus.subscribe(self.name, MessageType.MATCH_START, self._handle_match_start)
            self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_MATCH_DONE, self._handle_play_video_match_done)
            self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_RESULT_DONE, self._handle_play_video_result_done)
            self.message_bus.subscribe(self.name, MessageType.MATCH_DONE, self._handle_match_done)
            self.message_bus.subscribe(self.name, MessageType.GAME_STATUS, self._handle_game_status_request)
            self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status)
            self.message_bus.subscribe(self.name, MessageType.FORCE_API_UPDATE, self._handle_force_api_update)

            # Send ready status
            ready_message = MessageBuilder.system_status(
                sender=self.name,
                status="ready",
                details={
                    "component": "games_thread",
                    "capabilities": ["game_monitoring", "fixture_tracking"]
                }
            )
            self.message_bus.publish(ready_message)

            logger.info("GamesThread initialized successfully")
            return True

        except Exception as e:
            logger.error(f"Failed to initialize GamesThread: {e}")
            return False

    def run(self):
        """Main run loop for the games thread"""
        logger.info("GamesThread started")

        try:
            while self.running and not self._shutdown_event.is_set():
                try:
                    # Process any pending messages with shorter timeout for responsive shutdown
                    message = self.message_bus.get_message(self.name, timeout=0.05)
                    if message:
                        self._process_message(message)

                    # If a game is active, perform game-related operations
                    if self.game_active and self.current_fixture_id:
                        self._monitor_game_state()

                    # Periodic orphan bet cleanup - check every 10 seconds
                    current_time = time.time()
                    if current_time - self._last_orphan_cleanup_time >= self._orphan_cleanup_interval:
                        logger.debug("Running periodic orphan bet cleanup check...")
                        self._cleanup_orphaned_pending_bets()
                        self._last_orphan_cleanup_time = current_time

                    # Update heartbeat
                    self.heartbeat()

                    # Check shutdown event more frequently
                    if self._shutdown_event.wait(0.05):
                        break

                except Exception as e:
                    logger.error(f"GamesThread run loop error: {e}")
                    # Shorter sleep on error to be more responsive to shutdown
                    if not self._shutdown_event.wait(0.5):
                        continue
                    break

        except Exception as e:
            logger.error(f"GamesThread run failed: {e}")
        finally:
            self._cleanup()
            logger.info("GamesThread ended")

    def shutdown(self):
        """Shutdown the games thread"""
        logger.info("GamesThread shutdown requested")
        self._shutdown_event.set()
        self.game_active = False

    def _handle_start_game(self, message: Message):
        """Handle START_GAME message with comprehensive logic"""
        try:
            logger.info(f"Processing START_GAME message from {message.sender}")

            fixture_id = message.data.get("fixture_id")

            # PRIORITY CHECK: If we have yesterday's incomplete matches from initialization, use those first
            if self.yesterday_fixture_id and not fixture_id:
                logger.info(f"🎯 Found yesterday's incomplete fixture from initialization: {self.yesterday_fixture_id}")
                # Use the yesterday fixture ID that was detected during initialization
                fixture_id = self.yesterday_fixture_id
                # The pending_today_fixture_id should already be set from initialization
                logger.info(f"🎯 Will use pre-created today fixture: {self.pending_today_fixture_id}")

            if fixture_id:
                # Case 1: fixture_id is provided
                logger.info(f"Fixture ID {fixture_id} provided - checking fixture availability")

                session = self.db_manager.get_session()
                try:
                    # Check if any match with the fixture_id exists
                    fixture_matches = session.query(MatchModel).filter(
                        MatchModel.fixture_id == fixture_id,
                        MatchModel.active_status == True
                    ).all()

                    if fixture_matches:
                        logger.info(f"Fixture {fixture_id} exists with {len(fixture_matches)} matches")

                        # Check if the fixture is from yesterday
                        is_yesterday_fixture = self._is_fixture_from_yesterday(fixture_id, session)

                        if is_yesterday_fixture:
                            # Fixture is from yesterday - get or create today fixture but activate yesterday first
                            logger.info(f"Fixture {fixture_id} is from yesterday - getting/creating today fixture and activating yesterday to play remaining matches first")
                            new_fixture_id = self._get_or_create_today_fixture()
                            if new_fixture_id:
                                logger.info(f"Today fixture {new_fixture_id} ready - will play yesterday matches first, then switch to today")
                                # Store the today fixture ID for later use
                                self.pending_today_fixture_id = new_fixture_id
                                # Activate yesterday fixture to play remaining matches
                                self._activate_fixture(fixture_id, message)
                                return
                            else:
                                logger.warning("Could not get/create today fixture - activating yesterday fixture as fallback")
                                self._activate_fixture(fixture_id, message)
                                return
                        else:
                            # Fixture is from today - check if it has enough matches
                            # Check if there are at least 5 non-completed matches
                            non_completed_count = session.query(MatchModel).filter(
                                MatchModel.fixture_id == fixture_id,
                                MatchModel.active_status == True,
                                MatchModel.status.notin_(['done', 'cancelled', 'failed', 'paused'])
                            ).count()

                            logger.info(f"Fixture {fixture_id} has {non_completed_count} non-completed matches")

                            if non_completed_count >= 5:
                                # Enough matches, proceed with activation
                                logger.info(f"Fixture {fixture_id} has sufficient matches ({non_completed_count} >= 5) - activating")
                                self._activate_fixture(fixture_id, message)
                                return
                            else:
                                # Not enough matches - add matches to existing fixture
                                matches_needed = 5 - non_completed_count
                                logger.info(f"Fixture {fixture_id} needs {matches_needed} more matches - creating from templates")

                                # Create new matches from templates
                                template_matches = self._select_random_match_templates(matches_needed, session)
                                if template_matches:
                                    self._create_matches_from_templates(fixture_id, template_matches, session)
                                    logger.info(f"Created {len(template_matches)} new matches in fixture {fixture_id}")

                                    # Now activate the fixture
                                    self._activate_fixture(fixture_id, message)
                                    return
                                else:
                                    logger.warning(f"No match templates available to create matches for fixture {fixture_id}")
                                    self._send_response(message, "error", f"No match templates available for fixture {fixture_id}")
                                    return
                    else:
                        logger.info(f"Fixture {fixture_id} does not exist - proceeding as if no fixture_id was provided")
                        # Fall through to no fixture_id case

                finally:
                    session.close()

            # Case 2: No fixture_id provided or fixture doesn't exist
            logger.info("No fixture_id provided or fixture doesn't exist - checking database for available matches")

            session = self.db_manager.get_session()
            try:
                # Check if any matches are available in the database
                total_matches = session.query(MatchModel).filter(
                    MatchModel.active_status == True
                ).count()

                if total_matches > 0:
                    logger.info(f"Found {total_matches} matches in database")

                    # Get the last match in the matches table
                    last_match = session.query(MatchModel).filter(
                        MatchModel.active_status == True
                    ).order_by(MatchModel.created_at.desc()).first()

                    if last_match:
                        fixture_id = last_match.fixture_id
                        logger.info(f"Selected fixture {fixture_id} from last match (ID: {last_match.id})")

                        # Check if all matches in this fixture are completed
                        completed_count = session.query(MatchModel).filter(
                            MatchModel.fixture_id == fixture_id,
                            MatchModel.active_status == True,
                            MatchModel.status.in_(['done', 'cancelled', 'failed', 'paused'])
                        ).count()

                        total_fixture_matches = session.query(MatchModel).filter(
                            MatchModel.fixture_id == fixture_id,
                            MatchModel.active_status == True
                        ).count()

                        if completed_count == total_fixture_matches:
                            # All matches completed, get or create today fixture
                            logger.info(f"All {total_fixture_matches} matches in fixture {fixture_id} are completed - getting/creating today fixture")
                            new_fixture_id = self._get_or_create_today_fixture()
                            if new_fixture_id:
                                self._activate_fixture(new_fixture_id, message)
                                return
                            else:
                                logger.warning("Could not get/create today fixture")
                                self._send_response(message, "error", "Could not get/create today fixture")
                                return
                        else:
                            # Some matches are not completed, check if fixture is from yesterday
                            is_yesterday_fixture = self._is_fixture_from_yesterday(fixture_id, session)

                            if is_yesterday_fixture:
                                # Fixture is from yesterday - activate yesterday fixture first to play remaining matches, get/create today fixture for later
                                logger.info(f"Fixture {fixture_id} is from yesterday and has remaining matches - activating yesterday fixture first, will get/create today fixture after completion")
                                new_fixture_id = self._get_or_create_today_fixture()
                                if new_fixture_id:
                                    # Store the today fixture ID for later use
                                    self.pending_today_fixture_id = new_fixture_id
                                    logger.info(f"Today fixture {new_fixture_id} ready - will play yesterday matches first, then switch to today")
                                    # Activate yesterday fixture to play remaining matches
                                    self._activate_fixture(fixture_id, message)
                                    return
                                else:
                                    logger.warning("Could not get/create today fixture - activating yesterday fixture as fallback")
                                    self._activate_fixture(fixture_id, message)
                                    return
                            else:
                                # Fixture is from today - check if enough matches are available
                                logger.info(f"Fixture {fixture_id} has active matches - activating")

                                # Check if at least 5 non-completed matches are available
                                non_completed_count = session.query(MatchModel).filter(
                                    MatchModel.fixture_id == fixture_id,
                                    MatchModel.active_status == True,
                                    MatchModel.status.notin_(['done', 'cancelled', 'failed', 'paused'])
                                ).count()

                                logger.info(f"Fixture {fixture_id} has {non_completed_count} non-completed matches")

                                if non_completed_count >= 5:
                                    # Enough matches, activate
                                    self._activate_fixture(fixture_id, message)
                                    return
                                else:
                                    # Not enough matches, create new ones from templates
                                    matches_needed = 5 - non_completed_count
                                    logger.info(f"Fixture {fixture_id} needs {matches_needed} more matches - creating from templates")

                                    template_matches = self._select_random_match_templates(matches_needed, session)
                                    if template_matches:
                                        self._create_matches_from_templates(fixture_id, template_matches, session)
                                        logger.info(f"Created {len(template_matches)} new matches in fixture {fixture_id}")

                                        # Now activate the fixture
                                        self._activate_fixture(fixture_id, message)
                                        return
                                    else:
                                        logger.warning(f"No match templates available to create matches for fixture {fixture_id}")
                                        self._send_response(message, "error", f"No match templates available for fixture {fixture_id}")
                                        return
                    else:
                        logger.warning("No last match found despite having matches in database")
                        # Fall through to create new fixture from templates
                else:
                    logger.info("No matches available in database - creating new fixture from templates")

                # Get or create today fixture
                new_fixture_id = self._get_or_create_today_fixture()
                if new_fixture_id:
                    self._activate_fixture(new_fixture_id, message)
                    return
                else:
                    logger.warning("Could not get or create today fixture - waiting for templates to become available")
                    self._send_response(message, "waiting_for_downloads", "Waiting for match templates to be downloaded and validated")
                    return

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to handle START_GAME message: {e}")
            self._send_response(message, "error", str(e))

    def _handle_schedule_games(self, message: Message):
        """Handle SCHEDULE_GAMES message - change status of pending matches to scheduled"""
        try:
            fixture_id = message.data.get("fixture_id")

            if not fixture_id:
                # If no fixture_id provided, find the last fixture with pending matches
                fixture_id = self._find_last_fixture_with_pending_matches()

            if fixture_id:
                logger.info(f"Scheduling games for fixture: {fixture_id}")

                # Update status of all pending matches in the fixture to scheduled
                updated_count = self._schedule_fixture_matches(fixture_id)

                if updated_count > 0:
                    logger.info(f"Successfully scheduled {updated_count} matches for fixture {fixture_id}")

                    # Send success response
                    response = Message(
                        type=MessageType.GAME_STATUS,
                        sender=self.name,
                        recipient=message.sender,
                        data={
                            "status": "scheduled",
                            "fixture_id": fixture_id,
                            "matches_scheduled": updated_count,
                            "timestamp": time.time()
                        },
                        correlation_id=message.correlation_id
                    )
                    self.message_bus.publish(response)
                else:
                    logger.warning(f"No pending matches found to schedule for fixture {fixture_id}")

                    # Send response indicating no matches were scheduled
                    response = Message(
                        type=MessageType.GAME_STATUS,
                        sender=self.name,
                        recipient=message.sender,
                        data={
                            "status": "no_matches",
                            "fixture_id": fixture_id,
                            "message": "No pending matches found to schedule",
                            "timestamp": time.time()
                        },
                        correlation_id=message.correlation_id
                    )
                    self.message_bus.publish(response)
            else:
                logger.warning("No fixture with pending matches found")

                # Send error response
                error_response = Message(
                    type=MessageType.GAME_STATUS,
                    sender=self.name,
                    recipient=message.sender,
                    data={
                        "status": "error",
                        "error": "No fixture with pending matches found",
                        "timestamp": time.time()
                    },
                    correlation_id=message.correlation_id
                )
                self.message_bus.publish(error_response)

        except Exception as e:
            logger.error(f"Failed to handle SCHEDULE_GAMES message: {e}")

            # Send error response
            error_response = Message(
                type=MessageType.GAME_STATUS,
                sender=self.name,
                recipient=message.sender,
                data={
                    "status": "error",
                    "error": str(e),
                    "timestamp": time.time()
                },
                correlation_id=message.correlation_id
            )
            self.message_bus.publish(error_response)

    def _handle_shutdown_message(self, message: Message):
        """Handle shutdown message"""
        logger.info(f"Shutdown message received from {message.sender}")
        self._shutdown_event.set()
        self.game_active = False

    def _handle_match_start(self, message: Message):
        """Handle MATCH_START message and determine match result based on betting"""
        try:
            fixture_id = message.data.get("fixture_id")
            match_id = message.data.get("match_id")

            logger.info(f"Processing MATCH_START for fixture {fixture_id}, match {match_id}")

            self._set_match_status(match_id, 'ingame')
            uores = "UNDER"

            # Calculate parameters for extract_match() call
            # Get the match number from the match
            session = self.db_manager.get_session()
            try:
                match_obj = session.query(MatchModel).filter_by(id=match_id).first()
                match_number = match_obj.match_number if match_obj else 0

                # Log both IDs for debugging
                logger.info(f"📊 [MATCH INFO] Match #{match_number} (DB ID: {match_id}), Fixture: {fixture_id}")

                # Get accumulated balance (shortfall/surplus from previous extractions)
                balance = self._get_global_redistribution_adjustment(session)
                logger.info(f"💰 [BET CALC] Accumulated balance from DB: {balance:.2f}")

                # Calculate total payin from ALL bets (UNDER + OVER + other outcomes)
                # Get UNDER bets
                under_bets = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.outcome == 'UNDER',
                    BetDetailModel.result == 'pending',
                    BetDetailModel.result != 'cancelled'
                ).all()
                under_amount = sum(bet.amount for bet in under_bets) if under_bets else 0.0
                logger.info(f"💰 [BET CALC] UNDER bets: {len(under_bets)} bets, total amount: {under_amount:.2f}")
                if under_bets:
                    for bet in under_bets:
                        logger.info(f"💰 [BET CALC]   - Bet {bet.id}: amount={bet.amount:.2f}")

                # Get OVER bets
                over_bets = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.outcome == 'OVER',
                    BetDetailModel.result == 'pending',
                    BetDetailModel.result != 'cancelled'
                ).all()
                over_amount = sum(bet.amount for bet in over_bets) if over_bets else 0.0
                logger.info(f"💰 [BET CALC] OVER bets: {len(over_bets)} bets, total amount: {over_amount:.2f}")
                if over_bets:
                    for bet in over_bets:
                        logger.info(f"💰 [BET CALC]   - Bet {bet.id}: amount={bet.amount:.2f}")

                # Get all other bets (non UNDER/OVER)
                other_bets = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.result == 'pending',
                    BetDetailModel.result != 'cancelled',
                    ~BetDetailModel.outcome.in_(['UNDER', 'OVER'])
                ).all()
                other_amount = sum(bet.amount for bet in other_bets) if other_bets else 0.0
                logger.info(f"💰 [BET CALC] Other bets: {len(other_bets)} bets, total amount: {other_amount:.2f}")

                # Total payin is sum of all bets
                payin = under_amount + over_amount + other_amount
                logger.info(f"💰 [BET CALC] Total payin: {payin:.2f} (UNDER {under_amount:.2f} + OVER {over_amount:.2f} + Other {other_amount:.2f})")

                # Get redistribution cap percentage
                cap = self._get_redistribution_cap()
                logger.info(f"💰 [BET CALC] Redistribution cap: {cap:.2f}%")

                # Get UNDER/OVER coefficients from the specific match (not the fixture)
                under_coeff, over_coeff = self._get_match_coefficients(match_id, session)
                logger.info(f"💰 [BET CALC] UNDER coefficient: {under_coeff}")
                logger.info(f"💰 [BET CALC] OVER coefficient: {over_coeff}")

                # Calculate UNDER payout
                under_payout = under_amount * under_coeff if under_coeff else 0.0
                logger.info(f"💰 [BET CALC] UNDER payout calculation: {under_amount:.2f} × {under_coeff} = {under_payout:.2f}")

                # Calculate OVER payout
                over_payout = over_amount * over_coeff if over_coeff else 0.0
                logger.info(f"💰 [BET CALC] OVER payout calculation: {over_amount:.2f} × {over_coeff} = {over_payout:.2f}")

                # Build RESULTS dictionary with payouts for each result
                # Get all result options and their associated outcomes
                results_payouts = {}
                from ..database.models import ResultOptionModel, ExtractionAssociationModel

                result_options = session.query(ResultOptionModel).filter(
                    ResultOptionModel.is_active == True,
                    ~ResultOptionModel.result_name.in_(['UNDER', 'OVER'])
                ).all()

                for result_option in result_options:
                    result_name = result_option.result_name

                    # Get associated outcomes for this result
                    associations = session.query(ExtractionAssociationModel).filter(
                        ExtractionAssociationModel.extraction_result == result_name
                    ).all()

                    total_payout = 0.0
                    outcome_list = []

                    logger.info(f"💰 [BET CALC] Result '{result_name}': {len(associations)} associated outcomes")

                    for association in associations:
                        outcome_name = association.outcome_name
                        outcome_list.append(outcome_name)

                        # Get coefficient for this outcome
                        match_outcome = session.query(MatchOutcomeModel).filter(
                            MatchOutcomeModel.match_id == match_id,
                            MatchOutcomeModel.column_name == outcome_name
                        ).first()

                        if match_outcome:
                            coefficient = match_outcome.float_value

                            # Get total bets for this outcome
                            outcome_bets = session.query(BetDetailModel).filter(
                                BetDetailModel.match_id == match_id,
                                BetDetailModel.outcome == outcome_name,
                                BetDetailModel.result == 'pending'
                            ).all()

                            outcome_amount = sum(bet.amount for bet in outcome_bets) if outcome_bets else 0.0
                            outcome_payout = outcome_amount * coefficient
                            total_payout += outcome_payout
                            
                            logger.info(f"💰 [BET CALC]   Outcome '{outcome_name}': coeff={coefficient}, bets={len(outcome_bets)}, amount={outcome_amount:.2f}, payout={outcome_payout:.2f}")
                        else:
                            logger.warning(f"💰 [BET CALC]   Outcome '{outcome_name}': NO coefficient found!")

                    # Store in results dict with format: [list_of_outcomes, calculated_total_payout]
                    results_payouts[result_name] = [', '.join(outcome_list), total_payout]
                    logger.info(f"💰 [BET CALC] Result '{result_name}': outcomes=[{', '.join(outcome_list)}], total_payout={total_payout:.2f}")

                # Build the payouts dictionary with the required schema
                payouts = {
                    "UNDER": under_payout,
                    "OVER": over_payout,
                    "RESULTS": results_payouts
                }

                # Get all match outcomes for building odds dictionary
                match_outcomes = session.query(MatchOutcomeModel).filter(
                    MatchOutcomeModel.match_id == match_id
                ).all()

                # Build the odds dictionary from match outcomes
                odds = {}
                for outcome in match_outcomes:
                    odds[outcome.column_name] = outcome.float_value

                # Calculate uopayin (UNDER + OVER bets) and mpayin (all other bets)
                uopayin = under_amount + over_amount
                mpayin = other_amount

                logger.info(f"📊 [EXTRACT_MATCH] Match #{match_number} (DB ID: {match_id}): balance={balance:.2f}, uopayin={uopayin:.2f}, mpayin={mpayin:.2f}, cap={cap:.2f}")
                logger.info(f"📊 [EXTRACT_MATCH] Coefficients used: UNDER={under_coeff}, OVER={over_coeff}")
                logger.info(f"📊 [EXTRACT_MATCH] Payouts: UNDER={under_payout:.2f}, OVER={over_payout:.2f}")
                for rname, rdata in results_payouts.items():
                    logger.info(f"📊 [EXTRACT_MATCH]   Result '{rname}': outcomes='{rdata[0]}', payout={rdata[1]:.2f}")
                logger.info(f"📊 [EXTRACT_MATCH] Odds: {odds}")

                # Call extract_match before the first extraction
                uores, mres, impact, winning_out = extract_match(balance, uopayin, mpayin, match_number, cap, payouts, odds)
                checklist = set(payouts["RESULTS"].keys())
                winning_outcomes = [item for item in winning_out.split(", ")  if item in checklist]


                # Store the under/over result from video selection calculation
                logger.info(f"Storing under_over_result '{uores}' for match {match_id}")
                self._store_under_over_result(match_id, uores)

                # Send PLAY_VIDEO_MATCH with calculated result
                self._send_play_video_match(fixture_id, match_id, uores)

                # Perform result extraction immediately after sending PLAY_VIDEO_MATCH
                logger.info(f"🔍 [EXTRACTION DEBUG] Starting immediate extraction after PLAY_VIDEO_MATCH for fixture {fixture_id}, match {match_id}")
                extracted_result = mres
                logger.info(f"✅ [EXTRACTION DEBUG] Extraction completed immediately: {extracted_result}, winning outcomes: {winning_outcomes}")

                # Step 8: Update bet results
                logger.info(f"💾 [EXTRACTION DEBUG] Step 8: Updating bet results for match {match_id}")
                self._update_bet_results(match_id, mres, uores, winning_outcomes+[uores], session)

                # Step 9: Collect statistics
                logger.info(f"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics")
                self._collect_match_statistics(match_id, fixture_id, mres, session)

                # Step 10: Update global redistribution adjustment tracking
                # Calculate the ACTUAL adjustment from real values, not the theoretical impact
                total_payin = uopayin + mpayin
                expected_redistribution = total_payin * (cap / 100.0)
                
                # Get the actual payout for the winning UNDER/OVER outcome
                uo_payout = payouts[uores]
                # Get the actual payout for the winning result outcome
                result_payout = payouts["RESULTS"][mres][1] if mres in payouts.get("RESULTS", {}) else 0
                actual_redistributed = uo_payout + result_payout
                
                # Positive = surplus (house kept more than expected, can redistribute in future)
                # Negative = shortfall (house paid out more than expected, needs to recover)
                actual_adjustment = expected_redistribution - actual_redistributed
                
                logger.info(f"💰 [EXTRACTION DEBUG] Step 10: Updating global redistribution adjustment tracking")
                logger.info(f"💰 [ADJUSTMENT CALC] Total payin: {total_payin:.2f}")
                logger.info(f"💰 [ADJUSTMENT CALC] Cap: {cap:.2f}%")
                logger.info(f"💰 [ADJUSTMENT CALC] Expected redistribution: {total_payin:.2f} × {cap/100:.4f} = {expected_redistribution:.2f}")
                logger.info(f"💰 [ADJUSTMENT CALC] UO winner: {uores}, UO payout: {uo_payout:.2f}")
                logger.info(f"💰 [ADJUSTMENT CALC] Result winner: {mres}, Result payout: {result_payout:.2f}")
                logger.info(f"💰 [ADJUSTMENT CALC] Actual redistributed: {uo_payout:.2f} + {result_payout:.2f} = {actual_redistributed:.2f}")
                logger.info(f"💰 [ADJUSTMENT CALC] Adjustment: {expected_redistribution:.2f} - {actual_redistributed:.2f} = {actual_adjustment:.2f}")
                logger.info(f"💰 [ADJUSTMENT CALC] Interpretation: {'SURPLUS (house kept more)' if actual_adjustment > 0 else 'SHORTFALL (house paid more)'}")
                self._update_global_redistribution_adjustment(actual_adjustment, total_payin,
                    actual_redistributed, cap, session)
            finally:
                session.close()


        except Exception as e:
            logger.error(f"Failed to handle MATCH_START message: {e}")
            # Fallback to random selection if betting calculation fails
            try:
                fixture_id = message.data.get("fixture_id")
                match_id = message.data.get("match_id")
                result = self._weighted_random_selection(1.0, 1.0)  # Equal weights
                # Store the fallback under/over result
                logger.info(f"Storing fallback under_over_result '{result}' for match {match_id}")
                self._store_under_over_result(match_id, result)
                self._send_play_video_match(fixture_id, match_id, result)
            except Exception as fallback_e:
                logger.error(f"Fallback betting calculation also failed: {fallback_e}")

    def _handle_game_status_request(self, message: Message):
        """Handle GAME_STATUS requests from Qt player"""
        try:
            logger.info(f"Received GAME_STATUS request from {message.sender}")

            # Determine current game status
            game_status = self._determine_game_status()

            # If status is "already_active" but game is not active, activate the fixture
            # But NOT if we're waiting for downloads
            if game_status == "already_active" and not self.game_active:
                logger.info("Status is 'already_active' but game is not active - activating fixture")
                active_fixture = self._find_active_today_fixture()
                if active_fixture:
                    # Create a dummy message for activation
                    dummy_message = Message(
                        type=MessageType.START_GAME,
                        sender=message.sender,
                        recipient=self.name,
                        data={"timestamp": time.time()},
                        correlation_id=message.correlation_id
                    )
                    self._activate_fixture(active_fixture, dummy_message)
                    # Update status after activation
                    game_status = "started"
                else:
                    logger.warning("Could not find active fixture to activate")
            elif game_status == "waiting_for_downloads":
                logger.info("Game status is 'waiting_for_downloads' - not activating fixture until all ZIP files are available")

            # Send GAME_STATUS response back to the requester
            response = Message(
                type=MessageType.GAME_STATUS,
                sender=self.name,
                recipient=message.sender,
                data={
                    "status": game_status,
                    "fixture_id": self.current_fixture_id,
                    "game_active": self.game_active,
                    "timestamp": time.time()
                },
                correlation_id=message.correlation_id
            )
            # Broadcast the response instead of sending to specific recipient
            self.message_bus.publish(response, broadcast=True)
            logger.info(f"Broadcast game status response: {game_status}")

        except Exception as e:
            logger.error(f"Failed to handle game status request: {e}")

    def _process_message(self, message: Message):
        """Process incoming messages"""
        try:
            logger.debug(f"GamesThread processing message: {message}")

            # Handle messages directly since broadcast messages don't trigger subscription handlers
            if message.type == MessageType.START_GAME:
                self._handle_start_game(message)
            elif message.type == MessageType.SCHEDULE_GAMES:
                self._handle_schedule_games(message)
            elif message.type == MessageType.SYSTEM_SHUTDOWN:
                self._handle_shutdown_message(message)
            elif message.type == MessageType.SYSTEM_STATUS:
                self._handle_system_status(message)
            elif message.type == MessageType.GAME_UPDATE:
                self._handle_game_update(message)
            elif message.type == MessageType.PLAY_VIDEO_MATCH_DONE:
                self._handle_play_video_match_done(message)
            elif message.type == MessageType.PLAY_VIDEO_RESULT_DONE:
                self._handle_play_video_result_done(message)
            elif message.type == MessageType.MATCH_DONE:
                self._handle_match_done(message)
            elif message.type == MessageType.MATCH_START:
                self._handle_match_start(message)
            elif message.type == MessageType.GAME_STATUS:
                self._handle_game_status_request(message)
            elif message.type == MessageType.START_INTRO:
                self._handle_start_intro(message)

        except Exception as e:
            logger.error(f"Failed to process message: {e}")

    def _handle_game_update(self, message: Message):
        """Handle game update messages"""
        try:
            update_data = message.data
            logger.debug(f"Game update received: {update_data}")

            # Process game update data as needed
            # This could include updating match states, processing outcomes, etc.

        except Exception as e:
            logger.error(f"Failed to handle game update: {e}")

    def _handle_start_intro(self, message: Message):
        """Handle START_INTRO message - broadcast GAME_STARTED"""
        try:
            fixture_id = message.data.get("fixture_id")
            logger.info(f"Received START_INTRO message for fixture {fixture_id} - broadcasting GAME_STARTED")

            # Broadcast GAME_STARTED message to notify all components that game has started with this fixture
            game_started_message = MessageBuilder.game_started(
                sender=self.name,
                fixture_id=fixture_id
            )
            self.message_bus.publish(game_started_message, broadcast=True)
            logger.info(f"🎯 Broadcast GAME_STARTED message for fixture {fixture_id}")

        except Exception as e:
            logger.error(f"Failed to handle START_INTRO message: {e}")

    def _find_last_fixture_with_pending_matches(self) -> Optional[str]:
        """Find the last fixture that has pending matches"""
        try:
            session = self.db_manager.get_session()
            try:
                # Query for matches with PENDING status
                pending_matches = session.query(MatchModel).filter(
                    MatchModel.status == 'pending',
                    MatchModel.active_status == True
                ).order_by(MatchModel.fixture_active_time.desc()).all()

                if pending_matches:
                    # Get the fixture_id from the most recent pending match
                    latest_match = pending_matches[0]
                    fixture_id = latest_match.fixture_id
                    logger.info(f"Found fixture with pending matches: {fixture_id} ({len(pending_matches)} matches)")
                    return fixture_id
                else:
                    logger.info("No pending matches found")
                    return None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to find last fixture with pending matches: {e}")
            return None

    def _schedule_fixture_matches(self, fixture_id: str) -> int:
        """Update status of all pending matches in a fixture to scheduled"""
        try:
            session = self.db_manager.get_session()
            try:
                # Query for pending matches in the specified fixture
                pending_matches = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.status == 'pending',
                    MatchModel.active_status == True
                ).all()

                updated_count = 0

                for match in pending_matches:
                    # Change status from PENDING to SCHEDULED
                    match.status = 'scheduled'
                    logger.debug(f"Scheduling match #{match.match_number}: {match.fighter1_township} vs {match.fighter2_township} - status changed to {match.status}")
                    updated_count += 1

                # Commit the changes
                session.commit()
                logger.info(f"Scheduled {updated_count} matches for fixture {fixture_id}")

                return updated_count

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to schedule matches for fixture {fixture_id}: {e}")
            return 0

    def _monitor_game_state(self):
        """Monitor the current game state"""
        try:
            if not self.current_fixture_id:
                return

            # Check if there are still pending or scheduled matches for this fixture
            session = self.db_manager.get_session()
            try:
                active_count = session.query(MatchModel).filter(
                    MatchModel.fixture_id == self.current_fixture_id,
                    MatchModel.status.in_(['pending', 'scheduled', 'bet', 'ingame']),
                    MatchModel.active_status == True
                ).count()

                if active_count == 0:
                    logger.info(f"All matches completed for fixture {self.current_fixture_id} - checking for new fixture creation")

                    # Check if we need to create new matches and if current fixture's first match is from yesterday
                    need_new_matches = True
                    current_fixture_is_yesterday = self._is_fixture_from_yesterday(self.current_fixture_id, session)

                    if need_new_matches:
                        if self.pending_today_fixture_id or current_fixture_is_yesterday:
                            # Fixture is from yesterday (either detected now or was pre-detected) - switch fixtures
                            if self.pending_today_fixture_id:
                                today_fixture_id = self.pending_today_fixture_id
                                self.pending_today_fixture_id = None  # Clear it
                                logger.info(f"Yesterday fixture exhausted - switching to pre-created today fixture {today_fixture_id}")
                                # Switch to the pre-created today fixture
                                self.current_fixture_id = today_fixture_id
                                # Start the game with the today fixture
                                self._activate_fixture(today_fixture_id, Message(
                                    type=MessageType.START_GAME,
                                    sender=self.name,
                                    recipient=self.name,
                                    data={"fixture_id": today_fixture_id, "timestamp": time.time()},
                                    correlation_id=None
                                ))
                                return
                            else:
                                # Fallback: create new fixture if no pre-created one exists
                                logger.info("Current fixture is from yesterday and needs new matches - creating new fixture with new fixture_id and restarting match numbers from 1")
                                new_fixture_id = self._create_new_fixture_for_continuation()
                                if new_fixture_id:
                                    logger.info(f"Created new fixture {new_fixture_id} for continuation - switching to new fixture for today's matches")
                                    # Switch to the new fixture for today's matches
                                    self.current_fixture_id = new_fixture_id
                                    # Start the game with the new fixture
                                    self._activate_fixture(new_fixture_id, Message(
                                        type=MessageType.START_GAME,
                                        sender=self.name,
                                        recipient=self.name,
                                        data={"fixture_id": new_fixture_id, "timestamp": time.time()},
                                        correlation_id=None
                                    ))
                                    return
                                else:
                                    logger.warning("Could not create new fixture for continuation - stopping game since current fixture is from yesterday")
                                    # Cannot create new fixture and cannot add to yesterday's fixture - stop the game
                                    self.game_active = False
                                    completed_message = Message(
                                        type=MessageType.GAME_STATUS,
                                        sender=self.name,
                                        data={
                                            "status": "completed_cannot_create_new_fixture",
                                            "fixture_id": self.current_fixture_id,
                                            "timestamp": time.time()
                                        }
                                    )
                                    self.message_bus.publish(completed_message)
                                    self.current_fixture_id = None
                                    return
                        else:
                            # Current fixture is from today - can add matches to it
                            logger.info(f"Creating new matches in current fixture {self.current_fixture_id}")

                            # First try: Create 5 new matches from match templates
                            template_matches = self._select_random_match_templates(5, session)
                            if template_matches:
                                self._create_matches_from_templates(self.current_fixture_id, template_matches, session)
                                logger.info(f"Created 5 new matches in fixture {self.current_fixture_id} from match templates")
                                return

                            # Second try: Create 5 new matches from old completed matches
                            logger.info("No match templates available, trying to reuse old completed matches")
                            old_matches = self._select_random_completed_matches(5, session)
                            if old_matches:
                                self._create_matches_from_old_matches(self.current_fixture_id, old_matches, session)
                                logger.info(f"Created 5 new matches in fixture {self.current_fixture_id} from old matches")
                                return

                        # No matches available at all - stop the game
                        logger.warning("No match templates or old completed matches found - cannot create new matches")
                        self.game_active = False
                        completed_message = Message(
                            type=MessageType.GAME_STATUS,
                            sender=self.name,
                            data={
                                "status": "completed_no_matches_available",
                                "fixture_id": self.current_fixture_id,
                                "timestamp": time.time()
                            }
                        )
                        self.message_bus.publish(completed_message)
                        self.current_fixture_id = None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to monitor game state: {e}")

    def _send_response(self, original_message: Message, status: str, message: str = None):
        """Send response message back to the sender"""
        try:
            response_data = {
                "status": status,
                "timestamp": time.time()
            }

            if message:
                response_data["message"] = message

            response = Message(
                type=MessageType.GAME_STATUS,
                sender=self.name,
                recipient=original_message.sender,
                data=response_data,
                correlation_id=original_message.correlation_id
            )
            self.message_bus.publish(response)

            # For timer-related failures, also send to core so it can handle timer reset
            if status in ["waiting_for_downloads", "discarded", "error", "no_matches", "no_fixtures_available"]:
                core_response = Message(
                    type=MessageType.GAME_STATUS,
                    sender=self.name,
                    recipient="core",
                    data=response_data,
                    correlation_id=original_message.correlation_id
                )
                self.message_bus.publish(core_response)

        except Exception as e:
            logger.error(f"Failed to send response: {e}")

    def _is_fixture_all_terminal(self, fixture_id: str) -> bool:
        """Check if all matches in a fixture are in terminal states (done, cancelled, failed, paused)"""
        try:
            session = self.db_manager.get_session()
            try:
                # Get all matches for this fixture
                matches = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.active_status == True
                ).all()

                if not matches:
                    return False  # No matches means not terminal

                # Check if all matches are in terminal states
                terminal_states = ['done', 'cancelled', 'failed', 'paused']
                return all(match.status in terminal_states for match in matches)

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to check if fixture {fixture_id} is terminal: {e}")
            return False

    def _are_all_zips_validated_for_fixture(self, fixture_id: str) -> bool:
        """Check if all ZIP files for matches in a fixture are validated"""
        try:
            session = self.db_manager.get_session()
            try:
                # Get all matches with ZIP files
                matches_with_zips = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.active_status == True,
                    MatchModel.zip_filename.isnot(None)
                ).all()

                if not matches_with_zips:
                    return True  # No ZIPs needed

                # Check if all have valid status
                return all(match.zip_validation_status == 'valid' for match in matches_with_zips)

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to check ZIP validation status for fixture {fixture_id}: {e}")
            return False

    def _are_any_zips_being_validated(self) -> bool:
        """Check if any ZIP files are currently being validated system-wide"""
        try:
            session = self.db_manager.get_session()
            try:
                # Check if any matches have ZIP validation actively in progress (not just pending)
                validating_count = session.query(MatchModel).filter(
                    MatchModel.zip_validation_status == 'validating',
                    MatchModel.active_status == True,
                    MatchModel.zip_filename.isnot(None)
                ).count()

                return validating_count > 0

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to check if any ZIPs are being validated: {e}")
            return False

    def _mark_all_zips_as_validated(self):
        """Mark all ZIP files as validated (used after fixture update completion)"""
        try:
            session = self.db_manager.get_session()
            try:
                # Update all matches with ZIP files to have validated status
                updated_count = session.query(MatchModel).filter(
                    MatchModel.zip_filename.isnot(None),
                    MatchModel.active_status == True,
                    MatchModel.zip_validation_status != 'valid'
                ).update({'zip_validation_status': 'valid'})

                session.commit()
                logger.info(f"Marked {updated_count} ZIP files as validated")

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to mark ZIPs as validated: {e}")

    def _has_today_fixtures_all_terminal(self) -> bool:
        """Check if all fixtures with today's matches are in terminal states"""
        try:
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone (for day change detection)
                today = self._get_today_venue_date()

                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)

                # Find all fixtures that have matches with today's start_time
                fixtures_with_today_matches = session.query(MatchModel.fixture_id).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= utc_start,
                    MatchModel.start_time < utc_end
                ).distinct().all()

                if not fixtures_with_today_matches:
                    return False  # No today's fixtures

                # Check each fixture
                for fixture_row in fixtures_with_today_matches:
                    fixture_id = fixture_row.fixture_id
                    if not self._is_fixture_all_terminal(fixture_id):
                        return False  # Found a non-terminal fixture

                return True  # All fixtures are terminal

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to check today's fixtures terminal status: {e}")
            return False

    def _handle_ingame_matches(self, message: Message) -> bool:
        """Handle matches currently in 'ingame' status. Returns True if message was handled."""
        try:
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone (for day change detection)
                today = self._get_today_venue_date()

                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)

                # Find fixtures with ingame matches today
                ingame_matches = session.query(MatchModel).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= utc_start,
                    MatchModel.start_time < utc_end,
                    MatchModel.status == 'ingame',
                    MatchModel.active_status == True
                ).all()

                if not ingame_matches:
                    return False  # No ingame matches, continue processing

                # Get unique fixture IDs
                fixture_ids = list(set(match.fixture_id for match in ingame_matches))

                for fixture_id in fixture_ids:
                    # Check if timer is running for this fixture
                    # This is a simplified check - in real implementation you'd check the match_timer component
                    timer_running = self._is_timer_running_for_fixture(fixture_id)

                    if not timer_running:
                        # Timer not running, change status to pending and set active_status to False
                        logger.info(f"Timer not running for fixture {fixture_id}, changing ingame matches to pending and setting active_status to False")
                        self._change_fixture_matches_status(fixture_id, 'ingame', 'pending', active_status_to_set=False)

                        # Check if this was the only non-terminal fixture
                        if self._is_only_non_terminal_fixture(fixture_id):
                            logger.info("This was the only non-terminal fixture - discarding START_GAME message")
                            self._send_response(message, "discarded", "Timer not running and no other active fixtures")
                            return True
                    else:
                        # Timer is running, check for other pending/bet/scheduled matches
                        other_active_matches = session.query(MatchModel).filter(
                            MatchModel.fixture_id == fixture_id,
                            MatchModel.status.in_(['pending', 'bet', 'scheduled']),
                            MatchModel.active_status == True
                        ).all()

                        if other_active_matches:
                            # Change first pending/bet/scheduled match to bet status
                            first_match = other_active_matches[0]
                            if first_match.status != 'bet':
                                logger.info(f"Changing match {first_match.match_number} status to bet")
                                first_match.status = 'bet'
                                session.commit()

                        # Timer is running, discard the message
                        logger.info(f"Timer running for fixture {fixture_id} - discarding START_GAME message")
                        self._send_response(message, "discarded", "Timer already running for active fixture")
                        return True

                return False  # Continue processing

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to handle ingame matches: {e}")
            return False

    def _is_timer_running_for_fixture(self, fixture_id: str) -> bool:
        """Check if timer is running for a specific fixture"""
        # This is a simplified implementation
        # In a real implementation, you'd check the match_timer component status
        return self.current_fixture_id == fixture_id and self.game_active

    def _change_fixture_matches_status(self, fixture_id: str, from_status: str, to_status: str, active_status_to_set: Optional[bool] = None):
        """Change status of matches in a fixture from one status to another"""
        try:
            session = self.db_manager.get_session()
            try:
                matches = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.status == from_status,
                    MatchModel.active_status == True
                ).all()

                for match in matches:
                    logger.info(f"Changing match {match.match_number} status from {from_status} to {to_status}")
                    match.status = to_status
                    if active_status_to_set is not None:
                        match.active_status = active_status_to_set
                        logger.info(f"Setting active_status to {active_status_to_set} for match {match.match_number}")

                session.commit()

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to change match statuses: {e}")

    def _is_only_non_terminal_fixture(self, fixture_id: str) -> bool:
        """Check if this is the only non-terminal fixture"""
        try:
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone (for day change detection)
                today = self._get_today_venue_date()

                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)

                # Find all fixtures with today's matches
                all_fixtures = session.query(MatchModel.fixture_id).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= utc_start,
                    MatchModel.start_time < utc_end
                ).distinct().all()

                # Check each fixture except the current one
                terminal_states = ['done', 'cancelled', 'failed', 'paused']
                non_terminal_count = 0

                for fixture_row in all_fixtures:
                    fid = fixture_row.fixture_id
                    if fid == fixture_id:
                        continue

                    # Check if this fixture has non-terminal matches
                    non_terminal_matches = session.query(MatchModel).filter(
                        MatchModel.fixture_id == fid,
                        MatchModel.status.notin_(terminal_states),
                        MatchModel.active_status == True
                    ).all()

                    if non_terminal_matches:
                        non_terminal_count += 1

                return non_terminal_count == 0

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to check if only non-terminal fixture: {e}")
            return False

    def _start_async_zip_validation(self, fixture_id: str):
        """Start asynchronous ZIP validation for a fixture without blocking"""
        try:
            logger.info(f"Starting asynchronous ZIP validation for fixture {fixture_id}")

            # Start validation in a background thread
            validation_thread = threading.Thread(
                target=self._validate_fixture_zips_async,
                args=(fixture_id,),
                daemon=True
            )
            validation_thread.start()

        except Exception as e:
            logger.error(f"Failed to start async ZIP validation for fixture {fixture_id}: {e}")

    def _validate_all_pending_zips_at_launch(self):
        """Validate all ZIP files that are not yet validated at application launch"""
        try:
            logger.info("Starting validation of all unvalidated ZIP files at launch")

            session = self.db_manager.get_session()
            try:
                # Get all active match templates with ZIP files that are not validated
                pending_templates = session.query(MatchTemplateModel).filter(
                    MatchTemplateModel.active_status == True,
                    MatchTemplateModel.zip_filename.isnot(None),
                    MatchTemplateModel.zip_validation_status.in_(['pending', None])
                ).all()

                if not pending_templates:
                    logger.info("No unvalidated ZIP files found at launch")
                    return

                logger.info(f"Found {len(pending_templates)} unvalidated ZIP files to validate at launch")

                # Validate each pending ZIP file
                for template in pending_templates:
                    # Start validation for this template
                    self._validate_single_zip_async(template.id, session, MatchTemplateModel)

            finally:
                session.close()

            logger.info("Launch-time ZIP validation initiated for all pending files")

        except Exception as e:
            logger.error(f"Failed to start launch-time ZIP validation: {e}")

    def _validate_fixture_zips_async(self, fixture_id: str):
        """Validate ZIP files for a fixture asynchronously"""
        try:
            logger.info(f"Async ZIP validation started for fixture {fixture_id}")

            session = self.db_manager.get_session()
            try:
                # Get all active matches for this fixture that have ZIP files
                matches_with_zips = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.active_status == True,
                    MatchModel.zip_filename.isnot(None)
                ).all()

                if not matches_with_zips:
                    logger.debug(f"Fixture {fixture_id} has no matches requiring ZIP files")
                    return

                logger.info(f"Validating {len(matches_with_zips)} ZIP files for fixture {fixture_id}")

                # Reset any stale 'validating' statuses (older than 5 minutes)
                stale_threshold = datetime.utcnow() - timedelta(minutes=5)
                stale_count = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.zip_validation_status == 'validating',
                    MatchModel.updated_at < stale_threshold
                ).update({'zip_validation_status': 'pending'})
                if stale_count > 0:
                    logger.info(f"Reset {stale_count} stale 'validating' statuses to 'pending'")
                    session.commit()

                for match in matches_with_zips:
                    # Check if already validated
                    if match.zip_validation_status == 'valid':
                        logger.debug(f"Match {match.match_number} ZIP already validated, skipping")
                        continue
                    elif match.zip_validation_status == 'validating':
                        logger.debug(f"Match {match.match_number} ZIP validation in progress, skipping")
                        continue

                    # Start validation for this match
                    self._validate_single_zip_async(match.id, session, MatchModel)

            finally:
                session.close()

            logger.info(f"Async ZIP validation completed for fixture {fixture_id}")

            # Check if we were waiting for this fixture
            if self.waiting_for_validation_fixture == fixture_id:
                # Check if all are now validated
                if self._are_all_zips_validated_for_fixture(fixture_id):
                    logger.info(f"All ZIPs now validated for waiting fixture {fixture_id} - activating")
                    # Create a dummy message for activation
                    dummy_message = Message(
                        type=MessageType.START_GAME,
                        sender=self.name,
                        recipient=self.name,
                        data={"fixture_id": fixture_id, "timestamp": time.time()},
                        correlation_id=None
                    )
                    self._activate_fixture(fixture_id, dummy_message)
                    self.waiting_for_validation_fixture = None
                else:
                    logger.warning(f"ZIP validation completed for fixture {fixture_id} but not all ZIPs are valid - not activating")

        except Exception as e:
            logger.error(f"Async ZIP validation failed for fixture {fixture_id}: {e}")

    def _validate_single_zip_async(self, match_id: int, session, model_class=MatchModel):
        """Validate a single ZIP file asynchronously"""
        try:
            match = session.query(model_class).filter(model_class.id == match_id).first()
            if not match:
                logger.warning(f"Match {match_id} not found for ZIP validation")
                return

            # Update status to validating
            match.zip_validation_status = 'validating'
            session.commit()

            # Start validation in separate thread
            validation_thread = threading.Thread(
                target=self._perform_zip_validation,
                args=(match_id, model_class),
                daemon=True
            )
            validation_thread.start()

        except Exception as e:
            logger.error(f"Failed to start ZIP validation for match {match_id}: {e}")

    def _perform_zip_validation(self, match_id: int, model_class=MatchModel):
        """Perform actual ZIP validation"""
        try:
            session = self.db_manager.get_session()
            try:
                match = session.query(model_class).filter(model_class.id == match_id).first()
                if not match:
                    logger.warning(f"Match {match_id} not found during ZIP validation")
                    return

                zip_filename = match.zip_filename
                if not zip_filename:
                    logger.warning(f"Match {match_id} has no ZIP filename")
                    return

                from ..config.settings import get_user_data_dir
                from pathlib import Path
                import zipfile

                user_data_dir = get_user_data_dir()
                zip_path = user_data_dir / "zip_files" / zip_filename

                logger.info(f"Validating ZIP file: {zip_path}")

                # Check if file exists
                if not zip_path.exists():
                    logger.error(f"ZIP file missing: {zip_path}")
                    match.zip_validation_status = 'invalid'
                    session.commit()
                    return

                # Check file size
                if zip_path.stat().st_size == 0:
                    logger.error(f"ZIP file empty: {zip_path}")
                    match.zip_validation_status = 'invalid'
                    session.commit()
                    return

                # Try to open and validate ZIP structure
                try:
                    with zipfile.ZipFile(str(zip_path), 'r') as zip_ref:
                        # Check for required video files (WIN1.mp4, WIN2.mp4, etc.)
                        file_list = zip_ref.namelist()
                        required_videos = ['WIN1.mp4', 'WIN2.mp4', 'DRAW.mp4']  # Basic requirements
                        found_videos = [f for f in file_list if f.endswith('.mp4')]

                        if not found_videos:
                            logger.error(f"ZIP file contains no MP4 files: {zip_path}")
                            match.zip_validation_status = 'invalid'
                            session.commit()
                            return

                        logger.info(f"ZIP file valid - contains {len(found_videos)} video files: {zip_path}")

                except zipfile.BadZipFile as e:
                    logger.error(f"Invalid ZIP file: {zip_path} - {e}")
                    match.zip_validation_status = 'invalid'
                    session.commit()
                    return
                except Exception as e:
                    logger.error(f"Error validating ZIP file: {zip_path} - {e}")
                    match.zip_validation_status = 'invalid'
                    session.commit()
                    return

                # Validation successful
                match.zip_validation_status = 'valid'
                session.commit()
                logger.info(f"ZIP validation successful for match {match_id}: {zip_filename}")

            finally:
                session.close()

        except Exception as e:
            logger.error(f"ZIP validation failed for match {match_id}: {e}")
            try:
                session = self.db_manager.get_session()
                match = session.query(model_class).filter(model_class.id == match_id).first()
                if match:
                    match.zip_validation_status = 'invalid'
                    session.commit()

                    # If this is a MatchTemplateModel and validation failed, trigger re-download
                    if model_class == MatchTemplateModel and hasattr(match, 'zip_filename') and match.zip_filename:
                        logger.info(f"Detailed validation failed for {match.zip_filename}, triggering re-download")
                        # Reset status to allow re-download on next sync
                        match.zip_validation_status = 'pending'
                        session.commit()

                        # Remove the corrupted file to force re-download
                        try:
                            from ..config.settings import get_user_data_dir
                            user_data_dir = get_user_data_dir()
                            zip_path = user_data_dir / "zip_files" / match.zip_filename
                            if zip_path.exists():
                                zip_path.unlink()
                                logger.debug(f"Removed corrupted ZIP file {match.zip_filename} to allow re-download")
                        except Exception as cleanup_e:
                            logger.warning(f"Failed to remove corrupted ZIP file {match.zip_filename}: {cleanup_e}")

                session.close()
            except Exception as update_e:
                logger.error(f"Failed to update validation status after error: {update_e}")

    def _find_active_today_fixture(self) -> Optional[str]:
        """Find an active fixture with today's date"""
        try:
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone (for day change detection)
                today = self._get_today_venue_date()

                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)

                # Find fixtures with today's matches that are not in terminal states
                terminal_states = ['done', 'cancelled', 'failed', 'paused']

                active_matches = session.query(MatchModel).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= utc_start,
                    MatchModel.start_time < utc_end,
                    MatchModel.status.notin_(terminal_states),
                    MatchModel.active_status == True
                ).order_by(MatchModel.start_time.asc()).all()

                if active_matches:
                    return active_matches[0].fixture_id

                return None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to find active today fixture: {e}")
            return None

    def _initialize_new_fixture(self) -> Optional[str]:
        """Initialize a new fixture by creating one from templates or old matches"""
        try:
            session = self.db_manager.get_session()
            try:
                # First try: Create new fixture from match templates
                logger.info("Creating new fixture from match templates")
                template_matches = self._select_random_match_templates(5, session)
                if template_matches:
                    fixture_id = self._create_new_fixture_from_templates(template_matches, session)
                    if fixture_id:
                        logger.info(f"Created new fixture {fixture_id} from match templates")
                        return fixture_id
                    else:
                        logger.warning("Failed to create new fixture from templates")
                        return None

                # Second try: Create new fixture from old completed matches
                logger.info("No match templates available, trying to create fixture from old completed matches")
                old_matches = self._select_random_completed_matches(5, session)
                if old_matches:
                    fixture_id = self._create_new_fixture_from_old_matches(old_matches, session)
                    if fixture_id:
                        logger.info(f"Created new fixture {fixture_id} from old completed matches")
                        return fixture_id
                    else:
                        logger.warning("Failed to create new fixture from old matches")
                        return None

                # No matches available at all
                logger.warning("No match templates or old completed matches found - cannot create new fixture")
                return None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to initialize new fixture: {e}")
            return None

    def _activate_fixture(self, fixture_id: str, message: Message):
        """Activate a fixture and start the game"""
        try:
            logger.info(f"🎯 ACTIVATING FIXTURE: {fixture_id}")

            # Check if fixture is already active to prevent double activation
            if self.current_fixture_id == fixture_id and self.game_active:
                logger.warning(f"Fixture {fixture_id} is already active - ignoring duplicate activation")
                self._send_response(message, "already_active", f"Fixture {fixture_id} already active")
                return

            # Set current fixture
            self.current_fixture_id = fixture_id
            self.game_active = True

            # Step 1 & 2: Change match statuses in a single transaction
            logger.info(f"🔄 Starting match status changes for fixture {fixture_id}")
            self._schedule_and_apply_betting_logic(fixture_id)

            # Send game started confirmation
            logger.info(f"✅ Fixture {fixture_id} activated successfully")
            self._send_response(message, "started", f"Fixture {fixture_id} activated")

            # Start match timer
            logger.info(f"⏰ Starting match timer for fixture {fixture_id}")
            self._start_match_timer(fixture_id)

            # Only start ZIP validation if not all ZIPs are already validated
            if not self._are_all_zips_validated_for_fixture(fixture_id):
                logger.info(f"📦 Starting ZIP validation for fixture {fixture_id}")
                self._start_async_zip_validation(fixture_id)
            else:
                logger.info(f"📦 All ZIPs already validated for fixture {fixture_id} - skipping validation")

            # Dispatch START_INTRO message
            logger.info(f"🎬 Dispatching START_INTRO message for fixture {fixture_id}")
            self._dispatch_start_intro(fixture_id)

            # Broadcast GAME_STARTED message to notify all components that game has started with this fixture
            game_started_message = MessageBuilder.game_started(
                sender=self.name,
                fixture_id=fixture_id
            )
            self.message_bus.publish(game_started_message, broadcast=True)
            logger.info(f"🎯 Broadcast GAME_STARTED message for fixture {fixture_id}")

            # Refresh dashboard statuses
            self._refresh_dashboard_statuses()

        except Exception as e:
            logger.error(f"❌ Failed to activate fixture {fixture_id}: {e}")
            import traceback
            logger.error(f"Stack trace: {traceback.format_exc()}")
            self._send_response(message, "error", f"Failed to activate fixture: {str(e)}")

    def _schedule_and_apply_betting_logic(self, fixture_id: str):
        """Change match statuses in a single transaction: first to 'scheduled', then apply betting logic"""
        try:
            logger.info(f"🔄 Starting match status update for fixture {fixture_id}")
            
            # Get betting mode configuration from database (default to 'all_bets_on_start')
            betting_mode = self._get_betting_mode_config()
            logger.info(f"📋 Using betting mode: {betting_mode}")
            
            session = self.db_manager.get_session()
            try:
                # First, let's see what matches exist for this fixture
                all_matches = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.active_status == True
                ).all()
                
                if not all_matches:
                    logger.warning(f"⚠️ No matches found for fixture {fixture_id}")
                    return
                
                logger.info(f"📊 Found {len(all_matches)} total matches in fixture {fixture_id}")
                for match in all_matches:
                    logger.info(f"  Match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township} - Status: {match.status}")

                # Step 1: Change ALL matches in the fixture to 'scheduled' status first
                terminal_states = ['done', 'cancelled', 'failed', 'paused']
                matches = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.status.notin_(terminal_states),
                    MatchModel.active_status == True
                ).all()

                logger.info(f"📋 Found {len(matches)} non-terminal matches to process")

                scheduled_count = 0
                for match in matches:
                    if match.status != 'scheduled':
                        logger.info(f"🔄 Changing match {match.match_number} status from '{match.status}' to 'scheduled'")
                        match.status = 'scheduled'
                        scheduled_count += 1
                    else:
                        logger.info(f"✅ Match {match.match_number} already scheduled")

                # Flush to make sure scheduled status is available for next step
                if scheduled_count > 0:
                    session.flush()
                    logger.info(f"✅ Scheduled {scheduled_count} matches in fixture {fixture_id}")
                else:
                    logger.info("📋 No matches needed scheduling")

                # Step 2: Apply betting logic based on configuration
                if betting_mode == 'all_bets_on_start':
                    logger.info("🎰 Applying 'all_bets_on_start' logic")
                    # Change ALL scheduled matches to 'bet' status
                    scheduled_matches = session.query(MatchModel).filter(
                        MatchModel.fixture_id == fixture_id,
                        MatchModel.status == 'scheduled',
                        MatchModel.active_status == True
                    ).all()

                    logger.info(f"📋 Found {len(scheduled_matches)} scheduled matches to change to 'bet'")

                    bet_count = 0
                    for match in scheduled_matches:
                        logger.info(f"🎰 Changing match {match.match_number} status to 'bet' (all bets on start)")
                        match.status = 'bet'
                        bet_count += 1

                    if bet_count > 0:
                        logger.info(f"✅ Changed {bet_count} matches to 'bet' status (all bets on start mode)")
                    else:
                        logger.warning("⚠️ No scheduled matches found to change to 'bet' status")

                else:  # 'one_bet_at_a_time'
                    logger.info("🎯 Applying 'one_bet_at_a_time' logic")
                    # Change only the FIRST scheduled match to 'bet' status
                    first_match = session.query(MatchModel).filter(
                        MatchModel.fixture_id == fixture_id,
                        MatchModel.status == 'scheduled',
                        MatchModel.active_status == True
                    ).order_by(MatchModel.match_number.asc()).first()

                    if first_match:
                        logger.info(f"🎯 Changing first match {first_match.match_number} status to 'bet' (one bet at a time)")
                        first_match.status = 'bet'
                    else:
                        logger.warning("⚠️ No scheduled match found to change to 'bet' status")

                # Commit all changes in a single transaction
                logger.info("💾 Committing database changes...")
                session.commit()
                logger.info(f"✅ Successfully updated match statuses for fixture {fixture_id} with betting mode: {betting_mode}")

                # Send notification to web dashboard about fixture status update
                try:
                    from .message_bus import Message, MessageType
                    status_update_message = Message(
                        type=MessageType.CUSTOM,
                        sender=self.name,
                        recipient="web_dashboard",
                        data={
                            "fixture_status_update": {
                                "fixture_id": fixture_id,
                                "betting_mode": betting_mode,
                                "timestamp": time.time()
                            }
                        }
                    )
                    self.message_bus.publish(status_update_message)
                    logger.info(f"📢 Broadcast fixture status update notification for {fixture_id}")
                except Exception as msg_e:
                    logger.warning(f"Failed to send fixture status update notification: {msg_e}")

                # Verify the changes were applied
                final_matches = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.active_status == True
                ).all()
                
                logger.info(f"🔍 Final match statuses for fixture {fixture_id}:")
                for match in final_matches:
                    logger.info(f"  Match {match.match_number}: {match.status}")

            finally:
                session.close()

        except Exception as e:
            logger.error(f"❌ Failed to schedule and apply betting logic: {e}")
            import traceback
            logger.error(f"Stack trace: {traceback.format_exc()}")
            # Try to rollback in case of error
            try:
                if 'session' in locals():
                    session.rollback()
            except Exception as rollback_e:
                logger.error(f"Failed to rollback: {rollback_e}")

    def _get_betting_mode_config(self) -> str:
        """Get global betting mode configuration from game config (default: 'all_bets_on_start')"""
        try:
            session = self.db_manager.get_session()
            try:
                from ..database.models import GameConfigModel
                
                # Get global betting mode configuration from game_config table
                betting_mode_config = session.query(GameConfigModel).filter_by(
                    config_key='betting_mode'
                ).first()
                
                if betting_mode_config:
                    return betting_mode_config.get_typed_value()
                else:
                    # Default to 'all_bets_on_start' if no configuration found
                    logger.debug("No betting mode configuration found, using default: 'all_bets_on_start'")
                    return 'all_bets_on_start'

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to get betting mode config: {e}")
            # Default fallback
            return 'all_bets_on_start'

    def _refresh_dashboard_statuses(self):
        """Refresh dashboard statuses by sending update messages"""
        try:
            # Send refresh message to web dashboard
            refresh_message = Message(
                type=MessageType.GAME_STATUS,
                sender=self.name,
                data={
                    "action": "refresh",
                    "timestamp": time.time()
                }
            )
            self.message_bus.publish(refresh_message, broadcast=True)

        except Exception as e:
            logger.error(f"Failed to refresh dashboard statuses: {e}")

    def _start_match_timer(self, fixture_id: str):
        """Start the match timer for the fixture"""
        try:
            # Send message to match timer component
            timer_message = Message(
                type=MessageType.START_GAME,
                sender=self.name,
                recipient="match_timer",
                data={
                    "fixture_id": fixture_id,
                    "action": "start_timer",
                    "timestamp": time.time()
                }
            )
            self.message_bus.publish(timer_message)

            logger.info(f"Started match timer for fixture {fixture_id}")

        except Exception as e:
            logger.error(f"Failed to start match timer: {e}")

    def _dispatch_start_intro(self, fixture_id: str):
        """Dispatch START_INTRO message to trigger intro content
        
        IMPORTANT: When yesterday's fixture has incomplete matches:
        - Do NOT create any new matches in yesterday's fixture
        - Ensure today's fixture exists with matches ready
        - When yesterday's matches complete, seamlessly transition to today's fixture
        """
        try:
            from .message_bus import MessageBuilder

            # Check if the fixture is from yesterday - if so, don't add matches to it
            session = self.db_manager.get_session()
            try:
                is_yesterday_fixture = self._is_fixture_from_yesterday(fixture_id, session)
            finally:
                session.close()

            if is_yesterday_fixture:
                logger.info(f"🎬 Fixture {fixture_id} is from yesterday - will play remaining matches without adding new ones")
                remaining_matches = self._count_remaining_matches_in_fixture(fixture_id)
                logger.info(f"🎬 Yesterday fixture {fixture_id} has {remaining_matches} remaining matches")
                
                # IMPORTANT: Do NOT add matches to yesterday's fixture
                # Instead, ensure today's fixture exists and has matches ready for seamless transition
                logger.info(f"🎬 Ensuring today's fixture exists with matches ready for after yesterday's matches complete")
                today_fixture_id = self._get_or_create_today_fixture()
                if today_fixture_id:
                    # Check if today's fixture has enough matches
                    today_remaining = self._count_remaining_matches_in_fixture(today_fixture_id)
                    logger.info(f"🎬 Today fixture {today_fixture_id} has {today_remaining} matches")
                    
                    if today_remaining < 5:
                        logger.info(f"🚨 Today fixture needs {5 - today_remaining} more matches - creating them now")
                        self._ensure_minimum_matches_in_fixture(today_fixture_id, 5 - today_remaining)
                        today_remaining = self._count_remaining_matches_in_fixture(today_fixture_id)
                        logger.info(f"✅ Today fixture {today_fixture_id} now has {today_remaining} matches ready")
                    
                    # Store the today fixture ID for seamless transition
                    self.pending_today_fixture_id = today_fixture_id
                    logger.info(f"🎬 Today fixture {today_fixture_id} ready for seamless transition after yesterday's matches")
                else:
                    logger.warning("⚠️ Could not get or create today's fixture - will continue with yesterday's matches only")
            else:
                # Check if there are at least 5 matches remaining in the fixture
                logger.info(f"🎬 Checking minimum match count for fixture {fixture_id} before playing INTRO.mp4")
                remaining_matches = self._count_remaining_matches_in_fixture(fixture_id)
                logger.info(f"🎬 Fixture {fixture_id} has {remaining_matches} remaining matches")

                if remaining_matches < 5:
                    logger.info(f"🚨 Only {remaining_matches} matches remaining (minimum 5 required) - creating new matches")
                    self._ensure_minimum_matches_in_fixture(fixture_id, 5 - remaining_matches)
                    # Recount after adding matches
                    remaining_matches = self._count_remaining_matches_in_fixture(fixture_id)
                    logger.info(f"✅ After adding matches, fixture {fixture_id} now has {remaining_matches} remaining matches")

            # Find the first match that was set to 'bet' status
            first_bet_match_id = self._get_first_bet_match_id(fixture_id)

            # Unzip the ZIP file of the first match if it exists
            self._unzip_match_zip_file(first_bet_match_id)

            # Create and send START_INTRO message
            start_intro_message = MessageBuilder.start_intro(
                sender=self.name,
                fixture_id=fixture_id,
                match_id=first_bet_match_id
            )
            self.message_bus.publish(start_intro_message, broadcast=True)

            logger.info(f"🎬 START_INTRO message dispatched for fixture {fixture_id}, match {first_bet_match_id}")

        except Exception as e:
            logger.error(f"❌ Failed to dispatch START_INTRO message: {e}")

    def _get_first_bet_match_id(self, fixture_id: str) -> Optional[int]:
        """Get the ID of the first match set to 'bet' status in the fixture"""
        try:
            session = self.db_manager.get_session()
            try:
                from ..database.models import MatchModel

                # Find the first match with 'bet' status in this fixture
                first_bet_match = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.status == 'bet',
                    MatchModel.active_status == True
                ).order_by(MatchModel.match_number.asc()).first()

                if first_bet_match:
                    return first_bet_match.id
                else:
                    logger.warning(f"No match with 'bet' status found in fixture {fixture_id}")
                    return None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to get first bet match ID for fixture {fixture_id}: {e}")
            return None

    def _unzip_match_zip_file(self, match_id: int):
        """Unzip the ZIP file associated with a match to a temporary directory"""
        try:
            import zipfile
            import tempfile
            import os
            from pathlib import Path

            logger.info(f"DEBUG: Starting ZIP extraction for match {match_id}")

            # CLEANUP: Delete all previous unzipped match directories before proceeding
            self._cleanup_previous_match_extractions()

            session = self.db_manager.get_session()
            try:
                # Get the match from database
                match = session.query(MatchModel).filter_by(id=match_id).first()

                if not match:
                    logger.warning(f"DEBUG: Match {match_id} not found in database, skipping ZIP extraction")
                    return

                logger.info(f"DEBUG: Found match {match_id}, zip_filename: {match.zip_filename}")

                if not match.zip_filename:
                    logger.info(f"DEBUG: Match {match_id} has no associated ZIP file, skipping extraction")
                    return

                # Determine ZIP file location (ZIP files are stored in the zip_files directory)
                from ..config.settings import get_user_data_dir
                user_data_dir = get_user_data_dir()
                zip_file_path = user_data_dir / "zip_files" / match.zip_filename

                logger.info(f"DEBUG: Looking for ZIP file at: {zip_file_path}")
                logger.info(f"DEBUG: ZIP file exists: {zip_file_path.exists()}")

                if not zip_file_path.exists():
                    logger.warning(f"DEBUG: ZIP file not found: {zip_file_path}")
                    return

                logger.info(f"DEBUG: ZIP file size: {zip_file_path.stat().st_size} bytes")

                # Create temporary directory for extraction
                temp_dir = Path(tempfile.mkdtemp(prefix=f"match_{match_id}_"))
                logger.info(f"DEBUG: Created temp directory: {temp_dir}")

                # Extract the ZIP file
                logger.info(f"DEBUG: Starting ZIP extraction...")
                try:
                    with zipfile.ZipFile(str(zip_file_path), 'r') as zip_ref:
                        file_list = zip_ref.namelist()
                        logger.info(f"DEBUG: ZIP contains {len(file_list)} files: {file_list}")
                        zip_ref.extractall(str(temp_dir))
                except zipfile.BadZipFile as e:
                    logger.error(f"DEBUG: Invalid or corrupted ZIP file for match {match_id}: {e}")
                    # When ZIP extraction fails, act as if PLAY_VIDEO_RESULTS_DONE has been received
                    # Send PLAY_VIDEO_RESULTS with a fallback result
                    logger.info(f"DEBUG: ZIP extraction failed, sending PLAY_VIDEO_RESULTS_DONE simulation for match {match_id}")
                    self._handle_zip_extraction_failure(match_id, match.fixture_id)
                    return
                except Exception as e:
                    logger.error(f"DEBUG: Error during ZIP extraction for match {match_id}: {e}")
                    # When ZIP extraction fails, act as if PLAY_VIDEO_RESULTS_DONE has been received
                    logger.info(f"DEBUG: ZIP extraction failed, sending PLAY_VIDEO_RESULTS_DONE simulation for match {match_id}")
                    self._handle_zip_extraction_failure(match_id, match.fixture_id)
                    return

                # Log extraction results
                extracted_files = list(temp_dir.rglob("*"))
                logger.info(f"DEBUG: Successfully extracted {len(extracted_files)} files from {match.zip_filename}")
                for extracted_file in extracted_files:
                    if extracted_file.is_file():
                        logger.info(f"DEBUG: Extracted file: {extracted_file} (size: {extracted_file.stat().st_size} bytes)")

                # Store the temporary directory path for potential cleanup
                # In a real implementation, you might want to track this for cleanup
                match.temp_extract_path = str(temp_dir)

                # Update match in database with temp path (optional)
                session.commit()

                logger.info(f"DEBUG: ZIP extraction completed for match {match_id}")

            finally:
                session.close()

        except Exception as e:
            logger.error(f"DEBUG: Failed to unzip ZIP file for match {match_id}: {e}")
            import traceback
            logger.error(f"DEBUG: Full traceback: {traceback.format_exc()}")


    def _calculate_payout(self, match_id: int, outcome: str, coefficient: float, session) -> float:
        """Calculate payout for an outcome"""
        try:
            logger.info(f"💰 [DEBUG] Calculating payout for {outcome} (match_id={match_id}, coefficient={coefficient})")

            # Get total bets for this outcome on this match
            logger.info(f"📊 [DEBUG] Querying bets for outcome '{outcome}' on match {match_id}")
            total_bet_amount = session.query(
                BetDetailModel.amount
            ).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.outcome == outcome,
                BetDetailModel.result == 'pending'
            ).all()

            bet_count = len(total_bet_amount) if total_bet_amount else 0
            logger.info(f"📊 [DEBUG] Found {bet_count} pending bets for {outcome}")

            total_amount = sum(bet.amount for bet in total_bet_amount) if total_bet_amount else 0.0
            logger.info(f"💵 [DEBUG] Total bet amount for {outcome}: {total_amount:.2f}")

            payout = total_amount * coefficient
            logger.info(f"💰 [DEBUG] Calculated payout for {outcome}: {total_amount:.2f} × {coefficient} = {payout:.2f}")

            return payout

        except Exception as e:
            logger.error(f"❌ [DEBUG] Failed to calculate payout for {outcome}: {e}")
            return 0.0

    def _calculate_total_payin(self, match_id: int, session) -> float:
        """Calculate total payin (sum of all UNDER + OVER bets)"""
        try:
            logger.info(f"💵 [DEBUG] Calculating total payin for match {match_id}")

            # Query UNDER bets
            logger.info(f"📊 [DEBUG] Querying UNDER bets for match {match_id}")
            total_under = session.query(
                BetDetailModel.amount
            ).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.outcome == 'UNDER',
                BetDetailModel.result == 'pending',
                BetDetailModel.result != 'cancelled'
            ).all()

            under_count = len(total_under) if total_under else 0
            under_amount = sum(bet.amount for bet in total_under) if total_under else 0.0
            logger.info(f"💵 [DEBUG] UNDER bets: {under_count} bets, total amount: {under_amount:.2f}")

            # Query OVER bets
            logger.info(f"📊 [DEBUG] Querying OVER bets for match {match_id}")
            total_over = session.query(
                BetDetailModel.amount
            ).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.outcome == 'OVER',
                BetDetailModel.result == 'pending',
                BetDetailModel.result != 'cancelled'
            ).all()

            over_count = len(total_over) if total_over else 0
            over_amount = sum(bet.amount for bet in total_over) if total_over else 0.0
            logger.info(f"💵 [DEBUG] OVER bets: {over_count} bets, total amount: {over_amount:.2f}")

            total_payin = under_amount + over_amount
            logger.info(f"💵 [DEBUG] Total payin calculated: {under_amount:.2f} + {over_amount:.2f} = {total_payin:.2f}")

            return total_payin

        except Exception as e:
            logger.error(f"❌ [DEBUG] Failed to calculate total payin: {e}")
            return 0.0

    def _get_fixture_coefficients(self, fixture_id: str, session) -> tuple:
        """Get UNDER/OVER coefficients from fixture/match outcomes
        
        DEPRECATED: This method gets coefficients from the first match in the fixture,
        which may not be the match being processed. Use _get_match_coefficients() instead.
        """
        try:
            # Get one active match from the fixture to get UNDER/OVER coefficients
            match = session.query(MatchModel).filter(
                MatchModel.fixture_id == fixture_id,
                MatchModel.active_status == True
            ).first()

            if not match:
                logger.warning(f"No active matches found for fixture {fixture_id}")
                return None, None

            # Get UNDER/OVER coefficients from match outcomes
            under_coeff = None
            over_coeff = None

            for outcome in match.outcomes:
                if outcome.column_name == 'UNDER':
                    under_coeff = outcome.float_value
                elif outcome.column_name == 'OVER':
                    over_coeff = outcome.float_value

            return under_coeff, over_coeff

        except Exception as e:
            logger.error(f"Failed to get fixture coefficients: {e}")
            return None, None

    def _get_match_coefficients(self, match_id: int, session) -> tuple:
        """Get UNDER/OVER coefficients from a specific match's outcomes.
        
        Args:
            match_id: The specific match ID to get coefficients from
            session: Database session
            
        Returns:
            Tuple of (under_coefficient, over_coefficient)
        """
        try:
            # Get UNDER/OVER coefficients directly from match outcomes table
            under_coeff = None
            over_coeff = None
            
            match_outcomes = session.query(MatchOutcomeModel).filter(
                MatchOutcomeModel.match_id == match_id,
                MatchOutcomeModel.column_name.in_(['UNDER', 'OVER'])
            ).all()
            
            for outcome in match_outcomes:
                if outcome.column_name == 'UNDER':
                    under_coeff = outcome.float_value
                elif outcome.column_name == 'OVER':
                    over_coeff = outcome.float_value
            
            logger.debug(f"Match {match_id} coefficients: UNDER={under_coeff}, OVER={over_coeff}")
            return under_coeff, over_coeff

        except Exception as e:
            logger.error(f"Failed to get match coefficients for match {match_id}: {e}")
            return None, None

    def _get_redistribution_cap(self) -> float:
        """Get redistribution CAP percentage from game configuration"""
        try:
            session = self.db_manager.get_session()
            try:
                # Get CAP from game_config table (same as web dashboard saves it)
                cap_config = session.query(GameConfigModel).filter_by(
                    config_key='extraction_redistribution_cap'
                ).first()

                if cap_config:
                    cap_value = cap_config.get_typed_value()
                    if isinstance(cap_value, (int, float)) and 10 <= cap_value <= 100:
                        logger.debug(f"Using redistribution CAP: {cap_value}%")
                        return float(cap_value)
                    else:
                        logger.warning(f"Invalid CAP value: {cap_value}, using default 70%")
                        return 70.0
                else:
                    logger.debug("No redistribution CAP configuration found, using default 70%")
                    return 70.0

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to get redistribution CAP: {e}")
            return 70.0

    def _get_global_redistribution_adjustment(self, session) -> float:
        """Get accumulated global redistribution adjustment"""
        try:
            # Get the global redistribution adjustment record (fixed date 1970-01-01)
            global_date = date(1970, 1, 1)
            global_record = session.query(PersistentRedistributionAdjustmentModel)\
                .filter_by(date=global_date)\
                .first()

            if global_record:
                logger.debug(f"Found global redistribution adjustment: {global_record.accumulated_shortfall}")
                return global_record.accumulated_shortfall
            else:
                logger.debug("No global redistribution adjustment record found, returning 0.0")
                return 0.0

        except Exception as e:
            logger.error(f"Failed to get global redistribution adjustment: {e}")
            return 0.0

    def _update_global_redistribution_adjustment(self, adjustment, payin_amount, redistributed_amount, cap_percentage, session):
        """Update persistent global redistribution adjustment tracking after extraction"""
        try:
            # Calculate the redistribution adjustment for this extraction
            # Positive: under-redistribution (shortfall), Negative: over-redistribution (surplus)
            expected_redistribution = payin_amount * (cap_percentage / 100.0)

            logger.info(f"💰 [ADJUSTMENT DEBUG] Payin: {payin_amount:.2f}, Expected: {expected_redistribution:.2f}, Redistributed: {redistributed_amount:.2f}, Adjustment: {adjustment:.2f}")

            # Use a fixed date for the global persistent record
            global_date = date(1970, 1, 1)  # Fixed date for global record
    
            # Get or create the global record
            adjustment_record = session.query(PersistentRedistributionAdjustmentModel).filter_by(
                date=global_date
            ).first()

            if not adjustment_record:
                adjustment_record = PersistentRedistributionAdjustmentModel(
                    date=global_date,
                    accumulated_shortfall=adjustment,
                    total_payin=payin_amount,
                    total_redistributed=redistributed_amount,
                    cap_percentage=cap_percentage
                )
                session.add(adjustment_record)
                logger.info(f"Created global redistribution adjustment record with adjustment {adjustment:.2f}")
            else:
                adjustment_record.accumulated_shortfall += adjustment
                adjustment_record.total_payin += payin_amount
                adjustment_record.total_redistributed += redistributed_amount
                adjustment_record.cap_percentage = cap_percentage  # Update to latest
                logger.info(f"Updated global redistribution adjustment record, new accumulated adjustment: {adjustment_record.accumulated_shortfall:.2f}")

            session.commit()

        except Exception as e:
            logger.error(f"Failed to update persistent redistribution adjustment: {e}")
            session.rollback()

    def _weighted_random_selection(self, under_coeff: float, over_coeff: float) -> str:
        """Weighted random selection based on inverse coefficients"""
        try:
            import random

            logger.info(f"🎲 [DEBUG] Weighted random selection - UNDER coeff: {under_coeff}, OVER coeff: {over_coeff}")

            # Higher coefficients get lower probability (inverse weighting)
            logger.info(f"⚖️ [DEBUG] Calculating inverse weights (higher coeff = lower probability)")
            under_weight = 1.0 / under_coeff if under_coeff > 0 else 1.0
            over_weight = 1.0 / over_coeff if over_coeff > 0 else 1.0
            logger.info(f"⚖️ [DEBUG] Weights calculated - UNDER: {under_weight:.4f}, OVER: {over_weight:.4f}")

            total_weight = under_weight + over_weight
            logger.info(f"📊 [DEBUG] Total weight: {total_weight:.4f}")

            if total_weight == 0:
                # Fallback to equal weights
                logger.warning(f"⚠️ [DEBUG] Total weight is zero, falling back to equal weights")
                under_weight = over_weight = 1.0
                total_weight = 2.0
                logger.info(f"⚖️ [DEBUG] Equal weights applied - UNDER: {under_weight}, OVER: {over_weight}")

            # Generate random number
            rand = random.uniform(0, total_weight)
            logger.info(f"🎯 [DEBUG] Generated random number: {rand:.4f} (range: 0-{total_weight:.4f})")

            # Determine result
            if rand < under_weight:
                logger.info(f"🎯 [DEBUG] Random {rand:.4f} < UNDER weight {under_weight:.4f} → selecting UNDER")
                return 'UNDER'
            else:
                logger.info(f"🎯 [DEBUG] Random {rand:.4f} >= UNDER weight {under_weight:.4f} → selecting OVER")
                return 'OVER'

        except Exception as e:
            logger.error(f"❌ [DEBUG] Failed to perform weighted random selection: {e}")
            # Fallback to 50/50
            logger.info(f"🔄 [DEBUG] Using 50/50 fallback")
            import random
            fallback_result = 'UNDER' if random.random() < 0.5 else 'OVER'
            logger.info(f"🎲 [DEBUG] 50/50 fallback result: {fallback_result}")
            return fallback_result

    def _send_play_video_match(self, fixture_id: str, match_id: int, result: str):
        """Send PLAY_VIDEO_MATCH message with calculated result"""
        try:
            # Get video filename based on result
            video_filename = self._get_match_video_filename(match_id, result)

            # Unzip the match ZIP file before sending PLAY_VIDEO_MATCH
            logger.info(f"Unzipping ZIP file for match {match_id} before sending PLAY_VIDEO_MATCH")
            self._unzip_match_zip_file(match_id)

            # Send PLAY_VIDEO_MATCH message with result
            play_message = MessageBuilder.play_video_match(
                sender=self.name,
                fixture_id=fixture_id,
                match_id=match_id,
                video_filename=video_filename,
                result=result
            )

            self.message_bus.publish(play_message)
            logger.info(f"Sent PLAY_VIDEO_MATCH for fixture {fixture_id}, match {match_id}, result {result}, video {video_filename}")

        except Exception as e:
            logger.error(f"Failed to send PLAY_VIDEO_MATCH: {e}")

    def _get_match_video_filename(self, match_id: int, result: str) -> str:
        """Get appropriate video filename based on result"""
        try:
            # For now, use result.mp4 (UNDER.mp4 or OVER.mp4)
            # In future, this could be more sophisticated based on match data
            return f"{result}.mp4"

        except Exception as e:
            logger.error(f"Failed to get match video filename: {e}")
            return f"{result}.mp4"  # Fallback

    def _handle_play_video_match_done(self, message: Message):
        """Handle PLAY_VIDEO_MATCH_DONE message and query database for result"""
        try:
            fixture_id = message.data.get("fixture_id")
            match_id = message.data.get("match_id")

            logger.info(f"Processing PLAY_VIDEO_MATCH_DONE for fixture {fixture_id}, match {match_id}")

            # DEBUG: Log the full message data
            logger.info(f"DEBUG PLAY_VIDEO_MATCH_DONE: message.data = {message.data}")

            # Set match status to 'ingame'
            self._set_match_status(match_id, 'ingame')

            # Query database for the previously extracted result and winning outcomes
            extracted_result = self._query_extracted_result(match_id)

            logger.info(f"DEBUG PLAY_VIDEO_MATCH_DONE: extracted_result = '{extracted_result}'")

            if extracted_result:
                logger.info(f"Found extracted result for match {match_id}: {extracted_result}")
                # Get winning outcomes from the extraction stats
                winning_outcomes = self._query_winning_outcomes(match_id)
                logger.info(f"Found winning outcomes for match {match_id}: {winning_outcomes}")
                # Send PLAY_VIDEO_RESULTS message (note: RESULTS plural as per user request)
                self._send_play_video_results(fixture_id, match_id, extracted_result, winning_outcomes)
            else:
                logger.error(f"No extracted result found for match {match_id}")
                # Fallback to random selection
                fallback_result = self._fallback_result_selection()
                logger.info(f"Using fallback result for match {match_id}: {fallback_result}")
                self._send_play_video_results(fixture_id, match_id, fallback_result, [])

        except Exception as e:
            logger.error(f"Failed to handle PLAY_VIDEO_MATCH_DONE message: {e}")
            import traceback
            logger.error(f"DEBUG PLAY_VIDEO_MATCH_DONE: Exception traceback: {traceback.format_exc()}")

    def _handle_play_video_result_done(self, message: Message):
        """Handle PLAY_VIDEO_RESULTS_DONE message - result video finished, send MATCH_DONE and START_INTRO"""
        try:
            fixture_id = message.data.get("fixture_id")
            match_id = message.data.get("match_id")
            result = message.data.get("result")

            logger.info(f"Processing PLAY_VIDEO_RESULTS_DONE for fixture {fixture_id}, match {match_id}, result {result}")

            # DEBUG: Log the full message data
            logger.info(f"DEBUG PLAY_VIDEO_RESULTS_DONE: message.data = {message.data}")

            # Update match status to 'done' and save result
            self._set_match_status_and_result(match_id, 'done', result)

            # Send MATCH_DONE message with result
            self._send_match_done(fixture_id, match_id, result)

            # Send NEXT_MATCH message to advance to next match
            self._send_next_match(fixture_id, match_id)

        except Exception as e:
            logger.error(f"Failed to handle PLAY_VIDEO_RESULTS_DONE message: {e}")

    def _handle_match_done(self, message: Message):
        """Handle MATCH_DONE message"""
        try:
            fixture_id = message.data.get("fixture_id")
            match_id = message.data.get("match_id")
            result = message.data.get("result")

            logger.info(f"Processing MATCH_DONE for fixture {fixture_id}, match {match_id}, result {result}")

            # DEBUG: Log the message data in detail
            logger.info(f"DEBUG MATCH_DONE: message.data = {message.data}")

            # DEBUG: Check current match state before update
            session = self.db_manager.get_session()
            try:
                match = session.query(MatchModel).filter_by(id=match_id).first()
                if match:
                    logger.info(f"DEBUG MATCH_DONE: Before update - match {match_id} status='{match.status}', result='{match.result}'")
                else:
                    logger.error(f"DEBUG MATCH_DONE: Match {match_id} not found in database!")
            finally:
                session.close()

            # Update match status to 'done' and save result
            self._set_match_status_and_result(match_id, 'done', result)

            # DEBUG: Check match state after update
            session = self.db_manager.get_session()
            try:
                match = session.query(MatchModel).filter_by(id=match_id).first()
                if match:
                    logger.info(f"DEBUG MATCH_DONE: After update - match {match_id} status='{match.status}', result='{match.result}'")
                else:
                    logger.error(f"DEBUG MATCH_DONE: Match {match_id} still not found after update!")
            finally:
                session.close()

            # SAFETY NET: Ensure all bets are resolved when match ends
            # This catches any edge cases where bets might remain pending
            logger.info(f"🛡️ [SAFETY NET] Calling _ensure_all_bets_resolved for match {match_id}")
            self._ensure_all_bets_resolved(match_id, result)

            # NEXT_MATCH is now sent immediately in _handle_play_video_result_done
            # to avoid the 2-second delay and ensure proper sequencing

        except Exception as e:
            logger.error(f"Failed to handle MATCH_DONE message: {e}")

    def _handle_system_status(self, message: Message):
        """Handle SYSTEM_STATUS messages, particularly fixture update completion and status requests"""
        try:
            logger.debug(f"GamesThread handling SYSTEM_STATUS message from {message.sender}: {message.data}")
            status = message.data.get("status")
            details = message.data.get("details", {})

            if status == "fixture_update_completed":
                synchronized_matches = message.data.get("synchronized_matches", 0)
                downloaded_zips = message.data.get("downloaded_zips", 0)

                logger.info(f"Fixture update completed: {synchronized_matches} matches synchronized, {downloaded_zips} ZIPs downloaded")

                # Mark all downloaded ZIPs as validated since the update completed successfully
                if downloaded_zips > 0:
                    self._mark_all_zips_as_validated()
                    logger.info(f"Marked {downloaded_zips} ZIP files as validated after fixture update")

                    # Clear any waiting fixture since downloads are now complete
                    if self.waiting_for_validation_fixture:
                        logger.info(f"Clearing waiting fixture {self.waiting_for_validation_fixture} since fixture update completed")
                        self.waiting_for_validation_fixture = None

                # Check if we should start a game now that fixtures are available
                if synchronized_matches > 0 and not self.game_active:
                    logger.info("New fixtures available and no game is active - attempting to start game")
                    # Send START_GAME message to ourselves to trigger game start
                    start_game_message = Message(
                        type=MessageType.START_GAME,
                        sender=self.name,
                        recipient=self.name,
                        data={
                            "timestamp": time.time()
                        }
                    )
                    self.message_bus.publish(start_game_message)

            elif status == "status_request":
                # Handle status requests from Qt player
                request_type = details.get("request_type")
                logger.debug(f"Status request type: {request_type}, details: {details}")
                if request_type == "game_status":
                    logger.info(f"Received game status request from {message.sender}")

                    # Determine current game status
                    game_status = self._determine_game_status()
                    logger.debug(f"Determined game status: {game_status}")

                    # Send GAME_STATUS response back to the requester (broadcast to ensure delivery)
                    response = Message(
                        type=MessageType.GAME_STATUS,
                        sender=self.name,
                        recipient=message.sender,
                        data={
                            "status": game_status,
                            "fixture_id": self.current_fixture_id,
                            "game_active": self.game_active,
                            "timestamp": time.time()
                        },
                        correlation_id=message.correlation_id
                    )
                    logger.debug(f"About to publish GAME_STATUS response: {response.data}")
                    # Broadcast the response to ensure it reaches the Qt player
                    self.message_bus.publish(response, broadcast=True)
                    logger.info(f"Sent game status response to {message.sender}: {game_status}")
                else:
                    logger.debug(f"Ignoring status_request with unknown request_type: {request_type}")
            else:
                logger.debug(f"Ignoring SYSTEM_STATUS message with unknown status: {status}")

        except Exception as e:
            logger.error(f"Failed to handle system status message: {e}")
            import traceback
            logger.error(f"Full traceback: {traceback.format_exc()}")

    def _store_under_over_result(self, match_id: int, under_over_result: str):
        """Store the under/over result from video selection calculation"""
        try:
            session = self.db_manager.get_session()
            try:
                match = session.query(MatchModel).filter_by(id=match_id).first()
                if match:
                    match.under_over_result = under_over_result
                    session.commit()
                    logger.info(f"Stored under_over_result '{under_over_result}' for match {match_id}")
                else:
                    logger.error(f"Match {match_id} not found for storing under_over_result")
            finally:
                session.close()
        except Exception as e:
            logger.error(f"Failed to store under_over_result for match {match_id}: {e}")

    def _set_match_status(self, match_id: int, status: str):
        """Set match status in database"""
        try:
            session = self.db_manager.get_session()
            try:
                match = session.query(MatchModel).filter_by(id=match_id).first()
                if match:
                    match.status = status
                    session.commit()
                    logger.info(f"Updated match {match_id} status to {status}")
                else:
                    logger.error(f"Match {match_id} not found")
            finally:
                session.close()
        except Exception as e:
            logger.error(f"Failed to set match status: {e}")

    def _set_match_status_and_result(self, match_id: int, status: str, result: str):
        """Set match status and result in database
        
        NOTE: This function is called early with UNDER/OVER results from video playback.
        The result field should contain the actual fight winner (WIN1, DRAW, KO1, etc.),
        not UNDER/OVER which is stored in under_over_result field.
        """
        import json
        try:
            logger.info(f"DEBUG _set_match_status_and_result: Called with match_id={match_id}, status='{status}', result='{result}'")

            session = self.db_manager.get_session()
            try:
                match = session.query(MatchModel).filter_by(id=match_id).first()
                if match:
                    logger.info(f"DEBUG _set_match_status_and_result: Found match {match_id}, current status='{match.status}', current result='{match.result}'")
                    match.status = status
                    
                    # Handle UNDER/OVER results properly
                    # The match.result should contain the actual fight winner, not UNDER/OVER
                    if result in ['UNDER', 'OVER']:
                        logger.info(f"DEBUG _set_match_status_and_result: Result is UNDER/OVER, storing in under_over_result field")
                        match.under_over_result = result
                        
                        # Try to find the actual fight winner from winning_outcomes
                        actual_result = None
                        if match.winning_outcomes:
                            try:
                                winning_outcomes = json.loads(match.winning_outcomes)
                                if isinstance(winning_outcomes, list):
                                    for outcome in winning_outcomes:
                                        if outcome not in ['UNDER', 'OVER']:
                                            actual_result = outcome
                                            logger.info(f"DEBUG _set_match_status_and_result: Found fight winner '{actual_result}' from winning_outcomes")
                                            break
                            except (json.JSONDecodeError, TypeError):
                                pass
                        
                        # If no winning_outcomes, try to find from match outcomes
                        if not actual_result:
                            match_outcomes = session.query(MatchOutcomeModel).filter(
                                MatchOutcomeModel.match_id == match_id
                            ).all()
                            for outcome in match_outcomes:
                                if outcome.column_name not in ['UNDER', 'OVER']:
                                    actual_result = outcome.column_name
                                    logger.info(f"DEBUG _set_match_status_and_result: Found fight winner '{actual_result}' from match outcomes")
                                    break
                        
                        if actual_result:
                            match.result = actual_result
                            logger.info(f"DEBUG _set_match_status_and_result: Set match.result to fight winner '{actual_result}'")
                        else:
                            # Keep existing result if we can't find a fight winner
                            logger.warning(f"DEBUG _set_match_status_and_result: Could not find fight winner, keeping existing result '{match.result}'")
                    else:
                        # Result is not UNDER/OVER, set it directly
                        match.result = result
                        logger.info(f"DEBUG _set_match_status_and_result: Set match.result to '{result}'")
                    
                    # Set end_time when match is completed
                    if status == 'done':
                        match.end_time = datetime.utcnow()
                        logger.info(f"DEBUG _set_match_status_and_result: Set end_time for match {match_id}")
                    session.commit()
                    logger.info(f"Updated match {match_id} status to {status} and result to {match.result}")

                    # DEBUG: Verify the update
                    session.refresh(match)
                    logger.info(f"DEBUG _set_match_status_and_result: After commit - match.status='{match.status}', match.result='{match.result}', match.under_over_result='{match.under_over_result}'")
                else:
                    logger.error(f"Match {match_id} not found")
            finally:
                session.close()
        except Exception as e:
            logger.error(f"Failed to set match status and result: {e}")
            import traceback
            logger.error(f"DEBUG _set_match_status_and_result: Exception traceback: {traceback.format_exc()}")


    def _get_outcome_coefficient(self, match_id: int, outcome: str, session) -> float:
        """Get coefficient for a specific outcome from match outcomes"""
        try:
            from ..database.models import MatchOutcomeModel

            # For all outcomes (including UNDER/OVER), get from match outcomes
            match_outcome = session.query(MatchOutcomeModel).filter(
                MatchOutcomeModel.match_id == match_id,
                MatchOutcomeModel.column_name == outcome
            ).first()

            return match_outcome.float_value if match_outcome else 1.0
        except Exception as e:
            logger.error(f"Failed to get coefficient for outcome {outcome}: {e}")
            return 1.0

    def _update_bet_results(self, match_id: int, selected_result: str, under_over_result: Optional[str], extraction_winning_outcome_names: List[str], session):
        """Update bet results for UNDER/OVER and selected result with win amount calculation"""
        try:
            logger.info(f"DEBUG _update_bet_results: Starting for match {match_id}, selected_result='{selected_result}', extraction_winning_outcome_names={extraction_winning_outcome_names}")

            # Initialize match variable to avoid UnboundLocalError
            match = None

            # Get coefficient for the selected result
            win_coefficient = self._get_outcome_coefficient(match_id, selected_result, session)
            logger.info(f"DEBUG _update_bet_results: win_coefficient = {win_coefficient}")

            # Use the passed under_over_result
            under_over_outcome = under_over_result
            logger.info(f"DEBUG _update_bet_results: under_over_outcome = '{under_over_outcome}'")

            if under_over_outcome:
                # UNDER/OVER bet wins
                under_over_bets = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.outcome == under_over_outcome,
                    BetDetailModel.result == 'pending'
                ).all()

                logger.info(f"DEBUG _update_bet_results: Found {len(under_over_bets)} winning {under_over_outcome} bets")

                # Get coefficient for the UNDER/OVER outcome
                under_over_coefficient = self._get_outcome_coefficient(match_id, under_over_outcome, session)
                logger.info(f"DEBUG _update_bet_results: under_over_coefficient = {under_over_coefficient}")
                
                for bet in under_over_bets:
                    win_amount = bet.amount * under_over_coefficient
                    bet.result = 'win'
                    bet.win_amount = win_amount
                    logger.info(f"DEBUG _update_bet_results: Set bet {bet.id} to win with amount {win_amount}")

                # Other UNDER/OVER bet loses
                other_under_over = 'OVER' if under_over_outcome == 'UNDER' else 'UNDER'
                losing_count = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.outcome == other_under_over,
                    BetDetailModel.result == 'pending'
                ).update({'result': 'lost'})
                logger.info(f"DEBUG _update_bet_results: Set {losing_count} {other_under_over} bets to lost")
            else:
                # No UNDER/OVER result selected, all UNDER/OVER bets lose
                losing_count = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.outcome.in_(['UNDER', 'OVER']),
                    BetDetailModel.result == 'pending'
                ).update({'result': 'lost'})
                logger.info(f"DEBUG _update_bet_results: Set {losing_count} UNDER/OVER bets to lost (no UNDER/OVER result)")

            # Update bets for the selected result to 'win' (if not UNDER/OVER)
            if selected_result not in ['UNDER', 'OVER']:
                winning_bets = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.outcome == selected_result,
                    BetDetailModel.result == 'pending'
                ).all()

                logger.info(f"DEBUG _update_bet_results: Found {len(winning_bets)} winning {selected_result} bets")

                for bet in winning_bets:
                    win_amount = bet.amount * win_coefficient
                    bet.result = 'win'
                    bet.win_amount = win_amount
                    logger.info(f"DEBUG _update_bet_results: Set bet {bet.id} to win with amount {win_amount}")

            # Update bets for associated winning outcomes to 'win'
            if extraction_winning_outcome_names:
                logger.info(f"DEBUG _update_bet_results: Updating bets for {len(extraction_winning_outcome_names)} associated winning outcomes: {extraction_winning_outcome_names}")

                for outcome_name in extraction_winning_outcome_names:
                    # Skip if this outcome is already handled above (selected_result or UNDER/OVER when under_over_outcome is set)
                    if outcome_name == selected_result or (outcome_name in ['UNDER', 'OVER'] and under_over_outcome is not None):
                        continue

                    # Get coefficient for this associated outcome
                    associated_coefficient = self._get_outcome_coefficient(match_id, outcome_name, session)

                    associated_winning_bets = session.query(BetDetailModel).filter(
                        BetDetailModel.match_id == match_id,
                        BetDetailModel.outcome == outcome_name,
                        BetDetailModel.result == 'pending'
                    ).all()

                    logger.info(f"DEBUG _update_bet_results: Found {len(associated_winning_bets)} winning {outcome_name} bets (associated)")

                    for bet in associated_winning_bets:
                        win_amount = bet.amount * associated_coefficient
                        bet.result = 'win'
                        bet.win_amount = win_amount
                        logger.info(f"DEBUG _update_bet_results: Set associated bet {bet.id} ({outcome_name}) to win with amount {win_amount}")

            # Update all other bets to 'lost'
            losing_outcomes = [selected_result, 'UNDER', 'OVER']
            if extraction_winning_outcome_names:
                losing_outcomes.extend(extraction_winning_outcome_names)

            losing_count = session.query(BetDetailModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.result == 'pending',
                ~BetDetailModel.outcome.in_(losing_outcomes)
            ).update({'result': 'lost'})
            logger.info(f"DEBUG _update_bet_results: Set {losing_count} other bets to lost")

            # Update the match result in the matches table with winning outcomes in separate fields
            match = session.query(MatchModel).filter_by(id=match_id).first()
            if match:
                logger.info(f"DEBUG _update_bet_results: Before update - match.result = '{match.result}'")

                # extraction_winning_outcome_names is already passed as parameter
                logger.info(f"DEBUG _update_bet_results: Using passed extraction_winning_outcome_names: {extraction_winning_outcome_names}")

                # Winning outcomes are now pre-filtered in _perform_result_extraction
                winning_outcome_names = extraction_winning_outcome_names
                logger.info(f"DEBUG _update_bet_results: Using pre-filtered winning outcomes: {winning_outcome_names}")

                # Set the main result
                # The match.result should always contain the actual fight winner (WIN1, DRAW, WIN2, etc.)
                # not UNDER/OVER which is stored separately in under_over_result
                if selected_result in ['UNDER', 'OVER']:
                    # selected_result is UNDER/OVER, we need to find the actual fight winner
                    main_result = None
                    
                    # First, try to find a non-UNDER/OVER outcome from extraction winning outcomes
                    if extraction_winning_outcome_names:
                        for outcome in extraction_winning_outcome_names:
                            if outcome not in ['UNDER', 'OVER']:
                                main_result = outcome
                                logger.info(f"DEBUG _update_bet_results: Found fight winner '{main_result}' from extraction_winning_outcome_names")
                                break
                    
                    # If not found in winning outcomes, look in match outcomes
                    if not main_result:
                        # Get all match outcomes and find a fight winner (non-UNDER/OVER outcome)
                        match_outcomes = session.query(MatchOutcomeModel).filter(
                            MatchOutcomeModel.match_id == match_id
                        ).all()
                        for outcome in match_outcomes:
                            if outcome.column_name not in ['UNDER', 'OVER']:
                                main_result = outcome.column_name
                                logger.info(f"DEBUG _update_bet_results: Found fight winner '{main_result}' from match outcomes")
                                break
                    
                    # Set the result
                    if main_result:
                        match.result = main_result
                        logger.info(f"DEBUG _update_bet_results: selected_result is UNDER/OVER, set match.result to fight winner '{main_result}'")
                    else:
                        # Ultimate fallback - use selected_result but log warning
                        match.result = selected_result
                        logger.warning(f"DEBUG _update_bet_results: Could not find fight winner, using selected_result '{selected_result}' as fallback")
                else:
                    match.result = selected_result
                    logger.info(f"DEBUG _update_bet_results: Set match.result to '{selected_result}'")

                # Set winning outcomes as JSON array in separate field
                if extraction_winning_outcome_names:
                    match.winning_outcomes = json.dumps(extraction_winning_outcome_names)
                    logger.info(f"DEBUG _update_bet_results: Set match.winning_outcomes to {extraction_winning_outcome_names}")
                else:
                    match.winning_outcomes = None
                    logger.info(f"DEBUG _update_bet_results: No winning outcomes, set match.winning_outcomes to None")

                # Set under_over_result in separate field
                if under_over_outcome:
                    match.under_over_result = under_over_outcome
                    logger.info(f"DEBUG _update_bet_results: Set match.under_over_result to '{under_over_outcome}'")
                else:
                    match.under_over_result = None
                    logger.info(f"DEBUG _update_bet_results: No UNDER/OVER result, set match.under_over_result to None")

                logger.info(f"Updated match {match_id} with result='{selected_result}', winning_outcomes={winning_outcome_names}, under_over_result='{under_over_outcome}'")
            else:
                logger.error(f"DEBUG _update_bet_results: Match {match_id} not found for result update!")

            session.commit()
            logger.info(f"Updated bet results for match {match_id}: winner={selected_result}, coefficient={win_coefficient}")

        except Exception as e:
            logger.error(f"Failed to update bet results: {e}")
            import traceback
            logger.error(f"DEBUG _update_bet_results: Exception traceback: {traceback.format_exc()}")
            session.rollback()

    def _collect_match_statistics(self, match_id: int, fixture_id: str, selected_result: str, session):
        """Collect and store statistics for match completion"""
        try:
            from ..database.models import ExtractionStatsModel, BetDetailModel, MatchModel
            import json

            # Get match information
            match = session.query(MatchModel).filter_by(id=match_id).first()
            if not match:
                logger.warning(f"Match {match_id} not found for statistics collection")
                return

            # Store the accumulated shortfall value at the time of match completion
            # This historical value will be used in reports instead of the current global value
            accumulated_shortfall = self._get_global_redistribution_adjustment(session)
            match.accumulated_shortfall = accumulated_shortfall
            logger.info(f"💰 [SHORTFALL TRACKING] Stored accumulated shortfall {accumulated_shortfall:.2f} in match {match_id} at completion time")
            
            # Store the CAP percentage configured at the time of match completion
            cap_percentage = self._get_redistribution_cap()
            match.cap_percent = cap_percentage
            logger.info(f"🎯 [CAP TRACKING] Stored CAP percentage {cap_percentage:.2f}% in match {match_id} at completion time")

            # Calculate statistics (excluding cancelled bets)
            total_bets = session.query(BetDetailModel).join(MatchModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.result != 'cancelled',
                MatchModel.active_status == True
            ).count()

            total_amount_collected = session.query(
                BetDetailModel.amount
            ).join(MatchModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.result != 'cancelled',
                MatchModel.active_status == True
            ).all()
            total_amount_collected = sum(bet.amount for bet in total_amount_collected) if total_amount_collected else 0.0

            # Calculate redistribution amount (sum of all win_amounts)
            total_redistributed = session.query(
                BetDetailModel.win_amount
            ).join(MatchModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.result == 'win',
                MatchModel.active_status == True
            ).all()
            total_redistributed = sum(bet.win_amount for bet in total_redistributed) if total_redistributed else 0.0

            # Get UNDER/OVER specific statistics (excluding cancelled bets)
            under_bets = session.query(BetDetailModel).join(MatchModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.outcome == 'UNDER',
                BetDetailModel.result != 'cancelled',
                MatchModel.active_status == True
            ).count()

            under_amount = session.query(
                BetDetailModel.amount
            ).join(MatchModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.outcome == 'UNDER',
                BetDetailModel.result != 'cancelled',
                MatchModel.active_status == True
            ).all()
            under_amount = sum(bet.amount for bet in under_amount) if under_amount else 0.0

            over_bets = session.query(BetDetailModel).join(MatchModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.outcome == 'OVER',
                BetDetailModel.result != 'cancelled',
                MatchModel.active_status == True
            ).count()

            over_amount = session.query(
                BetDetailModel.amount
            ).join(MatchModel).filter(
                BetDetailModel.match_id == match_id,
                BetDetailModel.outcome == 'OVER',
                BetDetailModel.result != 'cancelled',
                MatchModel.active_status == True
            ).all()
            over_amount = sum(bet.amount for bet in over_amount) if over_amount else 0.0

            # Check if CAP was applied
            cap_percentage = self._get_redistribution_cap()
            cap_applied = False
            cap_threshold = total_amount_collected * (cap_percentage / 100.0)

            # Get extraction result (the actual result selected)
            extraction_result = selected_result if selected_result not in ['UNDER', 'OVER'] else None

            # Create result breakdown (simplified for now)
            result_breakdown = {
                'selected_result': selected_result,
                'extraction_result': extraction_result,
                'under_over_result': selected_result if selected_result in ['UNDER', 'OVER'] else None,
                'total_payin': total_amount_collected,
                'total_payout': total_redistributed,
                'profit': total_amount_collected - total_redistributed
            }

            # Create or update extraction stats record
            stats_record = ExtractionStatsModel(
                match_id=match_id,
                fixture_id=fixture_id,
                match_datetime=match.start_time or datetime.utcnow(),
                total_bets=total_bets,
                total_amount_collected=total_amount_collected,
                total_redistributed=total_redistributed,
                actual_result=selected_result,
                result_breakdown=json.dumps(result_breakdown),
                under_bets=under_bets,
                under_amount=under_amount,
                over_bets=over_bets,
                over_amount=over_amount,
                extraction_result=extraction_result,
                cap_applied=cap_applied,
                cap_percentage=cap_percentage if cap_applied else None
            )

            session.add(stats_record)
            session.commit()

            logger.info(f"Collected statistics for match {match_id}: {total_bets} bets, collected={total_amount_collected:.2f}, redistributed={total_redistributed:.2f}")

        except Exception as e:
            logger.error(f"Failed to collect match statistics: {e}")
            session.rollback()

    def _fallback_result_selection(self) -> str:
        """Fallback result selection when extraction fails"""
        try:
            session = self.db_manager.get_session()
            try:
                from ..database.models import ResultOptionModel

                # Get first active result option (excluding UNDER/OVER)
                result_option = session.query(ResultOptionModel).filter(
                    ResultOptionModel.is_active == True,
                    ~ResultOptionModel.result_name.in_(['UNDER', 'OVER'])
                ).first()

                if result_option:
                    return result_option.result_name
                else:
                    return "WIN1"  # Ultimate fallback

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Fallback result selection failed: {e}")
            return "WIN1"

    def _query_extracted_result(self, match_id: int) -> Optional[str]:
        """Query database for previously extracted result"""
        try:
            logger.info(f"DEBUG _query_extracted_result: Querying for match {match_id}")

            session = self.db_manager.get_session()
            try:
                # Query for the most recent extraction stats for this match
                from ..database.models import ExtractionStatsModel
                extraction_stats = session.query(ExtractionStatsModel).filter(
                    ExtractionStatsModel.match_id == match_id
                ).order_by(ExtractionStatsModel.created_at.desc()).first()

                logger.info(f"DEBUG _query_extracted_result: Query result - extraction_stats exists: {extraction_stats is not None}")

                if extraction_stats:
                    logger.info(f"DEBUG _query_extracted_result: extraction_stats.actual_result = '{extraction_stats.actual_result}'")
                    logger.info(f"DEBUG _query_extracted_result: extraction_stats.extraction_result = '{extraction_stats.extraction_result}'")
                    logger.info(f"DEBUG _query_extracted_result: extraction_stats.created_at = {extraction_stats.created_at}")

                if extraction_stats and extraction_stats.actual_result:
                    logger.info(f"Found extracted result for match {match_id}: {extraction_stats.actual_result}")
                    return extraction_stats.actual_result
                else:
                    logger.warning(f"No extraction stats found for match {match_id}")
                    return None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to query extracted result for match {match_id}: {e}")
            import traceback
            logger.error(f"DEBUG _query_extracted_result: Exception traceback: {traceback.format_exc()}")
            return None

    def _query_winning_outcomes(self, match_id: int) -> List[str]:
        """Query database for winning outcomes from extraction stats"""
        try:
            logger.info(f"DEBUG _query_winning_outcomes: Querying for match {match_id}")

            session = self.db_manager.get_session()
            try:
                # Query for the most recent extraction stats for this match
                from ..database.models import ExtractionStatsModel
                extraction_stats = session.query(ExtractionStatsModel).filter(
                    ExtractionStatsModel.match_id == match_id
                ).order_by(ExtractionStatsModel.created_at.desc()).first()

                if extraction_stats and extraction_stats.result_breakdown:
                    # Parse the result_breakdown JSON to extract winning outcomes
                    import json
                    try:
                        breakdown = json.loads(extraction_stats.result_breakdown)
                        # The winning outcomes should be stored in the breakdown
                        # For now, we'll extract from the match's winning_outcomes field
                        match = session.query(MatchModel).filter_by(id=match_id).first()
                        if match and match.winning_outcomes:
                            winning_outcomes = json.loads(match.winning_outcomes)
                            logger.info(f"Found winning outcomes for match {match_id}: {winning_outcomes}")
                            return winning_outcomes
                    except json.JSONDecodeError as e:
                        logger.error(f"Failed to parse result_breakdown JSON: {e}")

                logger.warning(f"No winning outcomes found for match {match_id}")
                return []

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to query winning outcomes for match {match_id}: {e}")
            import traceback
            logger.error(f"DEBUG _query_winning_outcomes: Exception traceback: {traceback.format_exc()}")
            return []

    def _determine_under_over_result(self, match_id: int, main_result: str) -> Optional[str]:
        """Determine the under/over result for display purposes"""
        try:
            session = self.db_manager.get_session()
            try:
                # First, check if we have a stored under_over_result from video selection
                match = session.query(MatchModel).filter_by(id=match_id).first()
                logger.info(f"DEBUG _determine_under_over_result: Match {match_id} found: {match is not None}")
                if match:
                    logger.info(f"DEBUG _determine_under_over_result: match.under_over_result = '{match.under_over_result}'")
                    if match.under_over_result:
                        logger.info(f"Using stored under_over_result '{match.under_over_result}' for match {match_id}")
                        return match.under_over_result

                # If the main result is already UNDER or OVER, return it
                if main_result in ['UNDER', 'OVER']:
                    logger.info(f"DEBUG _determine_under_over_result: Main result '{main_result}' is UNDER/OVER, returning it")
                    return main_result

                logger.warning(f"DEBUG _determine_under_over_result: No stored under_over_result found for match {match_id}, main_result='{main_result}' - this should not happen!")
                # Check if there are winning UNDER or OVER bets for this match
                from ..database.models import BetDetailModel
                winning_under_over = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.result == 'win',
                    BetDetailModel.outcome.in_(['UNDER', 'OVER'])
                ).first()

                if winning_under_over:
                    logger.info(f"Found winning {winning_under_over.outcome} bet for match {match_id}")
                    return winning_under_over.outcome

                # If no winning UNDER/OVER bets, check if there were any UNDER/OVER bets at all
                # and determine based on some logic (e.g., random or based on main result characteristics)
                under_bets = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.outcome == 'UNDER'
                ).count()

                over_bets = session.query(BetDetailModel).filter(
                    BetDetailModel.match_id == match_id,
                    BetDetailModel.outcome == 'OVER'
                ).count()

                # Simple logic: if there were UNDER bets, show UNDER; if OVER bets, show OVER; otherwise random
                if under_bets > 0 and over_bets == 0:
                    return 'UNDER'
                elif over_bets > 0 and under_bets == 0:
                    return 'OVER'
                elif under_bets > 0 and over_bets > 0:
                    # Both types of bets exist, use simple random selection
                    import random
                    return 'UNDER' if random.random() < 0.5 else 'OVER'
                else:
                    # No UNDER/OVER bets, don't show under/over result
                    return None

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to determine under/over result for match {match_id}: {e}")
            return None

    def _send_play_video_results(self, fixture_id: str, match_id: int, result: str, winning_outcomes: Optional[List[str]] = None):
        """Send PLAY_VIDEO_RESULTS message (plural as per user request)"""
        try:
            logger.info(f"DEBUG _send_play_video_results: Sending PLAY_VIDEO_RESULTS with fixture_id={fixture_id}, match_id={match_id}, result='{result}', winning_outcomes={winning_outcomes}")

            # Determine under/over result separately from main result
            under_over_result = self._determine_under_over_result(match_id, result)
            logger.info(f"DEBUG _send_play_video_results: under_over_result = '{under_over_result}'")

            play_results_message = MessageBuilder.play_video_result(
                sender=self.name,
                fixture_id=fixture_id,
                match_id=match_id,
                result=result,
                under_over_result=under_over_result,
                winning_outcomes=winning_outcomes
            )

            # DEBUG: Log the message content
            logger.info(f"DEBUG _send_play_video_results: Message data: {play_results_message.data}")

            self.message_bus.publish(play_results_message)
            logger.info(f"Sent PLAY_VIDEO_RESULTS for fixture {fixture_id}, match {match_id}, result {result}, under_over {under_over_result}, winning_outcomes {winning_outcomes}")

        except Exception as e:
            logger.error(f"Failed to send PLAY_VIDEO_RESULTS: {e}")
            import traceback
            logger.error(f"DEBUG _send_play_video_results: Exception traceback: {traceback.format_exc()}")

    def _send_play_video_result(self, fixture_id: str, match_id: int, result: str):
        """Send PLAY_VIDEO_RESULT message"""
        try:
            play_result_message = MessageBuilder.play_video_result(
                sender=self.name,
                fixture_id=fixture_id,
                match_id=match_id,
                result=result
            )
            self.message_bus.publish(play_result_message)
            logger.info(f"Sent PLAY_VIDEO_RESULT for fixture {fixture_id}, match {match_id}, result {result}")

        except Exception as e:
            logger.error(f"Failed to send PLAY_VIDEO_RESULT: {e}")

    def _send_match_done(self, fixture_id: str, match_id: int, result: str = None):
        """Send MATCH_DONE message"""
        try:
            logger.info(f"DEBUG _send_match_done: Sending MATCH_DONE with fixture_id={fixture_id}, match_id={match_id}, result='{result}'")

            match_done_message = MessageBuilder.match_done(
                sender=self.name,
                fixture_id=fixture_id,
                match_id=match_id,
                result=result
            )

            # DEBUG: Log the message content
            logger.info(f"DEBUG _send_match_done: Message data: {match_done_message.data}")

            self.message_bus.publish(match_done_message)
            logger.info(f"Sent MATCH_DONE for fixture {fixture_id}, match {match_id}, result {result}")

        except Exception as e:
            logger.error(f"Failed to send MATCH_DONE: {e}")
            import traceback
            logger.error(f"DEBUG _send_match_done: Exception traceback: {traceback.format_exc()}")

    def _send_next_match(self, fixture_id: str, match_id: int):
        """Send NEXT_MATCH message"""
        try:
            next_match_message = MessageBuilder.next_match(
                sender=self.name,
                fixture_id=fixture_id,
                match_id=match_id
            )
            self.message_bus.publish(next_match_message)
            logger.info(f"Sent NEXT_MATCH for fixture {fixture_id}, match {match_id}")

        except Exception as e:
            logger.error(f"Failed to send NEXT_MATCH: {e}")

    def _select_random_completed_matches(self, count: int, session) -> List[MatchModel]:
        """Select random completed matches from the database (including cancelled and failed)"""
        try:
            from sqlalchemy.orm import joinedload

            # Get all completed matches (status = 'done', 'cancelled', or 'failed')
            # Exclude matches from fixtures that contain "_recycle_" in the fixture name
            completed_matches = session.query(MatchModel).options(joinedload(MatchModel.outcomes)).filter(
                MatchModel.status.in_(['done', 'end', 'cancelled', 'failed']),
                MatchModel.active_status == True,
                ~MatchModel.fixture_id.like('%_recycle_%')
            ).all()

            if len(completed_matches) < count:
                logger.warning(f"Only {len(completed_matches)} completed matches found, requested {count}")
                return completed_matches

            # Select random matches
            import random
            selected_matches = random.sample(completed_matches, count)
            logger.info(f"Selected {len(selected_matches)} random completed matches")
            return selected_matches

        except Exception as e:
            logger.error(f"Failed to select random completed matches: {e}")
            return []

    def _create_matches_from_old_matches(self, fixture_id: str, old_matches: List[MatchModel], session):
        """Create new matches in the fixture by copying from old completed matches"""
        try:
            now = datetime.utcnow()

            # Determine the status for new matches based on system state
            new_match_status = self._determine_new_match_status(fixture_id, session)

            # Find the maximum match_number in the fixture and increment from there
            max_match_number = session.query(MatchModel.match_number).filter(
                MatchModel.fixture_id == fixture_id
            ).order_by(MatchModel.match_number.desc()).first()

            match_number = (max_match_number[0] + 1) if max_match_number else 1

            for old_match in old_matches:
                # Create a new match based on the old one
                new_match = MatchModel(
                    match_number=match_number,
                    fighter1_township=old_match.fighter1_township,
                    fighter2_township=old_match.fighter2_township,
                    venue_kampala_township=old_match.venue_kampala_township,
                    start_time=now,
                    status=new_match_status,
                    fixture_id=fixture_id,
                    filename=old_match.filename,
                    file_sha1sum=old_match.file_sha1sum,
                    active_status=True,
                    zip_filename=old_match.zip_filename,
                    zip_sha1sum=old_match.zip_sha1sum,
                    zip_upload_status='completed',  # Assume ZIP is already available
                    zip_validation_status='valid',  # ZIP already validated from old match
                    fixture_active_time=int(now.timestamp()),
                    result=None,  # Reset result for new match
                    end_time=None,  # Reset end time for new match
                    done=False,  # Reset done flag for new match
                    running=False  # Reset running flag for new match
                )

                session.add(new_match)
                session.flush()  # Get the ID

                # Copy match outcomes
                for outcome in old_match.outcomes:
                    new_outcome = MatchOutcomeModel(
                        match_id=new_match.id,
                        column_name=outcome.column_name,
                        float_value=outcome.float_value
                    )
                    session.add(new_outcome)

                logger.debug(f"Created new match #{match_number} from old match #{old_match.match_number} with status {new_match_status}")
                match_number += 1

            session.commit()
            logger.info(f"Created {len(old_matches)} new matches in fixture {fixture_id} with status {new_match_status}")

        except Exception as e:
            logger.error(f"Failed to create matches from old matches: {e}")
            session.rollback()
            raise

    def _create_matches_from_templates(self, fixture_id: str, template_matches: List[MatchTemplateModel], session):
        """Create new matches in the fixture by copying from match templates"""
        try:
            now = datetime.utcnow()

            # Determine the status for new matches based on system state
            new_match_status = self._determine_new_match_status(fixture_id, session)

            # Find the maximum match_number in the fixture and increment from there
            max_match_number = session.query(MatchModel.match_number).filter(
                MatchModel.fixture_id == fixture_id
            ).order_by(MatchModel.match_number.desc()).first()

            match_number = (max_match_number[0] + 1) if max_match_number else 1

            for template_match in template_matches:
                # Create a new match based on the template
                new_match = MatchModel(
                    match_number=match_number,
                    fighter1_township=template_match.fighter1_township,
                    fighter2_township=template_match.fighter2_township,
                    venue_kampala_township=template_match.venue_kampala_township,
                    start_time=now,
                    status=new_match_status,
                    fixture_id=fixture_id,
                    filename=template_match.filename,
                    file_sha1sum=template_match.file_sha1sum,
                    active_status=True,
                    zip_filename=template_match.zip_filename,
                    zip_sha1sum=template_match.zip_sha1sum,
                    zip_upload_status='completed',  # Assume ZIP is already available
                    zip_validation_status='valid',  # ZIP already validated from template
                    fixture_active_time=int(now.timestamp()),
                    result=None,  # Reset result for new match
                    end_time=None,  # Reset end time for new match
                    done=False,  # Reset done flag for new match
                    running=False  # Reset running flag for new match
                )

                session.add(new_match)
                session.flush()  # Get the ID

                # Copy match outcomes from template
                for template_outcome in template_match.outcomes:
                    new_outcome = MatchOutcomeModel(
                        match_id=new_match.id,
                        column_name=template_outcome.column_name,
                        float_value=template_outcome.float_value
                    )
                    session.add(new_outcome)

                logger.debug(f"Created new match #{match_number} from template #{template_match.match_number} with status {new_match_status}")
                match_number += 1

            session.commit()
            logger.info(f"Created {len(template_matches)} new matches in fixture {fixture_id} with status {new_match_status}")

        except Exception as e:
            logger.error(f"Failed to create matches from templates: {e}")
            session.rollback()
            raise

    def _create_new_fixture_from_old_matches(self, old_matches: List[MatchModel], session) -> Optional[str]:
        """Create a new fixture with matches copied from old completed matches"""
        try:
            # Generate a unique fixture ID
            import uuid
            fixture_id = f"recycle_{uuid.uuid4().hex[:8]}"
            now = datetime.utcnow()

            # Determine the status for new matches based on system state
            new_match_status = self._determine_new_match_status(fixture_id, session)

            # For a new fixture, start match_number from 1
            match_number = 1
            for old_match in old_matches:
                # Create a new match based on the old one
                new_match = MatchModel(
                    match_number=match_number,
                    fighter1_township=old_match.fighter1_township,
                    fighter2_township=old_match.fighter2_township,
                    venue_kampala_township=old_match.venue_kampala_township,
                    start_time=now,
                    status=new_match_status,
                    fixture_id=fixture_id,
                    filename=old_match.filename,
                    file_sha1sum=old_match.file_sha1sum,
                    active_status=True,
                    zip_filename=old_match.zip_filename,
                    zip_sha1sum=old_match.zip_sha1sum,
                    zip_upload_status='completed',  # Assume ZIP is already available
                    zip_validation_status='valid',  # ZIP already validated from old match
                    fixture_active_time=int(now.timestamp()),
                    result=None,  # Reset result for new match
                    end_time=None,  # Reset end time for new match
                    done=False,  # Reset done flag for new match
                    running=False  # Reset running flag for new match
                )

                session.add(new_match)
                session.flush()  # Get the ID

                # Copy match outcomes
                for outcome in old_match.outcomes:
                    new_outcome = MatchOutcomeModel(
                        match_id=new_match.id,
                        column_name=outcome.column_name,
                        float_value=outcome.float_value
                    )
                    session.add(new_outcome)

                logger.debug(f"Created match #{match_number} in new fixture {fixture_id} from old match #{old_match.match_number}")
                match_number += 1

            session.commit()
            logger.info(f"Created new fixture {fixture_id} with {len(old_matches)} matches from old completed matches (status: {new_match_status})")
            return fixture_id

        except Exception as e:
            logger.error(f"Failed to create new fixture from old matches: {e}")
            session.rollback()
            return None

    def _create_new_fixture_from_templates(self, template_matches: List[MatchTemplateModel], session) -> Optional[str]:
        """Create a new fixture with matches copied from match templates"""
        try:
            # Generate a unique fixture ID
            import uuid
            fixture_id = f"template_{uuid.uuid4().hex[:8]}"
            now = datetime.utcnow()

            # Determine the status for new matches based on system state
            new_match_status = self._determine_new_match_status(fixture_id, session)

            # For a new fixture, start match_number from 1
            match_number = 1
            for template_match in template_matches:
                # Create a new match based on the template
                new_match = MatchModel(
                    match_number=match_number,
                    fighter1_township=template_match.fighter1_township,
                    fighter2_township=template_match.fighter2_township,
                    venue_kampala_township=template_match.venue_kampala_township,
                    start_time=now,
                    status=new_match_status,
                    fixture_id=fixture_id,
                    filename=template_match.filename,
                    file_sha1sum=template_match.file_sha1sum,
                    active_status=True,
                    zip_filename=template_match.zip_filename,
                    zip_sha1sum=template_match.zip_sha1sum,
                    zip_upload_status='completed',  # Assume ZIP is already available
                    zip_validation_status='valid',  # ZIP already validated from template
                    fixture_active_time=int(now.timestamp()),
                    result=None,  # Reset result for new match
                    end_time=None,  # Reset end time for new match
                    done=False,  # Reset done flag for new match
                    running=False  # Reset running flag for new match
                )

                session.add(new_match)
                session.flush()  # Get the ID

                # Copy match outcomes from template
                for template_outcome in template_match.outcomes:
                    new_outcome = MatchOutcomeModel(
                        match_id=new_match.id,
                        column_name=template_outcome.column_name,
                        float_value=template_outcome.float_value
                    )
                    session.add(new_outcome)

                logger.debug(f"Created match #{match_number} in new fixture {fixture_id} from template #{template_match.match_number}")
                match_number += 1

            session.commit()
            logger.info(f"Created new fixture {fixture_id} with {len(template_matches)} matches from match templates (status: {new_match_status})")
            return fixture_id

        except Exception as e:
            logger.error(f"Failed to create new fixture from templates: {e}")
            session.rollback()
            return None

    def _create_new_fixture_from_templates_for_continuation(self, template_matches: List[MatchTemplateModel], session) -> Optional[str]:
        """Create a new fixture for continuation with matches copied from match templates, ensuring match numbers start from 1"""
        try:
            # Generate a unique fixture ID for continuation creation
            import uuid
            fixture_id = f"continuation_template_{uuid.uuid4().hex[:8]}"
            now = datetime.utcnow()

            # Determine the status for new matches based on system state
            new_match_status = self._determine_new_match_status(fixture_id, session)

            # For continuation fixture creation, always start match_number from 1
            match_number = 1
            for template_match in template_matches:
                # Create a new match based on the template
                new_match = MatchModel(
                    match_number=match_number,
                    fighter1_township=template_match.fighter1_township,
                    fighter2_township=template_match.fighter2_township,
                    venue_kampala_township=template_match.venue_kampala_township,
                    start_time=now,
                    status=new_match_status,
                    fixture_id=fixture_id,
                    filename=template_match.filename,
                    file_sha1sum=template_match.file_sha1sum,
                    active_status=True,
                    zip_filename=template_match.zip_filename,
                    zip_sha1sum=template_match.zip_sha1sum,
                    zip_upload_status='completed',  # Assume ZIP is already available
                    zip_validation_status='valid',  # ZIP already validated from template
                    fixture_active_time=int(now.timestamp()),
                    result=None,  # Reset result for new match
                    end_time=None,  # Reset end time for new match
                    done=False,  # Reset done flag for new match
                    running=False  # Reset running flag for new match
                )

                session.add(new_match)
                session.flush()  # Get the ID

                # Copy match outcomes from template
                for template_outcome in template_match.outcomes:
                    new_outcome = MatchOutcomeModel(
                        match_id=new_match.id,
                        column_name=template_outcome.column_name,
                        float_value=template_outcome.float_value
                    )
                    session.add(new_outcome)

                logger.debug(f"Created match #{match_number} in continuation fixture {fixture_id} from template #{template_match.match_number}")
                match_number += 1

            session.commit()
            logger.info(f"Created continuation fixture {fixture_id} with {len(template_matches)} matches from match templates (status: {new_match_status})")
            return fixture_id

        except Exception as e:
            logger.error(f"Failed to create new fixture from templates for continuation: {e}")
            session.rollback()
            return None

    def _create_new_fixture_from_templates_at_midnight(self, template_matches: List[MatchTemplateModel], session) -> Optional[str]:
        """Create a new fixture at midnight with matches copied from match templates, ensuring match numbers start from 1"""
        try:
            # Generate a unique fixture ID for midnight creation
            import uuid
            fixture_id = f"midnight_template_{uuid.uuid4().hex[:8]}"
            now = datetime.utcnow()

            # Determine the status for new matches based on system state
            new_match_status = self._determine_new_match_status(fixture_id, session)

            # For midnight fixture creation, always start match_number from 1
            match_number = 1
            for template_match in template_matches:
                # Create a new match based on the template
                new_match = MatchModel(
                    match_number=match_number,
                    fighter1_township=template_match.fighter1_township,
                    fighter2_township=template_match.fighter2_township,
                    venue_kampala_township=template_match.venue_kampala_township,
                    start_time=now,
                    status=new_match_status,
                    fixture_id=fixture_id,
                    filename=template_match.filename,
                    file_sha1sum=template_match.file_sha1sum,
                    active_status=True,
                    zip_filename=template_match.zip_filename,
                    zip_sha1sum=template_match.zip_sha1sum,
                    zip_upload_status='completed',  # Assume ZIP is already available
                    zip_validation_status='valid',  # ZIP already validated from template
                    fixture_active_time=int(now.timestamp()),
                    result=None,  # Reset result for new match
                    end_time=None,  # Reset end time for new match
                    done=False,  # Reset done flag for new match
                    running=False  # Reset running flag for new match
                )

                session.add(new_match)
                session.flush()  # Get the ID

                # Copy match outcomes from template
                for template_outcome in template_match.outcomes:
                    new_outcome = MatchOutcomeModel(
                        match_id=new_match.id,
                        column_name=template_outcome.column_name,
                        float_value=template_outcome.float_value
                    )
                    session.add(new_outcome)

                logger.debug(f"Created match #{match_number} in midnight fixture {fixture_id} from template #{template_match.match_number}")
                match_number += 1

            session.commit()
            logger.info(f"Created midnight fixture {fixture_id} with {len(template_matches)} matches from match templates (status: {new_match_status})")
            return fixture_id

        except Exception as e:
            logger.error(f"Failed to create new fixture from templates at midnight: {e}")
            session.rollback()
            return None

    def _create_new_fixture_from_old_matches_for_continuation(self, old_matches: List[MatchModel], session) -> Optional[str]:
        """Create a new fixture for continuation with matches copied from old completed matches, ensuring match numbers start from 1"""
        try:
            # Generate a unique fixture ID for continuation creation
            import uuid
            fixture_id = f"continuation_recycle_{uuid.uuid4().hex[:8]}"
            now = datetime.utcnow()

            # Determine the status for new matches based on system state
            new_match_status = self._determine_new_match_status(fixture_id, session)

            # For continuation fixture creation, always start match_number from 1
            match_number = 1
            for old_match in old_matches:
                # Create a new match based on the old one
                new_match = MatchModel(
                    match_number=match_number,
                    fighter1_township=old_match.fighter1_township,
                    fighter2_township=old_match.fighter2_township,
                    venue_kampala_township=old_match.venue_kampala_township,
                    start_time=now,
                    status=new_match_status,
                    fixture_id=fixture_id,
                    filename=old_match.filename,
                    file_sha1sum=old_match.file_sha1sum,
                    active_status=True,
                    zip_filename=old_match.zip_filename,
                    zip_sha1sum=old_match.zip_sha1sum,
                    zip_upload_status='completed',  # Assume ZIP is already available
                    zip_validation_status='valid',  # ZIP already validated from old match
                    fixture_active_time=int(now.timestamp()),
                    result=None,  # Reset result for new match
                    end_time=None,  # Reset end time for new match
                    done=False,  # Reset done flag for new match
                    running=False  # Reset running flag for new match
                )

                session.add(new_match)
                session.flush()  # Get the ID

                # Copy match outcomes
                for outcome in old_match.outcomes:
                    new_outcome = MatchOutcomeModel(
                        match_id=new_match.id,
                        column_name=outcome.column_name,
                        float_value=outcome.float_value
                    )
                    session.add(new_outcome)

                logger.debug(f"Created match #{match_number} in continuation fixture {fixture_id} from old match #{old_match.match_number}")
                match_number += 1

            session.commit()
            logger.info(f"Created continuation fixture {fixture_id} with {len(old_matches)} matches from old completed matches (status: {new_match_status})")
            return fixture_id

        except Exception as e:
            logger.error(f"Failed to create new fixture from old matches for continuation: {e}")
            session.rollback()
            return None

    def _create_new_fixture_from_old_matches_at_midnight(self, old_matches: List[MatchModel], session) -> Optional[str]:
        """Create a new fixture at midnight with matches copied from old completed matches, ensuring match numbers start from 1"""
        try:
            # Generate a unique fixture ID for midnight creation
            import uuid
            fixture_id = f"midnight_recycle_{uuid.uuid4().hex[:8]}"
            now = datetime.utcnow()

            # Determine the status for new matches based on system state
            new_match_status = self._determine_new_match_status(fixture_id, session)

            # For midnight fixture creation, always start match_number from 1
            match_number = 1
            for old_match in old_matches:
                # Create a new match based on the old one
                new_match = MatchModel(
                    match_number=match_number,
                    fighter1_township=old_match.fighter1_township,
                    fighter2_township=old_match.fighter2_township,
                    venue_kampala_township=old_match.venue_kampala_township,
                    start_time=now,
                    status=new_match_status,
                    fixture_id=fixture_id,
                    filename=old_match.filename,
                    file_sha1sum=old_match.file_sha1sum,
                    active_status=True,
                    zip_filename=old_match.zip_filename,
                    zip_sha1sum=old_match.zip_sha1sum,
                    zip_upload_status='completed',  # Assume ZIP is already available
                    zip_validation_status='valid',  # ZIP already validated from old match
                    fixture_active_time=int(now.timestamp()),
                    result=None,  # Reset result for new match
                    end_time=None,  # Reset end time for new match
                    done=False,  # Reset done flag for new match
                    running=False  # Reset running flag for new match
                )

                session.add(new_match)
                session.flush()  # Get the ID

                # Copy match outcomes
                for outcome in old_match.outcomes:
                    new_outcome = MatchOutcomeModel(
                        match_id=new_match.id,
                        column_name=outcome.column_name,
                        float_value=outcome.float_value
                    )
                    session.add(new_outcome)

                logger.debug(f"Created match #{match_number} in midnight fixture {fixture_id} from old match #{old_match.match_number}")
                match_number += 1

            session.commit()
            logger.info(f"Created midnight fixture {fixture_id} with {len(old_matches)} matches from old completed matches (status: {new_match_status})")
            return fixture_id

        except Exception as e:
            logger.error(f"Failed to create new fixture from old matches at midnight: {e}")
            session.rollback()
            return None

    def _determine_game_status(self) -> str:
        """Determine the current game status for status requests"""
        try:
            # If waiting for validation, return waiting status
            if self.waiting_for_validation_fixture:
                return "waiting_for_downloads"

            # If a game is currently active, return "started"
            if self.game_active and self.current_fixture_id:
                return "started"

            # Check if there are any active fixtures (matches in non-terminal states)
            session = self.db_manager.get_session()
            try:
                # Get today's date in venue timezone (for day change detection)
                today = self._get_today_venue_date()

                # Convert venue date range to UTC for database query
                from ..utils.timezone_utils import venue_to_utc_datetime
                venue_start = datetime.combine(today, datetime.min.time())
                venue_end = datetime.combine(today, datetime.max.time())
                utc_start = venue_to_utc_datetime(venue_start, self.db_manager)
                utc_end = venue_to_utc_datetime(venue_end, self.db_manager)

                # Check for active matches today
                active_matches = session.query(MatchModel).filter(
                    MatchModel.start_time.isnot(None),
                    MatchModel.start_time >= utc_start,
                    MatchModel.start_time < utc_end,
                    MatchModel.status.notin_(['done', 'cancelled', 'failed', 'paused']),
                    MatchModel.active_status == True
                ).all()

                if active_matches:
                    # Active matches found - return "already_active" since validation happens asynchronously
                    logger.debug("Active matches found - game can be activated")
                    return "already_active"

                # Check if all today's fixtures are in terminal states
                if self._has_today_fixtures_all_terminal():
                    return "completed_no_old_matches"

                # Check if there are any fixtures at all (even if not today)
                any_fixtures = session.query(MatchModel).filter(
                    MatchModel.active_status == True
                ).count()

                if any_fixtures > 0:
                    return "ready"  # Fixtures exist but no active game

                # No fixtures at all
                return "shutdown"

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to determine game status: {e}")
            return "ready"  # Default fallback

    def _count_remaining_matches_in_fixture(self, fixture_id: str) -> int:
        """Count remaining matches in fixture that can still be played"""
        try:
            session = self.db_manager.get_session()
            try:
                # Count matches that are not in terminal states (done, cancelled, failed, paused)
                remaining_count = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.status.notin_(['done', 'cancelled', 'failed', 'paused']),
                    MatchModel.active_status == True
                ).count()

                logger.debug(f"Fixture {fixture_id} has {remaining_count} remaining matches")
                return remaining_count

            finally:
                session.close()

        except Exception as e:
            logger.error(f"Failed to count remaining matches in fixture {fixture_id}: {e}")
            return 0

    def _ensure_minimum_matches_in_fixture(self, fixture_id: str, minimum_required: int):
        """Ensure fixture has at least minimum_required matches by creating new ones from templates or old matches"""
        try:
            logger.info(f"🔄 Ensuring fixture {fixture_id} has at least {minimum_required} matches")

            session = self.db_manager.get_session()
            try:
                # First try: Select random match templates
                template_matches = self._select_random_match_templates(minimum_required, session)

                if template_matches:
                    logger.info(f"📋 Selected {len(template_matches)} match templates to create new matches")
                    self._create_matches_from_templates(fixture_id, template_matches, session)
                    logger.info(f"✅ Created {len(template_matches)} new matches in fixture {fixture_id} from templates")
                    return

                # Second try: Select random completed matches from matches table
                logger.info("No match templates available, trying to reuse old completed matches")
                old_matches = self._select_random_completed_matches(minimum_required, session)

                if old_matches:
                    logger.info(f"📋 Selected {len(old_matches)} old completed matches to create new matches")
                    self._create_matches_from_old_matches(fixture_id, old_matches, session)
                    logger.info(f"✅ Created {len(old_matches)} new matches in fixture {fixture_id} from old matches")
                    return

                # No matches available at all
                logger.warning(f"⚠️ No match templates or old completed matches found - cannot create new matches for fixture {fixture_id}")

            finally:
                session.close()

        except Exception as e:
            logger.error(f"❌ Failed to ensure minimum matches in fixture {fixture_id}: {e}")

    def _get_last_played_match_id(self, fixture_id: str, session) -> Optional[int]:
        """Get the ID of the last match that was played in this fixture"""
        try:
            # Find the most recently completed match in this fixture
            last_match = session.query(MatchModel).filter(
                MatchModel.fixture_id == fixture_id,
                MatchModel.status.in_(['done', 'end', 'cancelled', 'failed']),
                MatchModel.active_status == True
            ).order_by(MatchModel.updated_at.desc()).first()

            if last_match:
                logger.debug(f"Last played match in fixture {fixture_id}: #{last_match.match_number} (ID: {last_match.id})")
                return last_match.id
            else:
                logger.debug(f"No completed matches found in fixture {fixture_id}")
                return None

        except Exception as e:
            logger.error(f"Failed to get last played match ID for fixture {fixture_id}: {e}")
            return None

    def _select_random_completed_matches_excluding_last(self, count: int, exclude_match_id: Optional[int], session) -> List[MatchModel]:
        """Select random completed matches from the database, excluding matches with same fighters as the last played match"""
        try:
            # Build query for completed matches
            # Exclude matches from fixtures that contain "_recycle_" in the fixture name
            query = session.query(MatchModel).filter(
                MatchModel.status.in_(['done', 'end', 'cancelled', 'failed']),
                MatchModel.active_status == True,
                ~MatchModel.fixture_id.like('%_recycle_%')
            )

            # Exclude matches with same fighters as the last played match
            if exclude_match_id:
                last_match = session.query(MatchModel).filter(MatchModel.id == exclude_match_id).first()
                if last_match:
                    # Exclude matches with same fighter combinations (both directions)
                    query = query.filter(
                        ~((MatchModel.fighter1_township == last_match.fighter1_township) &
                          (MatchModel.fighter2_township == last_match.fighter2_township)) &
                        ~((MatchModel.fighter1_township == last_match.fighter2_township) &
                          (MatchModel.fighter2_township == last_match.fighter1_township))
                    )
                    logger.info(f"Excluding matches with fighters: {last_match.fighter1_township} vs {last_match.fighter2_township}")

            completed_matches = query.all()

            if len(completed_matches) < count:
                logger.warning(f"Only {len(completed_matches)} completed matches available (excluding same fighters), requested {count}")
                return completed_matches

            # Select random matches
            import random
            selected_matches = random.sample(completed_matches, count)
            logger.info(f"Selected {len(selected_matches)} random completed matches (excluding same fighters as last match)")
            return selected_matches

        except Exception as e:
            logger.error(f"Failed to select random completed matches excluding same fighters: {e}")
            return []

    def _select_random_completed_matches_with_fallback(self, count: int, fixture_id: Optional[str], session, max_attempts: int = 5) -> List[MatchModel]:
        """Select random matches with progressive fallback - try up to 5 times with relaxed criteria"""
        import random

        for attempt in range(max_attempts):
            try:
                if attempt == 0:
                    # Attempt 1: Exclude last 3 matches (fighters + venue)
                    exclusion_count = 3
                    fighters_only = False
                    logger.info(f"🎯 Attempt {attempt + 1}: Excluding last {exclusion_count} matches (fighters + venue)")
                elif attempt == 1:
                    # Attempt 2: Exclude last 2 matches (fighters + venue)
                    exclusion_count = 2
                    fighters_only = False
                    logger.info(f"🎯 Attempt {attempt + 1}: Excluding last {exclusion_count} matches (fighters + venue)")
                elif attempt == 2:
                    # Attempt 3: Exclude last 1 match (fighters + venue)
                    exclusion_count = 1
                    fighters_only = False
                    logger.info(f"🎯 Attempt {attempt + 1}: Excluding last {exclusion_count} match (fighters + venue)")
                elif attempt == 3:
                    # Attempt 4: Exclude last 1 match (fighters only, ignore venue)
                    exclusion_count = 1
                    fighters_only = True
                    logger.info(f"🎯 Attempt {attempt + 1}: Excluding last {exclusion_count} match (fighters only)")
                else:
                    # Attempt 5: No exclusions
                    exclusion_count = 0
                    fighters_only = False
                    logger.info(f"🎯 Attempt {attempt + 1}: No exclusions (final fallback)")

                # Get available matches with current exclusion criteria
                available_matches = self._get_available_matches_excluding_recent(
                    fixture_id, exclusion_count, fighters_only, session
                )

                if len(available_matches) >= count:
                    selected = random.sample(available_matches, count)
                    logger.info(f"✅ Success on attempt {attempt + 1}: selected {len(selected)} matches from {len(available_matches)} available")
                    return selected
                else:
                    logger.warning(f"⚠️ Attempt {attempt + 1} failed: only {len(available_matches)} matches available, need {count}")
                    continue

            except Exception as e:
                logger.error(f"❌ Attempt {attempt + 1} failed with error: {e}")
                continue

        # Final fallback: return whatever matches are available
        try:
            logger.warning(f"🚨 All {max_attempts} attempts failed - returning all available matches")
            from sqlalchemy.orm import joinedload
            all_matches = session.query(MatchModel).options(joinedload(MatchModel.outcomes)).filter(
                MatchModel.status.in_(['done', 'end', 'cancelled', 'failed']),
                MatchModel.active_status == True,
                ~MatchModel.fixture_id.like('%_recycle_%')
            ).all()

            if all_matches:
                result = all_matches[:count] if len(all_matches) >= count else all_matches
                logger.info(f"🔄 Final fallback: returning {len(result)} matches from {len(all_matches)} total available")
                return result
            else:
                # If no matches found with exclusions, recycle the oldest match from database
                logger.warning("🚨 No matches available after exclusions - recycling the oldest match from database")
                from sqlalchemy.orm import joinedload
                oldest_match = session.query(MatchModel).options(joinedload(MatchModel.outcomes)).filter(
                    MatchModel.status.in_(['done', 'end', 'cancelled', 'failed']),
                    MatchModel.active_status == True
                ).order_by(MatchModel.created_at.asc()).first()

                if oldest_match:
                    logger.info(f"♻️ Recycled oldest match: {oldest_match.match_number} ({oldest_match.fighter1_township} vs {oldest_match.fighter2_township})")
                    return [oldest_match]
                else:
                    logger.error("🚨 No completed matches found in database at all")
                    return []
        except Exception as e:
            logger.error(f"Failed to select random completed matches with fallback: {e}")
            return []

    def _select_random_match_templates(self, count: int, session) -> List[MatchTemplateModel]:
        """Select random match templates from the database that have validated ZIP files"""
        try:
            from sqlalchemy.orm import joinedload

            # Get all active match templates with validated ZIP files
            match_templates = session.query(MatchTemplateModel).options(joinedload(MatchTemplateModel.outcomes)).filter(
                MatchTemplateModel.active_status == True,
                MatchTemplateModel.zip_upload_status == 'completed',
                MatchTemplateModel.zip_validation_status == 'valid'
            ).all()

            if len(match_templates) < count:
                logger.warning(f"Only {len(match_templates)} validated match templates found, requested {count}")
                
                # If templates table is completely empty, trigger forced API update
                if len(match_templates) == 0:
                    logger.warning("Match templates table is empty - triggering forced API update to synchronize with server")
                    self._trigger_forced_api_update()
                
                return match_templates

            # Select random templates
            import random
            selected_templates = random.sample(match_templates, count)
            logger.info(f"Selected {len(selected_templates)} random validated match templates")
            return selected_templates

        except Exception as e:
            logger.error(f"Failed to select random match templates: {e}")
            return []

    def _trigger_forced_api_update(self):
        """Trigger forced API update to synchronize templates with server"""
        try:
            logger.info("Triggering forced API update to synchronize match templates")
            force_update_message = Message(
                type=MessageType.FORCE_API_UPDATE,
                sender=self.name,
                recipient="api_client",
                data={
                    "force_timestamp": True,  # Force timestamp to ensure latest fixture
                    "reason": "empty_templates_table"
                }
            )
            self.message_bus.publish(force_update_message)
        except Exception as e:
            logger.error(f"Failed to trigger forced API update: {e}")

    def _handle_force_api_update(self, message: Message):
        """Handle FORCE_API_UPDATE response from API client"""
        try:
            logger.info(f"Received FORCE_API_UPDATE response: {message.data}")
            # Could trigger retry of START_GAME if needed
        except Exception as e:
            logger.error(f"Failed to handle FORCE_API_UPDATE response: {e}")

    def _get_available_matches_excluding_recent(self, fixture_id: Optional[str], exclude_last_n: int, fighters_only: bool, session) -> List[MatchModel]:
        """Get available matches excluding the last N recent matches in the fixture"""
        try:
            # If no fixture_id provided (creating new fixture), don't exclude any recent matches
            if fixture_id is None:
                recent_matches = []
            else:
                # Get the last N matches in the fixture (by match_number, regardless of completion status)
                recent_matches = session.query(MatchModel).filter(
                    MatchModel.fixture_id == fixture_id,
                    MatchModel.active_status == True
                ).order_by(MatchModel.match_number.desc()).limit(exclude_last_n).all()

            logger.debug(f"Found {len(recent_matches)} recent matches to exclude: {[f'#{m.match_number}: {m.fighter1_township} vs {m.fighter2_township}' for m in recent_matches]}")

            # Build exclusion filters
            exclusion_filters = []
            for recent_match in recent_matches:
                if fighters_only:
                    # Exclude matches with same fighters only (both directions)
                    exclusion_filters.append(
                        ~((MatchModel.fighter1_township == recent_match.fighter1_township) &
                          (MatchModel.fighter2_township == recent_match.fighter2_township)) &
                        ~((MatchModel.fighter1_township == recent_match.fighter2_township) &
                          (MatchModel.fighter2_township == recent_match.fighter1_township))
                    )
                else:
                    # Exclude matches with same fighters AND venue
                    exclusion_filters.append(
                        ~((MatchModel.fighter1_township == recent_match.fighter1_township) &
                          (MatchModel.fighter2_township == recent_match.fighter2_township) &
                          (MatchModel.venue_kampala_township == recent_match.venue_kampala_township))
                    )

            # Query available matches with exclusions
            from sqlalchemy.orm import joinedload
            query = session.query(MatchModel).options(joinedload(MatchModel.outcomes)).filter(
                MatchModel.status.in_(['done', 'end', 'cancelled', 'failed']),
                MatchModel.active_status == True,
                ~MatchModel.fixture_id.like('%_recycle_%'),
                *exclusion_filters
            )

            available_matches = query.all()
            logger.debug(f"Found {len(available_matches)} matches available after exclusions")
            return available_matches

        except Exception as e:
            logger.error(f"Failed to get available matches excluding recent: {e}")
            return []

    def _determine_new_match_status(self, fixture_id: str, session) -> str:
        """Determine the status for new matches based on system state"""
        try:
            # Get betting mode configuration
            betting_mode = self._get_betting_mode_config()

            # If betting mode is "all_bets_on_start", new matches should be 'bet'
            if betting_mode == "all_bets_on_start":
                logger.info(f"Betting mode is 'all_bets_on_start' - new matches will be in bet status")
                return 'bet'

            # Check if the last match in the fixture is in 'bet' status
            last_match = session.query(MatchModel).filter(
                MatchModel.fixture_id == fixture_id,
                MatchModel.active_status == True
            ).order_by(MatchModel.match_number.desc()).first()

            if last_match and last_match.status == 'bet':
                logger.info(f"Last match in fixture {fixture_id} is in bet status - new matches will be in bet status")
                return 'bet'

            # Default status
            return 'scheduled'

        except Exception as e:
            logger.error(f"Failed to determine new match status: {e}")
            return 'scheduled'  # Default fallback


    def _cleanup_previous_match_extractions(self):
        """Clean up all previous unzipped match directories from temporary location"""
        try:
            import tempfile
            from pathlib import Path
            import shutil

            temp_base = Path(tempfile.gettempdir())
            logger.info(f"DEBUG: Cleaning up previous match extractions in: {temp_base}")

            # Find all directories matching the pattern match_*_*
            match_dirs = list(temp_base.glob("match_*_*"))
            logger.info(f"DEBUG: Found {len(match_dirs)} match extraction directories to clean up")

            cleaned_count = 0
            for match_dir in match_dirs:
                try:
                    if match_dir.is_dir():
                        logger.debug(f"DEBUG: Removing match extraction directory: {match_dir}")
                        shutil.rmtree(match_dir)
                        cleaned_count += 1
                except Exception as dir_error:
                    logger.warning(f"DEBUG: Failed to remove directory {match_dir}: {dir_error}")

            if cleaned_count > 0:
                logger.info(f"DEBUG: Successfully cleaned up {cleaned_count} previous match extraction directories")
            else:
                logger.debug("DEBUG: No previous match extraction directories found to clean up")

        except Exception as e:
            logger.error(f"DEBUG: Failed to cleanup previous match extractions: {e}")

    def _handle_zip_extraction_failure(self, match_id: int, fixture_id: str):
        """Handle ZIP extraction failure by simulating PLAY_VIDEO_RESULTS_DONE behavior"""
        try:
            logger.info(f"DEBUG: Handling ZIP extraction failure for match {match_id}, fixture {fixture_id}")

            # Use fallback result selection since ZIP extraction failed
            fallback_result = self._fallback_result_selection()
            logger.info(f"DEBUG: Using fallback result '{fallback_result}' for failed ZIP extraction")

            # Set match status to 'done' and save result (same as _handle_play_video_result_done)
            self._set_match_status_and_result(match_id, 'done', fallback_result)

            # CRITICAL FIX: Update bet results since ZIP extraction failed before _update_bet_results was called
            # This ensures all pending bets are resolved even when ZIP extraction fails
            logger.info(f"🔧 [ZIP FAILURE FIX] Updating bet results for match {match_id} with fallback result '{fallback_result}'")
            try:
                session = self.db_manager.get_session()
                try:
                    # Get UNDER/OVER result if available
                    match = session.query(MatchModel).filter_by(id=match_id).first()
                    under_over_result = match.under_over_result if match else None
                    
                    # Update all bets - mark all as lost since we can't determine winners without proper extraction
                    # This is a conservative approach - all bets lose when ZIP extraction fails
                    pending_bets = session.query(BetDetailModel).filter(
                        BetDetailModel.match_id == match_id,
                        BetDetailModel.result == 'pending'
                    ).all()
                    
                    logger.info(f"🔧 [ZIP FAILURE FIX] Found {len(pending_bets)} pending bets to resolve")
                    
                    for bet in pending_bets:
                        bet.result = 'lost'
                        logger.info(f"🔧 [ZIP FAILURE FIX] Set bet {bet.id} to lost (ZIP extraction failure)")
                    
                    session.commit()
                    logger.info(f"🔧 [ZIP FAILURE FIX] Resolved {len(pending_bets)} bets for match {match_id}")
                    
                finally:
                    session.close()
            except Exception as bet_e:
                logger.error(f"🔧 [ZIP FAILURE FIX] Failed to update bets: {bet_e}")
                # Fallback to safety net
                self._ensure_all_bets_resolved(match_id, fallback_result)

            # Send MATCH_DONE message
            self._send_match_done(fixture_id, match_id, fallback_result)

            # Send NEXT_MATCH message to advance to next match
            self._send_next_match(fixture_id, match_id)

            logger.info(f"DEBUG: ZIP extraction failure handled - match {match_id} completed with fallback result '{fallback_result}'")

        except Exception as e:
            logger.error(f"DEBUG: Failed to handle ZIP extraction failure for match {match_id}: {e}")

    def _cleanup(self):
        """Perform cleanup operations"""
        try:
            logger.info("GamesThread performing cleanup...")

            # Reset state
            self.game_active = False
            self.current_fixture_id = None

            # Send final status
            final_status = MessageBuilder.system_status(
                sender=self.name,
                status="shutdown",
                details={
                    "component": "games_thread",
                    "cleanup_completed": True
                }
            )
            self.message_bus.publish(final_status)

            logger.info("GamesThread cleanup completed")

        except Exception as e:
            logger.error(f"GamesThread cleanup error: {e}")
