<?php
/**
 * DBA Conflict Center — Centralized Generation Lock & Timing Auditor
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  PURPOSE: Prevent simultaneous OpenAI API calls across ALL modules
 * ═══════════════════════════════════════════════════════════════════════
 *
 *  PROBLEM (before this class):
 *    - Core checks NOTHING before starting
 *    - LATAM checks Core + PE, but NOT GeoBot or LuxStrat
 *    - GeoBot checks Core only, NOT LATAM or LuxStrat
 *    - LuxStrat checks NOTHING
 *    - Live Sports runs independently every 5 minutes
 *    → On Saturday 06:00 UTC: Core (5 posts) + LATAM (3 posts) = 8 simultaneous API batches
 *    → On Saturday 07:00 UTC: GeoBot France + LuxStrat FR pile on while Core may still run
 *    → Result: OpenAI rate limits, PHP worker exhaustion, site crash
 *
 *  SOLUTION: Single global lock + queue system
 *    - Every module calls DBA_Conflict_Center::acquire() before ANY OpenAI call
 *    - If another module is running → reschedule +10 min (no sleep, worker freed)
 *    - Provides admin dashboard showing the full timing map + collision warnings
 *    - Automatic stale lock cleanup (15-min TTL)
 *
 *  INTEGRATION (one-line change per scheduler):
 *    // At the START of any generation method, BEFORE OpenAI calls:
 *    if ( ! DBA_Conflict_Center::acquire( 'core', 'dba_daily_generation' ) ) return;
 *    // ... do generation ...
 *    DBA_Conflict_Center::release( 'core' );
 *
 * @package DamalionBlogAI
 * @since   2.15.0
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class DBA_Conflict_Center {

    // ═══════════════════════════════════════════════════════════════
    //  GLOBAL LOCK — Only ONE module can generate at a time
    // ═══════════════════════════════════════════════════════════════
    const LOCK_OPTION   = 'dba_global_generation_lock';
    const LOCK_TTL      = 900;  // 15 minutes max — auto-expires if module crashes
    const RETRY_DELAY   = 600;  // 10 minutes before retry
    const HISTORY_OPTION = 'dba_conflict_center_history';
    const MAX_HISTORY    = 50;

    /**
     * All known modules and their identifiers.
     * Used for the admin timing dashboard.
     */
    const MODULES = array(
        'core'          => 'Core Damalion (EU)',
        'pe_newsbot'    => 'PE NewsBot (US)',
        'geo_france'    => 'GeoBot France',
        'geo_italy'     => 'GeoBot Italy',
        'geo_spain'     => 'GeoBot Spain',
        'geo_portugal'  => 'GeoBot Portugal',
        'latam'         => 'LATAM GeoBot',
        'luxstrat'      => 'LuxStrat EN',
        'luxstrat_fr'   => 'LuxStrat FR',
        'luxstrat_it'   => 'LuxStrat IT',
        'live_sports'   => 'Live Sports AI',
        'startup_hub'   => 'Startup Hub (US)',
        'eeurope'       => 'Eastern Europe GeoBot',
        'kaizen_batch'  => 'Kaizen Batch Fix',
        'comfisc_fr'    => 'Comfisc FR (Comptabilité)',
    );

    /* =================================================================
     *  ACQUIRE LOCK — Call before ANY OpenAI API calls
     *
     *  Returns TRUE if the lock was acquired (safe to proceed).
     *  Returns FALSE if another module is running (caller should bail).
     *
     *  When returning FALSE, this method automatically reschedules the
     *  caller's cron hook for +10 minutes so nothing is lost.
     *
     *  @param string $module_id    One of the MODULES keys (e.g., 'core', 'latam')
     *  @param string $cron_hook    The WP-Cron hook to reschedule if blocked (optional)
     *  @param array  $cron_args    Arguments for the cron hook (optional)
     *  @return bool
     * ================================================================= */
    public static function acquire( string $module_id, string $cron_hook = '', array $cron_args = array() ) : bool {

        // 1. Read current lock
        $lock = get_option( self::LOCK_OPTION, null );

        // 2. Check if lock exists and is still valid (not stale)
        if ( $lock && is_array( $lock ) ) {
            $age = time() - ( $lock['timestamp'] ?? 0 );

            if ( $age < self::LOCK_TTL ) {
                // Lock is active — another module is running
                $blocker = $lock['module'] ?? 'unknown';
                $blocker_label = self::MODULES[ $blocker ] ?? $blocker;

                error_log( "DBA Conflict Center: [{$module_id}] BLOCKED by [{$blocker}] "
                    . "(running for {$age}s). " . ( $cron_hook ? "Rescheduling {$cron_hook} +10min." : 'No reschedule.' ) );

                // Log to history
                self::log_event( $module_id, 'blocked', "Blocked by {$blocker_label} (running {$age}s)" );

                // Reschedule the caller's cron hook if provided
                if ( $cron_hook ) {
                    $retry_time = time() + self::RETRY_DELAY;
                    if ( ! empty( $cron_args ) ) {
                        wp_schedule_single_event( $retry_time, $cron_hook, $cron_args );
                    } else {
                        wp_schedule_single_event( $retry_time, $cron_hook );
                    }
                    error_log( "DBA Conflict Center: [{$module_id}] rescheduled to "
                        . gmdate( 'H:i:s', $retry_time ) . ' UTC (PHP worker freed)' );
                }

                return false; // BLOCKED — caller should return immediately
            }

            // Lock is stale (older than 15 min) — previous module crashed or timed out
            error_log( "DBA Conflict Center: Stale lock from [{$lock['module']}] detected ({$age}s old). Clearing." );
            self::log_event( $lock['module'] ?? 'unknown', 'stale_cleared', "Stale lock cleared after {$age}s" );
        }

        // 3. Acquire the lock
        $lock_data = array(
            'module'    => $module_id,
            'label'     => self::MODULES[ $module_id ] ?? $module_id,
            'timestamp' => time(),
            'pid'       => getmypid(),
        );

        update_option( self::LOCK_OPTION, $lock_data, false ); // false = don't autoload

        error_log( "DBA Conflict Center: [{$module_id}] acquired global lock at "
            . gmdate( 'H:i:s' ) . ' UTC' );

        self::log_event( $module_id, 'acquired', 'Lock acquired' );

        return true; // SAFE — caller can proceed with OpenAI calls
    }

    /* =================================================================
     *  RELEASE LOCK — Call AFTER generation is complete
     *
     *  @param string $module_id  Same ID used in acquire()
     * ================================================================= */
    public static function release( string $module_id ) {
        $lock = get_option( self::LOCK_OPTION, null );

        // Only release if WE hold the lock (prevents accidental release by wrong module)
        if ( $lock && is_array( $lock ) && ( $lock['module'] ?? '' ) === $module_id ) {
            delete_option( self::LOCK_OPTION );
            $duration = time() - ( $lock['timestamp'] ?? time() );
            error_log( "DBA Conflict Center: [{$module_id}] released lock after {$duration}s" );
            self::log_event( $module_id, 'released', "Released after {$duration}s" );
        } else {
            // Lock doesn't belong to us — log but don't delete
            error_log( "DBA Conflict Center: [{$module_id}] tried to release, but lock belongs to ["
                . ( $lock['module'] ?? 'none' ) . ']' );
        }
    }

    /* =================================================================
     *  FORCE RELEASE — Admin emergency button
     * ================================================================= */
    public static function force_release() {
        $lock = get_option( self::LOCK_OPTION, null );
        delete_option( self::LOCK_OPTION );
        if ( $lock ) {
            self::log_event( 'admin', 'force_released', 'Force-released lock from [' . ( $lock['module'] ?? '?' ) . ']' );
            error_log( 'DBA Conflict Center: Lock FORCE RELEASED by admin' );
        }
        return $lock;
    }

    /* =================================================================
     *  STATUS — Check current lock state
     * ================================================================= */
    public static function get_status() {
        $lock = get_option( self::LOCK_OPTION, null );

        if ( ! $lock || ! is_array( $lock ) ) {
            return array( 'locked' => false, 'module' => null, 'label' => null, 'duration' => 0 );
        }

        $age = time() - ( $lock['timestamp'] ?? 0 );

        // Auto-clear stale locks on status check
        if ( $age >= self::LOCK_TTL ) {
            delete_option( self::LOCK_OPTION );
            return array( 'locked' => false, 'module' => null, 'label' => null, 'duration' => 0, 'was_stale' => true );
        }

        return array(
            'locked'   => true,
            'module'   => $lock['module'] ?? 'unknown',
            'label'    => $lock['label'] ?? 'Unknown Module',
            'duration' => $age,
            'pid'      => $lock['pid'] ?? 0,
        );
    }

    /**
     * Quick boolean check — is ANY module currently generating?
     */
    public static function is_any_running() : bool {
        $status = self::get_status();
        return ! empty( $status['locked'] );
    }

    /* =================================================================
     *  TIMING MAP — Full schedule for admin dashboard
     *
     *  Returns an array of all modules with their:
     *    - Cron UTC time
     *    - Active days
     *    - Posts per run
     *    - Estimated OpenAI calls
     *    - Collision warnings
     * ================================================================= */
    public static function get_timing_map() : array {
        $map = array();

        // ── Core Damalion ──
        $core_settings = get_option( 'dba_schedule_settings', array() );
        $core_days = $core_settings['posting_days'] ?? array( 'monday', 'wednesday', 'saturday' );
        $core_posts = $core_settings['posts_per_run'] ?? 2;
        $map['core'] = array(
            'label'       => 'Core Damalion (EU)',
            'cron_utc'    => '06:00',
            'timezone'    => $core_settings['timezone'] ?? 'Europe/Luxembourg',
            'days'        => $core_days,
            'posts'       => (int) $core_posts,
            'api_calls'   => (int) $core_posts * 2, // text + image per post
            'enabled'     => ! empty( $core_settings['enabled'] ),
            'next_run'    => wp_next_scheduled( 'dba_daily_generation' ),
            'has_lock'    => false, // Core currently has NO lock checking
            'cron_hook'   => 'dba_daily_generation',
        );

        // ── PE NewsBot ──
        $pe_settings = get_option( 'dba_pe_settings', array() );
        $pe_days = $pe_settings['posting_days'] ?? array( 'monday', 'wednesday', 'friday' );
        $map['pe_newsbot'] = array(
            'label'       => 'PE NewsBot (US)',
            'cron_utc'    => '15:00',
            'timezone'    => 'America/New_York',
            'days'        => $pe_days,
            'posts'       => 3,
            'api_calls'   => 8, // 3 individual + 1 roundup, each with text + image
            'enabled'     => ! empty( $pe_settings['enabled'] ),
            'next_run'    => wp_next_scheduled( 'dba_pe_daily_generation' ),
            'has_lock'    => true, // Checks core only
            'cron_hook'   => 'dba_pe_daily_generation',
        );

        // ── GeoBot countries ──
        if ( class_exists( 'DBA_GeoBot_Registry' ) ) {
            foreach ( DBA_GeoBot_Registry::get_countries() as $key => $config ) {
                $geo_settings = get_option( $config['settings_option'] ?? '', array() );
                $geo_days = $geo_settings['posting_days'] ?? ( $config['default_posting_days'] ?? array( 'saturday', 'sunday' ) );
                $map[ 'geo_' . $key ] = array(
                    'label'       => 'GeoBot ' . ucfirst( $key ),
                    'cron_utc'    => sprintf( '%02d:00', $config['cron_hour_utc'] ?? 7 ),
                    'timezone'    => $config['timezone'] ?? 'Europe/Paris',
                    'days'        => $geo_days,
                    'posts'       => 2,
                    'api_calls'   => 4,
                    'enabled'     => ! empty( $geo_settings['enabled'] ),
                    'next_run'    => wp_next_scheduled( $config['cron_hook'] ?? '' ),
                    'has_lock'    => true, // Checks core only
                    'cron_hook'   => $config['cron_hook'] ?? '',
                );
            }
        }

        // ── LATAM ──
        $latam_settings = get_option( 'dba_latam_settings', array() );
        $latam_posts    = max( 1, absint( $latam_settings['posts_per_run'] ?? 3 ) );
        $latam_days     = ! empty( $latam_settings['posting_days'] ) ? (array) $latam_settings['posting_days'] : array( 'saturday', 'sunday' );
        $latam_cet_hour = isset( $latam_settings['cron_hour'] ) ? intval( $latam_settings['cron_hour'] ) : 7;
        $latam_utc_hour = max( 0, $latam_cet_hour - 1 ); // CET→UTC approx (winter)

        // Find next scheduled run across ALL day-args
        $latam_next = false;
        foreach ( $latam_days as $latam_day ) {
            $ts = wp_next_scheduled( 'dba_latam_daily_run', array( $latam_day ) );
            if ( $ts && ( ! $latam_next || $ts < $latam_next ) ) {
                $latam_next = $ts;
            }
        }
        // Also check no-args (Conflict Center reschedules without args)
        $ts_no_args = wp_next_scheduled( 'dba_latam_daily_run' );
        if ( $ts_no_args && ( ! $latam_next || $ts_no_args < $latam_next ) ) {
            $latam_next = $ts_no_args;
        }

        $map['latam'] = array(
            'label'       => 'LATAM GeoBot',
            'cron_utc'    => sprintf( '%02d:00', $latam_utc_hour ),
            'timezone'    => 'Europe/Luxembourg',
            'days'        => $latam_days,
            'posts'       => $latam_posts,
            'api_calls'   => $latam_posts * 2,
            'enabled'     => ! empty( $latam_settings['enabled'] ),
            'next_run'    => $latam_next,
            'has_lock'    => true, // Uses Conflict Center global lock
            'cron_hook'   => 'dba_latam_daily_run',
        );

        // ── Eastern Europe GeoBot ──
        $ee_settings = get_option( 'dba_eeurope_settings', array() );
        $ee_posts    = max( 1, absint( $ee_settings['posts_per_run'] ?? 3 ) );
        $ee_days     = ! empty( $ee_settings['posting_days'] ) ? (array) $ee_settings['posting_days'] : array( 'thursday', 'friday' );
        $ee_cet_hour = isset( $ee_settings['cron_hour'] ) ? intval( $ee_settings['cron_hour'] ) : 9;
        $ee_utc_hour = max( 0, $ee_cet_hour - 1 ); // CET→UTC approx

        $ee_next = false;
        if ( class_exists( 'DBA_EEurope_Scheduler' ) ) {
            foreach ( $ee_days as $ee_day ) {
                $ts = wp_next_scheduled( 'dba_eeurope_daily_run', array( $ee_day ) );
                if ( $ts && ( ! $ee_next || $ts < $ee_next ) ) {
                    $ee_next = $ts;
                }
            }
            $ts_no_args = wp_next_scheduled( 'dba_eeurope_daily_run' );
            if ( $ts_no_args && ( ! $ee_next || $ts_no_args < $ee_next ) ) {
                $ee_next = $ts_no_args;
            }
        }

        $map['eeurope'] = array(
            'label'       => 'Eastern Europe GeoBot',
            'cron_utc'    => sprintf( '%02d:00', $ee_utc_hour ),
            'timezone'    => 'Europe/Luxembourg',
            'days'        => $ee_days,
            'posts'       => $ee_posts,
            'api_calls'   => $ee_posts * 2,
            'enabled'     => ! empty( $ee_settings['enabled'] ),
            'next_run'    => $ee_next,
            'has_lock'    => true, // Uses Conflict Center global lock
            'cron_hook'   => 'dba_eeurope_daily_run',
        );

        // ── LuxStrat EN ──
        $lux_settings = get_option( 'dba_luxstrat_settings', array() );
        $map['luxstrat'] = array(
            'label'       => 'LuxStrat EN',
            'cron_utc'    => '04:00', // 05:00 CET ≈ 04:00 UTC (winter)
            'timezone'    => 'Europe/Luxembourg',
            'days'        => array( 'sunday' ),
            'posts'       => 1,
            'api_calls'   => 2,
            'enabled'     => ! empty( $lux_settings['enabled'] ),
            'next_run'    => wp_next_scheduled( 'dba_luxstrat_cron' ),
            'has_lock'    => false, // Checks NOTHING
            'cron_hook'   => 'dba_luxstrat_cron',
        );

        // ── LuxStrat Intl (FR, IT, etc.) ──
        if ( class_exists( 'DBA_LuxStrat_Intl_Loader' ) ) {
            $intl_languages = DBA_LuxStrat_Intl_Loader::get_languages();
            foreach ( $intl_languages as $lang => $lang_config ) {
                $lang_settings = get_option( 'dba_luxstrat_' . $lang . '_settings', array() );
                $sched_hour = $lang_config['schedule']['hour'] ?? 7;
                $sched_days_n = $lang_config['schedule']['days'] ?? array();
                $day_map = array( 1 => 'monday', 2 => 'tuesday', 3 => 'wednesday', 4 => 'thursday', 5 => 'friday', 6 => 'saturday', 7 => 'sunday' );
                $sched_days = array_map( function( $n ) use ( $day_map ) { return $day_map[ $n ] ?? '?'; }, $sched_days_n );

                $map[ 'luxstrat_' . $lang ] = array(
                    'label'       => 'LuxStrat ' . strtoupper( $lang ),
                    'cron_utc'    => sprintf( '%02d:00', max( 0, $sched_hour - 1 ) ), // CET→UTC approx
                    'timezone'    => 'Europe/Luxembourg',
                    'days'        => $sched_days,
                    'posts'       => 1,
                    'api_calls'   => 2,
                    'enabled'     => ! empty( $lang_settings['enabled'] ),
                    'next_run'    => wp_next_scheduled( 'dba_luxstrat_' . $lang . '_cron' ),
                    'has_lock'    => false, // Checks NOTHING
                    'cron_hook'   => 'dba_luxstrat_' . $lang . '_cron',
                );
            }
        }

        // ── Comfisc (Comptabilité & Fiscalité) ──
        if ( class_exists( 'DBA_Comfisc_Loader' ) ) {
            $comfisc_languages = DBA_Comfisc_Loader::get_languages();
            foreach ( $comfisc_languages as $lang => $lang_config ) {
                $lang_settings = get_option( 'dba_comfisc_' . $lang . '_settings', array() );
                $sched_hour = $lang_config['schedule']['hour'] ?? 9;
                $sched_days_n = $lang_config['schedule']['days'] ?? array();
                $day_map = array( 1 => 'monday', 2 => 'tuesday', 3 => 'wednesday', 4 => 'thursday', 5 => 'friday', 6 => 'saturday', 7 => 'sunday' );
                $sched_days = array_map( function( $n ) use ( $day_map ) { return $day_map[ $n ] ?? '?'; }, $sched_days_n );

                $map[ 'comfisc_' . $lang ] = array(
                    'label'       => 'Comfisc ' . strtoupper( $lang ),
                    'cron_utc'    => sprintf( '%02d:00', max( 0, $sched_hour - 1 ) ), // CET→UTC approx
                    'timezone'    => 'Europe/Luxembourg',
                    'days'        => $sched_days,
                    'posts'       => 1,
                    'api_calls'   => 2,
                    'enabled'     => ! empty( $lang_settings['enabled'] ),
                    'next_run'    => wp_next_scheduled( 'dba_comfisc_' . $lang . '_cron' ),
                    'has_lock'    => true, // Uses Conflict Center global lock
                    'cron_hook'   => 'dba_comfisc_' . $lang . '_cron',
                );
            }
        }

        // ── Live Sports AI ──
        $map['live_sports'] = array(
            'label'       => 'Live Sports AI',
            'cron_utc'    => 'Every 5min',
            'timezone'    => 'UTC',
            'days'        => array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ),
            'posts'       => 1,
            'api_calls'   => 1,
            'enabled'     => (bool) wp_next_scheduled( 'lsa_enrich_pending_posts' ),
            'next_run'    => wp_next_scheduled( 'lsa_enrich_pending_posts' ),
            'has_lock'    => false, // Checks NOTHING
            'cron_hook'   => 'lsa_enrich_pending_posts',
        );

        return $map;
    }

    /* =================================================================
     *  COLLISION DETECTION — Find overlapping time slots
     *
     *  Returns warnings for any day where two modules could fire
     *  within 30 minutes of each other.
     * ================================================================= */
    public static function detect_collisions() : array {
        $map = self::get_timing_map();
        $warnings = array();
        $days_of_week = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' );

        foreach ( $days_of_week as $day ) {
            $modules_on_day = array();

            foreach ( $map as $id => $info ) {
                if ( ! $info['enabled'] ) continue;
                if ( $info['cron_utc'] === 'Every 5min' ) continue; // Skip continuous crons
                if ( in_array( $day, $info['days'], true ) ) {
                    // Parse UTC hour
                    $parts = explode( ':', $info['cron_utc'] );
                    $utc_minutes = ( (int) $parts[0] ) * 60 + ( (int) ( $parts[1] ?? 0 ) );
                    $modules_on_day[] = array(
                        'id'          => $id,
                        'label'       => $info['label'],
                        'utc_minutes' => $utc_minutes,
                        'api_calls'   => $info['api_calls'],
                        'has_lock'    => $info['has_lock'],
                    );
                }
            }

            // Sort by UTC time
            usort( $modules_on_day, function( $a, $b ) {
                return $a['utc_minutes'] - $b['utc_minutes'];
            } );

            // Check for collisions (< 30 min gap)
            for ( $i = 0; $i < count( $modules_on_day ) - 1; $i++ ) {
                $current = $modules_on_day[ $i ];
                $next    = $modules_on_day[ $i + 1 ];
                $gap     = $next['utc_minutes'] - $current['utc_minutes'];

                if ( $gap < 30 ) {
                    $severity = ( $gap === 0 ) ? 'critical' : 'warning';
                    $total_api = $current['api_calls'] + $next['api_calls'];

                    $warnings[] = array(
                        'day'       => ucfirst( $day ),
                        'severity'  => $severity,
                        'module_a'  => $current['label'],
                        'module_b'  => $next['label'],
                        'time_a'    => sprintf( '%02d:%02d', intdiv( $current['utc_minutes'], 60 ), $current['utc_minutes'] % 60 ),
                        'time_b'    => sprintf( '%02d:%02d', intdiv( $next['utc_minutes'], 60 ), $next['utc_minutes'] % 60 ),
                        'gap_min'   => $gap,
                        'total_api' => $total_api,
                        'message'   => $gap === 0
                            ? "⛔ SAME TIME: {$current['label']} and {$next['label']} both fire at "
                              . sprintf( '%02d:%02d', intdiv( $current['utc_minutes'], 60 ), $current['utc_minutes'] % 60 )
                              . " UTC on {$day} ({$total_api} API calls simultaneously)"
                            : "⚠️ {$gap}-min gap: {$current['label']} → {$next['label']} on {$day} ({$total_api} API calls)",
                    );
                }
            }

            // Check total API load for the day
            $total_day_calls = array_sum( array_column( $modules_on_day, 'api_calls' ) );
            if ( $total_day_calls > 20 ) {
                $warnings[] = array(
                    'day'       => ucfirst( $day ),
                    'severity'  => 'info',
                    'message'   => "📊 Heavy day: {$total_day_calls} total OpenAI API calls on " . ucfirst( $day )
                        . ' (' . count( $modules_on_day ) . ' modules)',
                    'total_api' => $total_day_calls,
                );
            }
        }

        return $warnings;
    }

    /* =================================================================
     *  EVENT HISTORY — For admin dashboard
     * ================================================================= */
    private static function log_event( string $module, string $event, string $detail = '' ) {
        $history = get_option( self::HISTORY_OPTION, array() );
        if ( ! is_array( $history ) ) $history = array();

        array_unshift( $history, array(
            'time'    => gmdate( 'Y-m-d H:i:s' ) . ' UTC',
            'module'  => $module,
            'label'   => self::MODULES[ $module ] ?? $module,
            'event'   => $event,
            'detail'  => $detail,
        ) );

        // Keep only last N events
        $history = array_slice( $history, 0, self::MAX_HISTORY );
        update_option( self::HISTORY_OPTION, $history, false );
    }

    public static function get_history() : array {
        return get_option( self::HISTORY_OPTION, array() );
    }

    public static function clear_history() {
        delete_option( self::HISTORY_OPTION );
    }

    /* =================================================================
     *  ADMIN INFO — Human-readable summary
     * ================================================================= */

    /**
     * Get a full report for the admin dashboard.
     */
    public static function get_dashboard_data() : array {
        return array(
            'status'     => self::get_status(),
            'timing_map' => self::get_timing_map(),
            'collisions' => self::detect_collisions(),
            'history'    => self::get_history(),
        );
    }

    /**
     * Summary string for the main admin page header.
     */
    public static function get_status_badge() : string {
        $status = self::get_status();
        if ( $status['locked'] ) {
            return '🔴 ' . ( $status['label'] ?? 'Unknown' ) . ' running (' . $status['duration'] . 's)';
        }

        $collisions = self::detect_collisions();
        $critical = array_filter( $collisions, function( $w ) { return ( $w['severity'] ?? '' ) === 'critical'; } );

        if ( ! empty( $critical ) ) {
            return '⚠️ ' . count( $critical ) . ' timing collision(s) detected';
        }

        return '✅ No conflicts';
    }
}
