<?php
/**
 * DBA GeoBot Scheduler — Isolated WP-Cron for country investment blogs
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  ZERO CONFLICT POLICY — Each country gets its own cron hook:
 *    France:   dba_geo_france_daily_generation    (07:00 UTC / 09:00 Paris)
 *    Italy:    dba_geo_italy_daily_generation     (07:00 UTC / 09:00 Rome)
 *    Spain:    dba_geo_spain_daily_generation     (07:00 UTC / 09:00 Madrid)
 *    Portugal: dba_geo_portugal_daily_generation  (09:00 UTC / 09:00 Lisbon)
 *
 *  Core Damalion: dba_daily_generation           (06:00 UTC)
 *  PE NewsBot:    dba_pe_daily_generation         (15:00 UTC)
 *  → Minimum 1-hour gap between any two modules.
 * ═══════════════════════════════════════════════════════════════════════
 *
 * Default posting days: Saturday + Sunday (per France spec).
 * Posts staggered by 3 minutes per post within a batch.
 *
 * @since 2.8.0
 */

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

class DBA_GeoBot_Scheduler {

    private static $instance = null;

    public static function get_instance() {
        if ( null === self::$instance ) self::$instance = new self();
        return self::$instance;
    }

    private function __construct() {
        // Register cron callbacks for ALL countries
        foreach ( DBA_GeoBot_Registry::get_countries() as $key => $config ) {
            add_action( $config['cron_hook'], function() use ( $key ) {
                $this->run_generation( $key );
            } );
        }

        // Self-healing: catch missed runs on every page load
        add_action( 'init', array( $this, 'maybe_catch_up' ), 99 );

        // Re-register missing crons on admin load
        add_action( 'admin_init', array( __CLASS__, 'ensure_scheduled' ) );
    }

    /* =================================================================
     *  ACTIVATION / DEACTIVATION — Called from main plugin hooks
     * ================================================================= */

    /**
     * Schedule crons for all registered countries.
     */
    public static function activate() {
        $base_offset = 0;

        foreach ( DBA_GeoBot_Registry::get_countries() as $key => $config ) {
            if ( ! wp_next_scheduled( $config['cron_hook'] ) ) {
                // Stagger countries by 15 minutes to avoid API contention
                $utc_hour = $config['cron_hour_utc'] ?? 7;
                $next_run = strtotime( "tomorrow {$utc_hour}:00:00 UTC" ) + ( $base_offset * 900 );

                wp_schedule_event( $next_run, 'daily', $config['cron_hook'] );
                error_log( "DBA GeoBot [{$key}]: Cron activated — next run at "
                    . gmdate( 'Y-m-d H:i:s', $next_run ) . ' UTC' );
            }
            $base_offset++;
        }
    }

    /**
     * Remove all GeoBot crons.
     */
    public static function deactivate() {
        foreach ( DBA_GeoBot_Registry::get_countries() as $key => $config ) {
            $timestamp = wp_next_scheduled( $config['cron_hook'] );
            if ( $timestamp ) {
                wp_unschedule_event( $timestamp, $config['cron_hook'] );
                error_log( "DBA GeoBot [{$key}]: Cron deactivated" );
            }
        }
    }

    /* =================================================================
     *  DAILY GENERATION — Entry point from WP-Cron
     * ================================================================= */

    public function run_generation( string $country_key ) {

        $config   = DBA_GeoBot_Registry::get_country( $country_key );
        if ( ! $config ) return;

        $settings = get_option( $config['settings_option'], array() );

        // Master switch
        if ( empty( $settings['enabled'] ) ) {
            error_log( "DBA GeoBot [{$country_key}]: DISABLED — skipping" );
            return;
        }

        // Check if today is a posting day
        $posting_days = $settings['posting_days'] ?? $config['default_posting_days'];

        try {
            $tz    = new DateTimeZone( $config['timezone'] );
            $now   = new DateTime( 'now', $tz );
            $today = strtolower( $now->format( 'l' ) );
            $date_key = $now->format( 'Y-m-d' );
        } catch ( Exception $e ) {
            $today    = strtolower( date( 'l' ) );
            $date_key = date( 'Y-m-d' );
        }

        if ( ! in_array( $today, $posting_days, true ) ) {
            error_log( "DBA GeoBot [{$country_key}]: Today ({$today}) is not a posting day — skipping" );
            return;
        }

        // Daily lock: prevent double-fire (cron + catch-up)
        $lock_key = 'dba_geobot_ran_' . $country_key . '_' . $date_key;
        if ( get_transient( $lock_key ) ) {
            error_log( "DBA GeoBot [{$country_key}]: Already ran today ({$date_key}). Skipping." );
            return;
        }
        set_transient( $lock_key, true, DAY_IN_SECONDS );

        error_log( "DBA GeoBot [{$country_key}]: Starting generation for {$today}" );

        // v2.15.0: Global conflict lock — prevents overlap with ALL modules (replaces core-only check)
        if ( class_exists( 'DBA_Conflict_Center' ) && ! DBA_Conflict_Center::acquire( 'geo_' . $country_key, $config['cron_hook'] ) ) {
            delete_transient( $lock_key ); // Clear daily lock so retry works
            error_log( "DBA GeoBot [{$country_key}]: Another module running — rescheduled +10min" );
            return;
        }

        @set_time_limit( 300 );
        @ini_set( 'memory_limit', '256M' );

        // Run the generator
        $generator = DBA_GeoBot_Generator::get_instance();
        $results   = $generator->run_batch( $country_key );

        if ( empty( $results['created'] ) && ! empty( $results['failed'] ) ) {
            // Clear lock on total failure so catch-up can retry on next page load
            delete_transient( $lock_key );
            error_log( "DBA GeoBot [{$country_key}]: Batch FAILED — lock cleared for retry" );
        }

        error_log( "DBA GeoBot [{$country_key}]: Batch complete — Created: "
            . ( $results['created'] ?? 0 ) . ' Failed: ' . ( $results['failed'] ?? 0 ) );

        // v2.15.0: Release global lock
        if ( class_exists( 'DBA_Conflict_Center' ) ) DBA_Conflict_Center::release( 'geo_' . $country_key );
    }

    /* =================================================================
     *  PUBLISH TIME HELPER
     * ================================================================= */

    /**
     * Get the publish datetime for today in the country's timezone.
     *
     * Uses the global stagger setting (dba_post_stagger_minutes) to space posts.
     *
     * @param string $country_key
     * @param int    $post_index  Post index in batch (0, 1, 2…) or legacy minute offset
     * @return array { 'post_date' => local, 'post_date_gmt' => UTC }
     */
    public static function get_publish_datetime( string $country_key, int $post_index = 0 ) {
        $config = DBA_GeoBot_Registry::get_country( $country_key );
        if ( ! $config ) {
            return array(
                'post_date'     => current_time( 'mysql' ),
                'post_date_gmt' => current_time( 'mysql', true ),
            );
        }

        // Read global stagger (default 20 min, range 5–60)
        $stagger = function_exists( 'dba_get_post_stagger_minutes' )
            ? dba_get_post_stagger_minutes() : 20;

        // Legacy compatibility: old generators pass $i * 3 (3-min intervals)
        if ( $post_index > 0 && $stagger > 5 && $post_index < $stagger ) {
            $post_index = (int) round( $post_index / 3 );
        }

        $offset_minutes = $post_index * $stagger;

        try {
            $tz  = new DateTimeZone( $config['timezone'] );
            $utc = new DateTimeZone( 'UTC' );

            $publish = new DateTime( 'now', $tz );
            $publish->setTime( $config['publish_hour'], $config['publish_min'], 0 );

            if ( $offset_minutes > 0 ) {
                $publish->modify( "+{$offset_minutes} minutes" );
            }

            $local = $publish->format( 'Y-m-d H:i:s' );
            $publish->setTimezone( $utc );
            $gmt = $publish->format( 'Y-m-d H:i:s' );

            return array(
                'post_date'     => $local,
                'post_date_gmt' => $gmt,
            );
        } catch ( Exception $e ) {
            return array(
                'post_date'     => current_time( 'mysql' ),
                'post_date_gmt' => current_time( 'mysql', true ),
            );
        }
    }

    /* =================================================================
     *  INFO HELPERS — For admin display
     * ================================================================= */

    public static function get_next_run_info( string $country_key ) {
        $config = DBA_GeoBot_Registry::get_country( $country_key );
        if ( ! $config ) return 'Unknown country';

        $next = wp_next_scheduled( $config['cron_hook'] );
        if ( ! $next ) return 'Not scheduled';

        try {
            $dt = new DateTime( '@' . $next );
            $dt->setTimezone( new DateTimeZone( $config['timezone'] ) );
            return $dt->format( 'l, F j, Y \a\t H:i' ) . ' (' . $config['timezone'] . ')';
        } catch ( Exception $e ) {
            return gmdate( 'Y-m-d H:i:s', $next ) . ' UTC';
        }
    }

    public static function get_last_run_info( string $country_key ) {
        $config = DBA_GeoBot_Registry::get_country( $country_key );
        if ( ! $config ) return array();
        return get_option( $config['last_run_option'], array() );
    }

    public static function get_posting_days_string( string $country_key ) {
        $config   = DBA_GeoBot_Registry::get_country( $country_key );
        $settings = get_option( $config['settings_option'], array() );
        $days     = $settings['posting_days'] ?? $config['default_posting_days'];
        return ! empty( $days ) ? implode( ', ', array_map( 'ucfirst', $days ) ) : 'None';
    }

    /**
     * Check if core Damalion cron is still running.
     */
    public static function is_core_running() {
        $core = get_transient( 'dba_generation_progress' );
        if ( ! $core ) return false;
        $status  = $core['current_status'] ?? '';
        $updated = $core['updated'] ?? 0;
        return ( $status === 'running' && ( time() - $updated ) < 600 );
    }

    /* =================================================================
     *  SELF-HEALING CATCH-UP — runs on every page load (init hook)
     *
     *  For each enabled country: if today is a posting day and the
     *  daily lock is missing → WP-Cron missed it → fire now via shutdown.
     *  Lightweight: exits fast on non-posting days.
     * ================================================================= */

    public function maybe_catch_up() {
        if ( ! class_exists( 'DBA_GeoBot_Registry' ) ) return;

        // Only run catch-up once per request
        static $checked = false;
        if ( $checked ) return;
        $checked = true;

        $countries_to_run = array();

        foreach ( DBA_GeoBot_Registry::get_countries() as $key => $config ) {
            $settings = get_option( $config['settings_option'], array() );
            if ( empty( $settings['enabled'] ) ) continue;

            $posting_days = $settings['posting_days'] ?? $config['default_posting_days'];

            try {
                $tz  = new DateTimeZone( $config['timezone'] );
                $now = new DateTime( 'now', $tz );
                $today    = strtolower( $now->format( 'l' ) );
                $hour     = (int) $now->format( 'G' );
                $date_key = $now->format( 'Y-m-d' );
            } catch ( Exception $e ) {
                continue;
            }

            // Not a posting day → skip
            if ( ! in_array( $today, $posting_days, true ) ) continue;

            // Too early (before scheduled hour) → skip
            $target_hour = $config['publish_hour'] ?? 8;
            if ( $hour < $target_hour ) continue;

            // Already ran today → skip
            $lock_key = 'dba_geobot_ran_' . $key . '_' . $date_key;
            if ( get_transient( $lock_key ) ) continue;

            // This country was missed → queue it (but only ONE per request to avoid pile-up)
            $countries_to_run[] = $key;
            error_log( "DBA GeoBot [{$key}]: Catch-up triggered — {$today} {$now->format('H:i')} — WP-Cron missed, will run on shutdown." );
            break; // FIX v2.8.1: Only run ONE catch-up per request to prevent resource exhaustion
        }

        if ( ! empty( $countries_to_run ) ) {
            // Store in a class property and run on shutdown (don't block the page)
            $this->_catchup_countries = $countries_to_run;
            add_action( 'shutdown', array( $this, 'run_catchup' ) );
        }
    }

    /** @var array Countries queued for catch-up */
    private $_catchup_countries = array();

    /**
     * Catch-up execution (runs in shutdown hook — doesn't slow the page).
     */
    public function run_catchup() {
        ignore_user_abort( true );
        if ( function_exists( 'set_time_limit' ) ) {
            @set_time_limit( 300 );
        }

        foreach ( $this->_catchup_countries as $country_key ) {
            error_log( "DBA GeoBot [{$country_key}]: Running catch-up generation..." );
            $this->run_generation( $country_key );
        }
    }

    /* =================================================================
     *  ENSURE SCHEDULED — Re-register missing crons on admin load
     * ================================================================= */

    public static function ensure_scheduled() {
        if ( ! class_exists( 'DBA_GeoBot_Registry' ) ) return;

        $any_missing = false;
        foreach ( DBA_GeoBot_Registry::get_countries() as $key => $config ) {
            $settings = get_option( $config['settings_option'], array() );
            if ( empty( $settings['enabled'] ) ) continue;

            if ( ! wp_next_scheduled( $config['cron_hook'] ) ) {
                $any_missing = true;
                error_log( "DBA GeoBot [{$key}]: Cron missing — rescheduling." );
            }
        }

        if ( $any_missing ) {
            self::activate();
        }
    }
}
