<?php
/**
 * DBA Startup Hub Scheduler
 *
 * Fires on configured posting days at 09:00 Eastern time.
 * Supports configurable posting days (Mon-Fri default).
 *
 * TWO-LAYER reliability:
 *   1. Real daily WP-Cron event (best case)
 *   2. Self-healing fallback on every page load (init hook):
 *      checks if today's run was missed and fires it immediately.
 *
 * Daily transient lock prevents double-execution.
 *
 * @since 2.16.0
 * @updated 2.17.0 — Added get_posting_days_string(), get_next_run_info(),
 *                    and self-bootstrap via plugins_loaded hook.
 * @updated 2.17.1 — Fixed double-lock bug: Conflict Center lock is now only
 *                    acquired inside Generator::run_batch(), not in the
 *                    scheduler's run() or run_catchup(). This prevents
 *                    run_batch() from blocking itself when called by the scheduler.
 */

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

class DBA_Startup_Hub_Scheduler {

    private static $instance = null;

    const CRON_HOOK   = 'dba_startup_hub_daily_generation';
    const TIMEZONE    = 'America/New_York';
    const TARGET_HOUR = 9;  // 09:00 ET

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

    private function __construct() {
        add_action( self::CRON_HOOK, array( $this, 'run' ) );

        // Self-healing: check on every page load if we missed today's run
        add_action( 'init', array( $this, 'maybe_catch_up' ), 99 );

        // Re-register cron if missing (admin only)
        add_action( 'admin_init', array( __CLASS__, 'ensure_scheduled' ) );
    }

    /* ──────────────────────────────────────────────
     *  ACTIVATE / DEACTIVATE
     * ────────────────────────────────────────────── */

    public static function activate() {
        if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
            $next = self::next_occurrence();
            wp_schedule_event( $next, 'daily', self::CRON_HOOK );
            error_log( 'DBA StartupHub: Cron scheduled — next: ' . gmdate( 'Y-m-d H:i:s', $next ) . ' UTC' );
        }
    }

    public static function deactivate() {
        wp_clear_scheduled_hook( self::CRON_HOOK );
        error_log( 'DBA StartupHub: Cron hook removed.' );
    }

    public static function ensure_scheduled() {
        $config   = DBA_Startup_Hub_Registry::get_config();
        $settings = get_option( $config['settings_option'], array() );
        if ( empty( $settings['enabled'] ) ) return;
        if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
            self::activate();
            error_log( 'DBA StartupHub: Cron was missing — rescheduled.' );
        }
    }

    /**
     * Calculate next occurrence of the target hour in Eastern time.
     *
     * @return int Unix timestamp.
     */
    private static function next_occurrence() {
        try {
            $tz  = new DateTimeZone( self::TIMEZONE );
            $now = new DateTime( 'now', $tz );

            // Today at target hour
            $target = clone $now;
            $target->setTime( self::TARGET_HOUR, 0, 0 );

            if ( $target > $now ) {
                return $target->getTimestamp();
            }

            // Tomorrow at target hour
            $target->modify( '+1 day' );
            return $target->getTimestamp();
        } catch ( \Exception $e ) {
            return time() + 3600;
        }
    }

    /* ──────────────────────────────────────────────
     *  SELF-HEALING CATCH-UP
     * ────────────────────────────────────────────── */

    public function maybe_catch_up() {
        $config   = DBA_Startup_Hub_Registry::get_config();
        $settings = get_option( $config['settings_option'], array() );
        if ( empty( $settings['enabled'] ) ) return;

        try {
            $tz  = new DateTimeZone( self::TIMEZONE );
            $now = new DateTime( 'now', $tz );
        } catch ( \Exception $e ) {
            return;
        }

        // Quick bail: before target hour
        if ( (int) $now->format( 'G' ) < self::TARGET_HOUR ) return;

        // Quick bail: not a posting day
        $posting_days = $settings['posting_days'] ?? array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday' );
        $today_name   = strtolower( $now->format( 'l' ) );
        if ( ! in_array( $today_name, $posting_days, true ) ) return;

        // Quick bail: already ran today
        $lock_key = 'dba_startup_hub_ran_' . $now->format( 'Y-m-d' );
        if ( get_transient( $lock_key ) ) return;

        error_log( 'DBA StartupHub catch-up: ' . $now->format( 'l H:i' ) . ' ET — WP-Cron missed, running now.' );
        set_transient( $lock_key, true, DAY_IN_SECONDS );

        add_action( 'shutdown', array( $this, 'run_catchup' ) );
    }

    /**
     * Catch-up execution (called from shutdown hook).
     *
     * NOTE: Conflict Center locking is handled inside Generator::run_batch().
     * We do NOT acquire the lock here to avoid a double-lock deadlock.
     */
    public function run_catchup() {
        ignore_user_abort( true );
        if ( function_exists( 'set_time_limit' ) ) @set_time_limit( 600 );

        $generator = DBA_Startup_Hub_Generator::get_instance();
        $results   = $generator->run_batch();

        error_log( 'DBA StartupHub catch-up: Complete — created: ' . $results['created'] . ', failed: ' . $results['failed'] );

        // If run_batch was blocked by Conflict Center, clear today's lock so
        // the next page load can retry via maybe_catch_up().
        if ( $results['created'] === 0 && $results['failed'] === 0 ) {
            try {
                $tz  = new DateTimeZone( self::TIMEZONE );
                $now = new DateTime( 'now', $tz );
                $lock_key = 'dba_startup_hub_ran_' . $now->format( 'Y-m-d' );
                // Only clear if nothing was produced (i.e. blocked, not "0 posts configured")
                $config   = DBA_Startup_Hub_Registry::get_config();
                $settings = get_option( $config['settings_option'], array() );
                $target   = max( 1, intval( $settings['posts_per_run'] ?? $config['default_posts_per_run'] ) );
                if ( $target > 0 ) {
                    delete_transient( $lock_key );
                    error_log( 'DBA StartupHub catch-up: Zero output with target=' . $target . ' — cleared daily lock for retry.' );
                }
            } catch ( \Exception $e ) {}
        }
    }

    /* ──────────────────────────────────────────────
     *  MAIN CRON CALLBACK
     * ────────────────────────────────────────────── */

    /**
     * Main cron entry point.
     *
     * NOTE: Conflict Center locking is handled inside Generator::run_batch().
     * We do NOT acquire the lock here to avoid a double-lock deadlock.
     */
    public function run() {
        $config   = DBA_Startup_Hub_Registry::get_config();
        $settings = get_option( $config['settings_option'], array() );
        if ( empty( $settings['enabled'] ) ) {
            error_log( 'DBA StartupHub: Module disabled. Skipping.' );
            return;
        }

        try {
            $tz  = new DateTimeZone( self::TIMEZONE );
            $now = new DateTime( 'now', $tz );
        } catch ( \Exception $e ) {
            $now = new DateTime( 'now' );
        }

        // Check posting day
        $posting_days = $settings['posting_days'] ?? array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday' );
        $today_name   = strtolower( $now->format( 'l' ) );
        if ( ! in_array( $today_name, $posting_days, true ) ) {
            error_log( 'DBA StartupHub: Today (' . $today_name . ') is not a posting day. Skipping.' );
            return;
        }

        // Daily lock
        $lock_key = 'dba_startup_hub_ran_' . $now->format( 'Y-m-d' );
        if ( get_transient( $lock_key ) ) {
            error_log( 'DBA StartupHub: Already ran today. Skipping.' );
            return;
        }
        set_transient( $lock_key, true, DAY_IN_SECONDS );

        error_log( 'DBA StartupHub Scheduler: ' . $now->format( 'l H:i' ) . ' ET — starting generation.' );

        $generator = DBA_Startup_Hub_Generator::get_instance();
        $results   = $generator->run_batch();

        error_log( 'DBA StartupHub Scheduler: Complete — created: ' . $results['created'] . ', failed: ' . $results['failed'] );

        // If run_batch was blocked by Conflict Center, clear today's lock so
        // the catch-up layer can retry on the next page load.
        if ( $results['created'] === 0 && $results['failed'] === 0 ) {
            $config   = DBA_Startup_Hub_Registry::get_config();
            $settings = get_option( $config['settings_option'], array() );
            $target   = max( 1, intval( $settings['posts_per_run'] ?? $config['default_posts_per_run'] ) );
            if ( $target > 0 ) {
                delete_transient( $lock_key );
                error_log( 'DBA StartupHub Scheduler: Zero output with target=' . $target . ' — cleared daily lock for retry.' );
            }
        }
    }

    /* ──────────────────────────────────────────────
     *  ADMIN INFO
     * ────────────────────────────────────────────── */

    /**
     * Get posting days as a human-readable string.
     *
     * Called by DBA_Startup_Hub_Admin::render_page() (line 508).
     *
     * @since 2.17.0
     * @return string  e.g. "Monday, Tuesday, Wednesday, Thursday, Friday"
     */
    public static function get_posting_days_string() {
        $info = self::get_schedule_info();
        return $info['posting_days'];
    }

    /**
     * Get next scheduled run as a human-readable string.
     *
     * Called by DBA_Startup_Hub_Admin::render_page() (line 509).
     *
     * @since 2.17.0
     * @return string  e.g. "Monday, 20 February 2026 at 09:00 (America/New_York)"
     */
    public static function get_next_run_info() {
        $info = self::get_schedule_info();
        return $info['next_run'];
    }

    public static function get_schedule_info() {
        try {
            $tz  = new DateTimeZone( self::TIMEZONE );
            $now = new DateTime( 'now', $tz );
        } catch ( \Exception $e ) {
            return array(
                'next_run'     => 'Error calculating',
                'timezone'     => self::TIMEZONE,
                'posting_days' => '',
                'cron_active'  => false,
                'lock_active'  => false,
            );
        }

        $ts = wp_next_scheduled( self::CRON_HOOK );
        if ( $ts ) {
            $next_dt = new DateTime( '@' . $ts );
            $next_dt->setTimezone( $tz );
            $next_run = $next_dt->format( 'l, j F Y \a\t H:i' ) . ' (' . self::TIMEZONE . ')';
        } else {
            $next_run = '⚠️ NOT SCHEDULED — visit admin to auto-fix';
        }

        $config   = DBA_Startup_Hub_Registry::get_config();
        $settings = get_option( $config['settings_option'], array() );
        $days     = $settings['posting_days'] ?? array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday' );

        return array(
            'next_run'     => $next_run,
            'timezone'     => self::TIMEZONE,
            'posting_days' => implode( ', ', array_map( 'ucfirst', $days ) ),
            'cron_active'  => (bool) $ts,
            'lock_active'  => (bool) get_transient( 'dba_startup_hub_ran_' . $now->format( 'Y-m-d' ) ),
        );
    }
}

/*
 * ══════════════════════════════════════════════════════════════
 *  SELF-BOOTSTRAP
 *
 *  Ensures get_instance() is always called so the cron listener
 *  and self-healing hooks are registered. This eliminates the
 *  need for an external bootstrap file to call it manually.
 * ══════════════════════════════════════════════════════════════
 */
add_action( 'plugins_loaded', function() {
    DBA_Startup_Hub_Scheduler::get_instance();
}, 20 );
